User-Defined Conversions in C#

C# allows conversions to be defined between classes or structs and other objects in the system. User-defined conversions are always static functions, which must either take as a parameter or return as a return value the object in which they’re declared. This means conversions can’t be declared between two existing types, which makes the language simpler.

1. A Simple Example

The following example implements a struct that handles roman numerals. You could also write it as a class, but since it’s a piece of data, a struct makes more sense.

using System; using System.Text; struct 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());

} private short value;

}

class Test {

public static void Main()

{

short s = 12;

RomanNumeral numeral = new RomanNumeral(s);

s = 165;

numeral = (RomanNumeral) s;

Console.WriteLine(“Roman as int: {0}”, (int)numeral);

Console.WriteLine(“Roman as string: {0}”, (string)numeral);

short s2 = numeral;

}

}

This struct declares a constructor that can take a short value, and it also declares a conver­sion from an integer to a RomanNumeral. The conversion is declared as an explicit conversion because it may throw an exception if the number is bigger than the magnitude supported by the struct. You’ll see a conversion to short that’s declared implicit, because the value in a RomanNumeral will always fit in a short. And finally, you’ll see a conversion to string that gives the romanized version of the number.

When you create an instance of this struct, you can use the constructor to set the value. You can use an explicit conversion to convert the integer value to a RomanNumeral. To get the romanized version of the RomanNumeral, write the following:

Console.WriteLine(roman);

If you do this, the compiler reports that an ambiguous conversion is present. The class includes implicit conversions both to short and to string, and Console.WriteLine() has over­loads that take both versions, so the compiler doesn’t know which one to call.

The example uses an explicit cast to disambiguate, but it’s a bit ugly. Since this struct will likely be used primarily to print the romanized notation, it probably makes sense to change the conversion to the integer to be an explicit one so that the conversion to string is the only implicit one.

2. Pre- and Post-Conversions

In the preceding example, the basic types that were converted to and from the RomanNumeral were exact matches to the types declared in the struct itself. You can also use the user-defined conversions when the source or destination types aren’t exact matches to the types in the conversion functions.

If the source or destination types aren’t exact matches, then the appropriate standard (in other words, built-in) conversion must be present to convert from the source type to the source type of the user-defined conversion and/or from the destination type of the user-defined conversion, and the type of the conversion (implicit or explicit) must also be compatible.

Perhaps an example will be a bit easier to understand. The following line calls the implicit user-defined conversion directly:

short s = numeral;

Since this is an implicit use of the user-defined conversion, another implicit conversion can appear at the end:

int i = numeral;

Here, the implicit conversion from RomanNumeral to short is performed, followed by the implicit conversion from short to long.

In the explicit case, the example has the following conversion:

numeral = (RomanNumeral) 165;

Since the usage is explicit, the explicit conversion from int to RomanNumeral is used. Also, an explicit conversion can occur before the user-defined conversion is called:

long bigvalue = 166;

short smallvalue = 12;

numeral = (RomanNumeral) bigvalue;

numeral = (RomanNumeral) smallvalue;

In the first conversion, the long value is converted by explicit conversion to an integer, and then the user-defined conversion is called. The second conversion is similar, except that an implicit conversion is performed before the explicit user-defined conversion.

3. Conversions Between Structs

User-defined conversions that deal with classes or structs rather than basic types work similarly, except you have a few more situations to consider. Since you can define the user conversion in either the source type or the destination type, you have a bit more design work to do, and the operation is a bit more complex. For details, see the “How It Works” section.

Building on the RomanNumeral example in the previous section, you can add a struct that handles binary numbers like so:

using System;

using System.Text;

struct 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());

}

private short value;

}

struct BinaryNumeral {

public BinaryNumeral(int value)

{

this.value = value;

}

public static implicit operator BinaryNumeral( int value)

{

BinaryNumeral retval = new BinaryNumeral(value);

return(retval);

}

public static implicit operator int(

BinaryNumeral binary)

{

return(binary.value);

}

public static implicit operator string(

BinaryNumeral binary)

{

StringBuilder            retval = new StringBuilder();

return(retval.ToString());

}

private int value;

}

class Test {

public static void Main()

{

RomanNumeral roman = new RomanNumeral(12);

BinaryNumeral          binary;

binary = (BinaryNumeral)(int)roman;

}

}

You can use the classes together, but since they don’t really know about each other, it takes a bit of extra typing. Converting from a RomanNumeral to a BinaryNumeral requires first converting to an int.

It’d be nice to write the Main() function as follows and make the types look like the built- in types, with the exception that RomanNumeral has a smaller range than binary and therefore will require an explicit conversion in that section:

binary = roman;

roman = (RomanNumeral) binary;

To get this, a user-defined conversion is required on either the RomanNumeral class or the BinaryNumeral class. In this case, it goes on the RomanNumeral class (for reasons that should become clear in the “Design Guidelines” section of this chapter).

You can modify the classes as follows, adding two conversions:

using System; using System.Text; struct 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;

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 static implicit operator BinaryNumeral(RomanNumeral roman)

{

return(new BinaryNumeral((short) roman));

}

public static explicit operator RomanNumeral(

BinaryNumeral binary)

