Type Conversions And The Implicit and Explicit Operators In C#

Posted by : on

Category : C#

Introduction

In C#, we can convert instances of one type to another in various ways. Although the methods described below can be used for any kind of conversion, certain conventions are typically followed for each type of conversion.

Type conversions for existing primitive types are already built into the language. Type conversions for Base Class Library (BCL) types also exist, and the language provides mechanisms for creating conversions between user-defined types.

In this post, I will start by describing how implicit and explicit conversions for built-in types work. Then, I will explain how to overload the implicit and explicit operators to create conversions for custom types. After that, I will describe how conversions work through constructor parameters, using static methods, and finally, using “ToSomething” and “AsSomething” instance methods.

As mentioned, these different conversion methods can be used for any type of conversion, but there are conventions for when each should be used.

Implicit And Explicit Conversions

Implicit conversions happen automatically, for example:

float f  = 0.1f;
double d = f;

Explicit conversions happen with a cast:

float f  = 0.1f;
int i = (int)f;

There is a convention for when to use implicit or explicit conversions, which should be followed when overloading the implicit and explicit operators in our own types.

An implicit conversion occurs when the conversion is guaranteed to succeed without any loss of information from one type to another.

In contrast, explicit conversions are used when the conversion may not always succeed, or when some information might be lost during the conversion. For example:

double d = 1e-50;
float f = (float)d;

Console.WriteLine(d); // prints 1E-50
Console.WriteLine(f); // prints 0

This convention should be followed, when we overload the implicit and explicit operators for our own types.

Overloading The Implicit And Explicit Operators

In C#, we can overload the implicit and explicit operators to create conversions for instances of types we have created. Although we can create conversions between any types, it’s best to reserve the implicit and explicit operators for conversions between types that represent the same concept but differ in their implementation.

For example, let’s suppose we have created two types: Human and Person. Both have the same fields, but there is one difference: the Human type has a DateOfBirth field containing a date with a year, a month, and a day, while the Person type has an Age field that is an integer representing the age in years.

Both of these types can have a method called GetAgeInYears that returns the age, but it is calculated differently. The method in the Human type subtracts the DateOfBirth from the current date and returns how many years have passed, while the Person type returns the value in the Age field.

Because both types represent the same concept, we can overload the implicit and explicit operators to create conversions between them. If we want to create a new Human from an existing Person, we can use an implicit conversion because no information is lost. However, if we want to create a new Person from an existing Human instance, then we should use an explicit conversion because we will lose the information about the number of months and days that have passed since the person’s birth.

The technical usage of the implicit and explicit operators is simple:

public static implicit operator Human(Person person) 
{
    // code that returns a new Human here
}

public static explicit operator Person(Human human) 
{
    // code that returns a new Person here
}

From C# 11 we are allowed to have checked explicit conversions. See Overloading the checked operators in C# 11

Conversions Using Constructors And Static From Methods

When we want to perform conversions between types that don’t represent the same concept, we should not do it by overloading the explicit and implicit operators. Instead, we should use a method to do it. Let’s suppose that we want to have a “Student” class that has all the information of a Person plus additional fields about that person’s student status. We have two ways of creating a Student instance from a Person instance.

The first way is to have a constructor that takes a Person instance as a parameter. Then, we can either apply default values to the extra fields of the Student instance, or we can explicitly include more parameters in the constructor to initialize those fields.

The second way is to have a static method of the Student type that also takes a Person instance as a parameter and returns a new instance of the Student type. For example something like this:

Person mike = new Person();
Student mikeStudent = Student.FromPerson(mike);

Conversions Using To And As Methods

We can also have methods that convert an existing instance to another type. These methods don’t actually convert the existing instance, but they return a reference to an instance of the desired type. By convention, these methods have the “To” and “As” prefixes.

The “To” prefix indicates that we will have a new memory allocation because a new instance of the desired type will be created. For example, if our Student type had fields that correspond to a Person’s fields, like first and last name and age, then the statement:

Person mike = mikeStudent.ToPerson();

will allocate memory by creating a new instance of Person.

In contrast, the “As” prefix indicates, by convention, that no new memory will be allocated. This can happen if we can return an instance of the desired type that has already been created with the creation of the original type.

For example, our Student type, instead of having fields for first and last name and age, could have a Person field that contains that information. The statement:

Person mike = mikeStudent.AsPerson();

would return the already instantiated Person field that exists in the Student class.

Other types of conversions

There are some other types of conversions that are supported by C#. These conversions are part of the language, and we don’t have the ability to change how they are done, we can only use them.

Reference Conversions

Reference conversions, are conversions that change the type of one instance to another type.

A type can derive from a base class or an interface. This means that we can have an implicit conversion to that type a process known as upcasting. For example:

public interface IInterface { }
public class Base : IParent { }
public class Derived : Base { }

Derived child = new Derived();

Base parent = child;

IInterface grandParent = child;

The converted instance, doesn’t change. Both the parent field and the child field point to the same instance. The difference is that the parent is more restricted to what it can do, as its available methods, are limited to those that exist only in the Base type.

To perform the opposite conversion, from a parent to a child, we can only do it explicitly, a process known as downcasting. This is because there is always the chance that this conversion cannot be done and may fail at runtime.

Derived child2 = (Derived)parent;

Boxing and Unboxing

With Boxing, we have the conversion of a value type instance to a reference type instance. The unboxing conversion does the reverse. Unboxing is done explicitly, as it can fail at runtime.

Here’s a simple example:

int i = 0;
object o = i; // boxing
int i2 = (int)o; // unboxing

Boxing, unboxing and by extension reference vs value types, is a very big subject and out of the scope of this post, I included it here for the sake of completion of the conversions that can happen in C#.

The is and as Keywords

Finally, we have two keywords that can help with conversions.

The as keyword can perform a downcast the same way an explicit downcast does, but won’t throw an exception if it fails. Instead, will return null. For example:

Derived child2 = parent as Derived;

The is keyword, will check in the context of conversions, if a conversion can happen. For example:

if(parent is Derived der) 
   Console.WriteLine("Parent is Derived");

Here the is not only checks if the parent can be downcast to Derived but also introduces a new variable named der. This variable will remain in scope, outside the is expression and will be a reference of the type Derived to the instance that is referenced by the parent field or null, if the conversion is not possible.

Finally, the as keyword, cannot be used for numeric or custom conversions.

Conclusion

That’s all about conversions in C#. Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: