
A professional code developer needs solid foundation. Our daily work can drive us away from what we have studied, as well as the routine can push us to repeat always the same mistakes. A review of the fundamentals is a useful exercise, can’t be bad! So let’s take advantage of this post to talk about our favorite programming language.
C# is static/strongly-typed in compile-time. After declaring a variable, it cannot be assigned to a value of another type unless this type is implicitly convertible to the type of the variable itself. So we can have these types of conversions:
- Implicit: for this type of conversion we don’t need any particular syntax and there will be no data loss
- Explicit: when there is data loss possibility (for example if we try to convert a variable of a given type that has a value greater than the maximum value that can be stored in the type of the host variable), the conversion is called narrowing. The compiler requires us (otherwise it doesn’t compile) to perform an explicit conversion called cast. A cast is performed by specifying, between round brackets, the type of data before the variable or value to be converted. We are thus informing the compiler that we are aware of the possibility of information loss.
For numeric types, when there is the certainty of no information loss, because the type of the host variable can certainly contain the value of the variable to be converted, the conversion is called widening.
int numInt = 1000;
long numLong = i;
For reference types there is always an implicit conversion from a class to a direct or indirect base class or to implemented interfaces.
Dipendente dip = new Dipendente();
Persona pers = dip;
long numLong = 2000;
int numInt = (int)numLong;
Note: This assignment compiles correctly, but it may generate an exception (OverflowException) at runtime if the value of the type to be converted (numLong) is outside the range of the destination variable type (numInt).
The same happens for reference types. If we want to convert a variable of the base class type to a derived class type, we need to perform an explicit cast.
Persona pers = new Persona();
Dipendente dip = (Dipendente)pers;
Note: This assignment compiles correctly, but it may generate an exception (InvalidCastException) at runtime if the object on the right (pers) is not of the type specified with the cast (Employee).
And if the conversion would take place between types that are not compatible with each other? In this case, helper class methods can help us. If we wanted, for example, convert a string to a number, we could use the Parse or TryParse methods.
string testo = "100";
int numero = int.Parse(testo);
If we wanted to convert a byte array to an Int64, we could use the BitConverter class and its method ToInt64(byte[] value, int startIndex):
byte[] bytes = {3,5,2,8};
Int64 valoreInt64 = BitConverter.ToInt64( bytes, 0 );
How is treated, instead, the conversion between classes defined by us? Also in this case, we might need to assign a different type to a variable of a given type.
C # allows us to create static methods that use special operators (implicit and explicit) in their statements and to specify that way conversions within a class, allowing us to convert it to and /or in another class. All without inheritance relationships between them or an interface that join them. In theory, therefore, no conversion would be possible. Let’s look at a practical example to clarify our ideas. This code implicitly converts from the NewProduct type to the OldProduct type:
class VecchioProdotto
{
public static implicit operator VecchioProdotto(NuovoProdotto np)
VecchioProdotto vp = new VecchioProdotto();
vp.nomeProdotto = np.nome;
vp.descrizioneProdotto = np.descrizione;
vp.matricola = np.codiceIdentificativo;
return vp;
}
In this static method, which will be used automatically in the conversion phase, we have mapped the properties of OldProduct class on those of the NewProduct class. Therefore, we can now write:
VecchioProdotto vp = np;
In this case, instead, we see an explicit conversion function (which requires a cast) that converts from the OldProduct to the NewProduct type:
class NuovoProdotto //Avremmo potuto indifferentemente inserire la funzione all'interno della classe VecchioProdotto
{
public static explicit operator NuovoProdotto(VecchioProdotto vp)
NuovoProdotto np = new NuovoProdotto();
np.nome = vp.nomeProdotto;
np.descrizione = vp.descrizioneProdotto;
np.codiceIdentificativo = vp.matricola + vp.idTipologia;
return np;
}
In this case we will write:
NuovoProdotto npr = (NuovoProdotto)vp;
User-defined type conversions can take place as well as between custom types, as we have already seen, even between a base type and a non-compatible custom type. However, it is not possible to define again an already existing conversion (implicit or explicit). We can declare a conversion function with the implicit and explicit operators from a source type to a destination type, following these rules:
- Neither the origin nor the destination must be of an interface or object type
- The source or target is a class or struct within which occurs the operator’s declaration.
- In case of conversion between two classes, the declaration can take place either in the origin class or in the destination class.
- The origin and destination must not be base classes or derived from each other.
- The function must be public and static, there is no return type and the name must coincide with the destination type
Ultimately, if a user-defined conversion can result in exceptions or data loss, then it must be defined with the explicit operator.
User-defined conversions with the implicit operator must be designed so that they never generate exceptions and that there is no data loss.
I hope it could be useful
See you next time!