User-Defined Conversions in C#: Design Guidelines and How It Works

1. Design Guidelines

When designing user-defined conversions, you should consider the following guidelines.

1.1. Implicit Conversions Are Safe Conversions

When defining conversions between types, the only conversions that should be implicit ones are those that don’t lose any data and don’t throw exceptions.

This is important, because implicit conversions can occur without it being obvious that a conversion has occurred.

1.2. Define the Conversion in the More Complex Type

This basically means not cluttering up a simple type with conversions to a more complex one. For conversions to and from one of the predefined types, you have no option but to define the conversion as part of the class, since the source isn’t available.

Even if the source is available, however, it’s strange to define the conversions from int to BinaryNumeral or RomanNumeral in the int class.

Sometimes, as in the example, the classes are peers to each other, and no obvious simpler class exists. In that case, pick a class, and put both conversions there.

1.3. One Conversion to and from a Hierarchy

The examples in this chapter had only a single conversion from the user-defined type to the numeric types and one conversion from numeric types to the user-defined type. In general, it’s good practice to do this and then use the built-in conversions to move between the destination types. When choosing the numeric type to convert from or to, choose the one that’s the most natural size for the type.

For example, the BinaryNumeral class contains an implicit conversion to int. If the user wants a smaller type, such as short, you can easily perform a cast.

If multiple conversions are available, the overloading rules will take effect, and the result may not always be intuitive for the user of the class. This is especially important when dealing with both signed and unsigned types.

1.4. Add Conversions Only As Needed

Extraneous conversions only make the user’s life harder.

1.5. Conversions That Operate in Other Languages

Some of the .NET languages don’t support the conversion syntax, and calling conversion functions—which have weird names—may be difficult or impossible. To make classes easily usable from these languages, you should supply alternate versions of the conversions. If, for example, an object supports a conversion to a string, it should also support calling ToString() on that function. Here’s how you’d do it on the RomanNumeral class:

using System;

using System.Text;

class RomanNumeral {

public RomanNumeral(short value)

{

if (value > 5000)

throw(new ArgumentOutOfRangeException());

this.value = value;

}

public static explicit operator RomanNumeral( short value)

{

RomanNumeral retval;

retval = new RomanNumeral(value);

return(retval);

}

public static implicit operator short(

RomanNumeral roman)

{

return(roman.value);

}

static string NumberString(

ref int value, int magnitude, char letter)

{

StringBuilder numberString = new StringBuilder();

while (value >= magnitude)

{

value -= magnitude;

numberString.Append(letter);

}

return(numberString.ToString());

}

public static implicit operator string(

RomanNumeral roman)

{

int                  temp = roman.value;

StringBuilder retval = new StringBuilder();

retval.Append(RomanNumeral.NumberString(ref temp, 1000, ‘M’)); retval.Append(RomanNumeral.NumberString(ref temp, 500, ‘D’)); retval.Append(RomanNumeral.NumberString(ref temp, 100, ‘C’)); retval.Append(RomanNumeral.NumberString(ref temp, 50, ‘L’)); retval.Append(RomanNumeral.NumberString(ref temp, 10, ‘X’)); retval.Append(RomanNumeral.NumberString(ref temp, 5, ‘V’)); retval.Append(RomanNumeral.NumberString(ref temp, 1, ‘I’));

return(retval.ToString());

}

public short ToShort()

{

return((short) this);

}

public override string ToString() {

return((string) this);

}

private short value;

}

The ToString() junction is an override because it overrides the ToString() version in object.

2. How It Works

To finish the section on user-defined conversions, a few details on how the compiler views conversions warrant a bit of explanation. Those who are really interested in the gory details can find them in the C# Language Reference.

You can safely skip the following sections if you’d like.

2.1. Conversion Lookup

When looking for candidate user-defined conversions, the compiler will search the source class and all of its base classes and the destination class and all of its base classes.

This leads to an interesting case:

public class S {

public static implicit operator T(S s)

{

// conversion here return(new T());

}

}

public class TBase {

}

public class T: TBase {

}

public class Test {

public static void Main()

{

S myS = new S();

TBase tb = (TBase) myS;

}

}

In this example, the compiler will find the conversion from S to T and, since the use is explicit, match it for the conversion to TBase, which will succeed only if the T returned by the conversion is really only a TBase.

Revising things a bit by removing the conversion from S and adding it to T, you get this:

// error class S

{

}

class TBase

{

}

class T: TBase {

public static implicit operator T(S s)

{

return(new T());

}

}

class Test {

public static void Main()

{

S myS = new S();

TBase tb = (TBase) myS;

}

}

This code doesn’t compile. The conversion is from S to TBase, and the compiler can’t find the definition of the conversion, because it doesn’t search class T.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *