Avoid Conversion Operators

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.

Resources

  1. More Effective C#, Bill Wagner

Site Footer

Subscribe To My Newsletter

Email address