In this article I will examine the conversion operators in C# and focus concretely on a specific usage scenario which can lead to subtle bugs in your application. I’ve recently seen such a problematic piece of code so decided to share my experience.
Conversion operators in C# provide a mechanism for types substitutability meaning that one type can be passed in where another type is expected. Typically, in OOP we achieve that via polymorphism e.g. if class B
inherits from class A
, B
is a specialization of A
and we can substitute A
for B
. Conversion operators seemingly achieve the same goal but can be dangerous in particular circumstances.
Here’s an example of a conversion operator. Below we have two classes – A
and B
with B
being implicitly convertible to A
:
public class A { public int Id { get; set; } public static implicit operator A(B source) // Conversion operator { return new A { Id = source.Id }; } } public class B { public int Id { get; set; } }
We can now pass an object of type B
everywhere an object of type A
is expected. This can be problematic if object A
’s state gets mutated. To demonstrate this, let’s define the following method:
public static void SetId(A obj, int id) { obj.Id = id; }
This is probably not the most useful method you’ve seen but it’s a very simple setup to show the problematic behavior. Now, suppose we have the following driver code:
static void Main(string[] args) { var myObj = new B(); SetId(myObj, 1); Console.WriteLine(myObj.Id); // 0 }
It’s quite normal by just looking at this code to expect it to output a value of 1
. This is not the case, however, and the result we get is 0
, so for some reason the Id
field of myObj
does not get modified in the SetId
method. With such a simple example it may not take you long until you figure out what’s wrong here. The reason myObj
is not changed is that when passing an object of type B
to a method expecting an object of type A
, the conversion operator will be invoked which creates a new object of type B
and this is what gets passed to the SetId
method. To put it in another way, myObj
is not passes to SetId
but a temporary object produced by the conversion operator. This temp object is alive only during the SetId
execution and becomes garbage immediately after that.
To fix this, and more importantly – avoid such scenarios in future, I think it’s better if we implement the type conversion more explicitly. One approach would be to simply create a new constructor overload for producing A
objects from B
objects:
public class A { public A(B source) { this.Id = source.Id; } // Rest of class A }
Let’s now see how it looks if someone produces similar client code as the previous one but this time creating the A
object from B
via A
’s constructor:
static void Main(string[] args) { var myObj = new B(); SetId(new A(myObj), 1); Console.WriteLine(myObj.Id); // 0 }
Now it’s immediately apparent that SetId
will work on a temporary object and no one would expect myObj
to get modified. The potential bug can be spotted at first glance! Most developers in this case would just assign the new A
object to a local variable and pass that into the function so the state changes are reflected after the method returns. Like so:
static void Main(string[] args) { var myObj = new B(); var fromB = new A(myObj); SetId(fromB, 1); Console.WriteLine(fromB.Id); // 1 }
In most cases, I would usually take a step further and instead of a constructor overload I would use a static factory method. But I guess this depends more on the specific use case and context.
For completeness, I should mention that conversion operators can also be defined as explicit
. In our example this just means changing the implicit
keywork with explicit
:
public static explicit operator A(B source)
The effect this will have is that the user will be forced to add a cast to the SetId
call:
SetId((A)myObj, 1);
The original problem remains though. A temp object would still be created by the conversion operator and thrown away right after SetId returns.
Summary
In this article I’ve introduced the conversion operators in C# and demonstrated how they can cause problems under certain conditions.
Thanks for reading.