CHAPTER 25

image

User-Defined Conversions

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 are declared. This means that conversions can’t be declared between two existing types, which makes the language simpler.

A Simple Example

This example implements a struct that stores Roman numerals. It could also be written as a class, but since it acts like a built-in value type, a struct makes more sense.

struct RomanNumeral
{
    public RomanNumeral(short value)
    {
       if (value > 5000)
       {
            throw (new ArgumentOutOfRangeException());
       }
       m_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.m_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.m_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 m_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 conversion from a short to a RomanNumeral. The conversion is declared as an explicit conversion because it isn’t a conversion that will always succeed; in this case, it may throw an exception if the number is bigger than the magnitude supported by the struct. There is a conversion to short that is declared implicit, because the value in a RomanNumeral will always fit in a short. And finally, there’s a conversion to string that gives the romanized version of the number.1

When an instance of this struct is created, the constructor can be used to set the value. An explicit conversion can be used to convert the integer value to a RomanNumeral. To get the romanized version of the RomanNumeral, you could write the following:

Console.WriteLine(roman);

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

In the example, an explicit cast is used to disambiguate, but it’s a bit ugly. It would probably be best to overload ToString() and remove the string conversion.

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 that were declared in the struct itself. A user-defined conversion can also be used in scenarios where the source or destination types are not exact matches to the types in the conversion functions.

If the source or destination types are not exact matches, then the appropriate standard (i.e., 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 clearer. In the preceding example, the following line

short s = numeral;

calls the implicit user-defined conversion directly. Since this is an implicit use of the user-defined conversion, there can also be another implicit conversion 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, there was the following conversion in the example:

numeral = (RomanNumeral) 165;

Since the usage is explicit, the explicit conversion from int to RomanNumeral is used. Also, an additional 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.

Conversions Between Structs

User-defined conversions that deal with classes or structs rather than basic types work in a similar way, except that there are a few more situations to consider. Since the user conversion can be defined in either the source or destination type, there’s a bit more design work to do, and the operation is a bit more complex. For details, see the “How It Works” section later in this chapter.

Adding to the RomanNumeral example in the previous section, a struct that handles binary numbers can be added.

struct BinaryNumeral
{
    public BinaryNumeral(int value)
    {
       m_value = value;
    }
    public static implicit operator BinaryNumeral(int value)
    {
       return new BinaryNumeral(value);
    }
    public static implicit operator int(BinaryNumeral binary)
    {
       return binary.m_value;
    }
    public static implicit operator string(BinaryNumeral binary)
    {
       StringBuilder retval = new StringBuilder();
       return retval.ToString();
    }
    private int m_value;
}
class Test
{
    public static void Main()
    {
       RomanNumeral roman = new RomanNumeral(12);
       BinaryNumeral binary;
       binary = (BinaryNumeral)(int)roman;
    }
}

The classes can be used 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 would be nice to write the Main() function as follows:

binary = roman;
roman = (RomanNumeral) binary;

This makes 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.

To get this, a user-defined conversion is required on either the RomanNumeral or 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.

The classes are modified as follows, adding two conversions:

struct RomanNumeral
{
    public static implicit operator BinaryNumeral(RomanNumeral roman)
    {
       return new BinaryNumeral((short) roman);
    }
    public static explicit operator RomanNumeral(
    BinaryNumeral binary)
    {
       return new RomanNumeral((short) binary);
    }
}
class Test
{
    public static void Main()
    {
       RomanNumeral roman = new RomanNumeral(122);
       BinaryNumeral binary;
       binary = roman;
       roman = (RomanNumeral) binary;
    }
}

With these added user-defined conversions, conversions between the RomanNumeral and BinaryNumeral types can now take place.

Classes and Pre- and Post-Conversions

As with basic types, classes can also 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 considered.

For implicit conversions, it’s pretty simple, and 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, the example will be modified to use classes rather than structs, and a new class that derives from RomanNumeral will be added.

class RomanNumeral
{
    public RomanNumeral(short value)
    {
       if (value > 5000)
       {
            throw(new ArgumentOutOfRangeException());
       }
       m_value = value;
    }
    public static explicit operator RomanNumeral(short value)
    {
       return new RomanNumeral(value);
    }
    public static implicit operator short(RomanNumeral roman)
    {
       return roman.m_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.m_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 m_value;
}
class BinaryNumeral
{
    public BinaryNumeral(int value)
    {
       m_value = value;
    }
    public static implicit operator BinaryNumeral(int value)
    {
       return new BinaryNumeral(value);
    }
    public static implicit operator int(BinaryNumeral binary)
    {
       return binary.m_value;
    }
    public static implicit operator string(BinaryNumeral binary)
    {
       StringBuilder    retval = new StringBuilder();
       return retval.ToString();
    }
    private int m_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 is performed.

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.

The conversion could be rewritten as follows:

using System;
using System.Text;
class RomanNumeral
{
    public RomanNumeral(short value)
    {
       if (value > 5000)
       {
            throw(new ArgumentOutOfRangeException());
       }
       m_value = value;
    }
    public static implicit operator short(RomanNumeral roman)
    {
       return roman.m_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.m_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 m_value;
}
class BinaryNumeral
{
    public BinaryNumeral(int value)
    {
       m_value = value;
    }
    public static implicit operator BinaryNumeral(int value)
    {
       return new BinaryNumeral(value);
    }
    public static implicit operator int(BinaryNumeral binary)
    {
       return binary.m_value;
    }
    public static implicit operator string(BinaryNumeral binary)
    {
       StringBuilder retval = new StringBuilder();
       return retval.ToString();
    }
    private int m_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 a RomanNumeralAlternate object, and it’s perfectly legal for that to be a reference to a derived type. 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.

Design Guidelines

When designing user-defined conversions, the following guidelines should be considered.

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.

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, there is no option but to define the conversion as part of the class, since the source isn’t available.

Even if the source were available, however, it would be really 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 there is no obvious simpler class. In that case, pick a class, and put both conversions there.

One Conversion to and from a Hierarchy

In my examples, there was 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 is good practice to do this and then to 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 is the most natural size for the type.

For example, in the BinaryNumeral class, there’s an implicit conversion to int. If the user wants a smaller type, such as short, a cast can easily be done.

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.

Add Conversions Only As Needed

I’ve seen a collection class that defined an implicit conversion to integer, which returned the count of items in the class. Don’t do the unexpected with user-defined conversions; extraneous conversions only make the user’s life harder.

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, alternate versions of the conversions should be supplied. If, for example, an object supports a conversion to string, it should also support calling ToString() on that function. Here’s how it would be done on the RomanNumeral class:

using System;
using System.Text;
class RomanNumeral
{
    public RomanNumeral(short value)
    {
       if (value > 5000)
       {
            throw(new ArgumentOutOfRangeException());
       }
       m_value = value;
    }
    public static explicit operator RomanNumeral(short value)
    {
       return new RomanNumeral(value);
    }
    public static implicit operator short(RomanNumeral roman)
    {
       return roman.m_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.m_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 m_value;
}

The ToString() function is an override because it overrides the ToString() method in object.

How It Works

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

This section can be safely skipped.

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, 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 class T isn’t searched.

1 No, this struct doesn’t handle niceties such as replacing “IIII” with “IV,” nor does it handle converting the romanized string to a short. The remainder of the implementation is left as an exercise for the reader.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.26.4