{

return(new RomanNumeral((short) binary));

} private short value;

}

struct BinaryNumeral {

public BinaryNumeral(int value)

{

this.value = value;

}

public static implicit operator BinaryNumeral( int value)

{

BinaryNumeral retval = new BinaryNumeral(value);

return(retval);

}

public static implicit operator int(

BinaryNumeral binary)

{

return(binary.value);

}

public static implicit operator string(

BinaryNumeral binary)

{

return(retval.ToString());

}

private int value;

}

class Test

{

public static void Main()

{

RomanNumeral roman = new RomanNumeral(122);

BinaryNumeral binary; binary = roman;

roman = (RomanNumeral) binary;

}

}

With these added conversions, conversions between the two types can now take place.

4. Classes and Pre- and Post-Conversions

As with basic types, classes can have standard conversions that occur either before or after the user-defined conversion, or even before and after. The only standard conversions that deal with classes, however, are conversions to a base or derived class, so those are the only ones covered in this section.

Implicit conversions are pretty simple; the conversion occurs in three steps:

  1. A conversion from a derived class to the source class of the user-defined conversion is optionally performed.
  2. The user-defined conversion occurs.
  3. A conversion from the destination class of the user-defined conversion to a base class is optionally performed.

To illustrate this, you can modify the example to use classes rather than structs and add a new class that derives from RomanNumeral:

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 static implicit operator BinaryNumeral(RomanNumeral roman) {

return(new BinaryNumeral((short) roman));

}

public static explicit operator RomanNumeral(

BinaryNumeral binary)

{

return(new RomanNumeral((short)(int) binary));

}

private short value;

}

class BinaryNumeral {

public BinaryNumeral(int value)

{

this.value = value;

}

public static implicit operator BinaryNumeral( int value)

{

BinaryNumeral retval = new BinaryNumeral(value);

return(retval);

}

public static implicit operator int(

BinaryNumeral binary)

{

return(binary.value);

}

public static implicit operator string(

BinaryNumeral binary)

{

StringBuilder retval = new StringBuilder();

return(retval.ToString());

}

private int value;

}

class RomanNumeralAlternate : RomanNumeral

{

public RomanNumeralAlternate(short value): base(value)

{

}

public static implicit operator string(

RomanNumeralAlternate roman)

{

return(“NYI”);

}

}

class Test {

public static void Main()

{

// implicit conversion section

RomanNumeralAlternate roman;

roman = new RomanNumeralAlternate(55);

BinaryNumeral binary = roman;

// explicit conversion section

BinaryNumeral binary2 = new BinaryNumeral(1500);

RomanNumeralAlternate roman2;

roman2 = (RomanNumeralAlternate) binary2;

}

}

The operation of the implicit conversion to BinaryNumeral is as expected; an implicit conversion of roman from RomanNumeralAlternate to RomanNumeral occurs, and then the user- defined conversion from RomanNumeral to BinaryNumeral takes places.

The explicit conversion section may have some people scratching their heads. The user-defined function from BinaryNumeral to RomanNumeral returns a RomanNumeral, and the post-conversion to RomanNumeralAlternate can never succeed.

You can rewrite the conversion as follows:

using System; using System.Text; class RomanNumeral {

public RomanNumeral(short value)

{

if (value > 5000)

throw(new ArgumentOutOfRangeException());

this.value = value;

}

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 static implicit operator BinaryNumeral(RomanNumeral roman)

{

return(new BinaryNumeral((short) roman));

}

public static explicit operator RomanNumeral( BinaryNumeral binary)

{

int        val = binary;

if (val >= 1000)

return((RomanNumeral)

new RomanNumeralAlternate((short) val));

else

return(new RomanNumeral((short) val));

}

private short value;

}

class BinaryNumeral {

public BinaryNumeral(int value)

{

this.value = value;

}

public static implicit operator BinaryNumeral( int value)

{

BinaryNumeral retval = new BinaryNumeral(value);

return(retval);

}

public static implicit operator int(

BinaryNumeral binary)

{

return(binary.value);

}

public static implicit operator string(

BinaryNumeral binary)

{

StringBuilder retval = new StringBuilder();

return(retval.ToString());

}

private int value;

}

class RomanNumeralAlternate : RomanNumeral {

public RomanNumeralAlternate(short value) : base(value)

{

}

public static implicit operator string( RomanNumeralAlternate roman)

{

return(“NYI”);

}

}

class Test {

public static void Main()

{

// implicit conversion section

RomanNumeralAlternate roman;

roman = new RomanNumeralAlternate(55);

BinaryNumeral binary = roman;

// explicit conversion section

BinaryNumeral binary2 = new BinaryNumeral(1500);

RomanNumeralAlternate roman2;

roman2 = (RomanNumeralAlternate) binary2;

}

}

The user-defined conversion operator now doesn’t return a RomanNumeral; it returns a RomanNumeral reference to an object, and it’s perfectly legal for that to be a reference to a derived type. This is weird, perhaps, but legal. With the revised version of the conversion function, the explicit conversion from BinaryNumeral to RomanNumeralAlternate may succeed, depending on whether the RomanNumeral reference is a reference to a RomanNumeral object or a RomanNumeralAlternate object.

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 *