[Previous] [Contents]

Syntax and Example

The syntax of the user-defined conversion uses the operator keyword to declare user-defined conversions: -

public static implicit operator conv-type-out (conv-type-in operand) -

public static explicit operator conv-type-out (conv-type-in operand) -

There are only a couple of rules regarding the syntax of defining conversions: -

  • Any conversion method for a struct or class-you can define as many as you need-must be static.
  • Conversions must be defined as either implicit or explicit. The implicit keyword means that the cast is not required by the client and will occur automatically. Conversely, using the explicit keyword signifies that the client must explicitly cast the value to the desired type.
  • All conversions either must take (as an argument) the type that the conversion is being defined on or must return that type.
  • As with operator overloading, the operator keyword is used in the conversion method signature but without any appended operator.

I know the first time I read these rules, I didn't have the foggiest idea what to do, so let's look at an example to crystallize this. In this example, we have two structures (Celsius and Fahrenheit) that enable the client to convert a value of type float to either temperature scale. I'll first present the Celsius structure and make some points about it, and then you'll see the complete working application.

struct Celsius
{
    public Celsius(float temp)
    {
        this.temp = temp;
    }
    public static implicit operator Celsius(float temp)
    {
        Celsius c;
        c = new Celsius(temp);
        return(c);
    }
    public static implicit operator float(Celsius c)
    {
        return((((c.temp - 32) / 9) * 5));
    }
    public float temp;
}

The first decision that you see was the one to use a structure instead of a class. I had no real reason for doing that other than the fact that using classes is more expensive than using structures-in terms of how the classes are allocated-and a class is not really necessary here because the Celsius structure doesn't need any C# class-specific features, such as inheritance.

Next notice that I've declared a constructor that takes a float as its only argument. This value is stored in a member variable named temp. Now look at the conversion operator defined immediately after the structure's constructor. This is the method that will be called when the client attempts to cast a float to Celsius or use a float in a place, such as with a method, where a Celsius structure is expected. This method doesn't have to do much, and in fact this is fairly formulaic code that can be used in most basic conversions. Here I simply instantiate a Celsius structure and then return that structure. That return call is what will cause the last method defined in the structure to be called. As you can see, the method simply provides the mathematical formula for converting from a Fahrenheit value to a Celsius value.

Here's the entire application, including a Fahrenheit structure: -

using System;
struct Celsius
{
    public Celsius(float temp)
    {
        this.temp = temp;
    }
    public static implicit operator Celsius(float temp)
    {
        Celsius c;
        c = new Celsius(temp);
        return(c);
    }
    public static implicit operator float(Celsius c)
    {
        return((((c.temp - 32) / 9) * 5));
    }
    public float temp;
}
struct Fahrenheit
{
    public Fahrenheit(float temp)
    {
        this.temp = temp;
    }
    public static implicit operator Fahrenheit(float temp)
    {
        Fahrenheit f;
        f = new Fahrenheit(temp);
        return(f);
    }
    public static implicit operator float(Fahrenheit f)
    {
        return((((f.temp * 9) / 5) + 32));
    }
    public float temp;
}
class Temp1App
{
    public static void Main()
    {
        float t;
        t=98.6F;
        Console.Write("Conversion of {0} to Celsius = ", t);
        Console.WriteLine((Celsius)t);
        t=0F;
        Console.Write("Conversion of {0} to Fahrenheit = ", t);
        Console.WriteLine((Fahrenheit)t);
    }
}

If you compile and execute this application, you get this output: -

    Conversion of 98.6 to Celsius = 37
    Conversion of 0 to Fahrenheit = 32

This works pretty well, and being able to write (Celsius)98.6F is certainly more intuitive than calling some static class method. But note that you can pass only values of type float to these conversion methods. For the application above, the following won't compile: -

    Celsius c = new Celsius(55);
    Console.WriteLine((Fahrenheit)c);

Also, because there's no Celsius conversion method that takes a Fahrenheit structure (or vice versa), the code has to assume that the value being passed in is a value that needs converting. In other words, if I call (Celsius)98.6F, I will receive the value 37. However, if that value is then passed back to the conversion method again, the conversion method has no way of knowing that the value has already been converted and logically already represents a valid Celsius temperature-to the conversion method, it's just a float. As a result, the value gets converted again. Therefore, we need to modify the application so that each structure can take as a valid argument the other structure.

When I originally thought of doing this, I cringed at the thought because I worried about how difficult this task would be. As it turns out, it's extremely easy. Here's the revised code with ensuing comments: -

using System;
class Temperature
{
    public Temperature(float Temp)
    {
        this.temp = Temp;
    }
    protected float temp;
    public float Temp
    {
        get
        {
            return this.temp;
        }
    }
}
class Celsius : Temperature
{
    public Celsius(float Temp)
        : base(Temp) {}
    public static implicit operator Celsius(float Temp)
    {
        return new Celsius(Temp);
    }
    public static implicit operator Celsius(Fahrenheit F)
    {
        return new Celsius(F.Temp);
    }
    public static implicit operator float(Celsius C)
    {
        return((((C.temp - 32) / 9) * 5));
    }
}
class Fahrenheit : Temperature
{
    public Fahrenheit(float Temp)
        : base(Temp) {}
    public static implicit operator Fahrenheit(float Temp)
    {
        return new Fahrenheit(Temp);
    }
    public static implicit operator Fahrenheit(Celsius C)
    {
        return new Fahrenheit(C.Temp);
    }
    public static implicit operator float(Fahrenheit F)
    {
        return((((F.temp * 9) / 5) + 32));
    }
}
class Temp2App
{
    public static void DisplayTemp(Celsius Temp)
    {
        Console.Write("Conversion of {0} {1} to Fahrenheit = ",
            Temp.ToString(), Temp.Temp);
        Console.WriteLine((Fahrenheit)Temp);
    }
    public static void DisplayTemp(Fahrenheit Temp)
    {
        Console.Write("Conversion of {0} {1} to Celsius = ",
            Temp.ToString(), Temp.Temp);
        Console.WriteLine((Celsius)Temp);
    }
    public static void Main()
    {
        Fahrenheit f = new Fahrenheit(98.6F);
        DisplayTemp(f);
        Celsius c = new Celsius(0F);
        DisplayTemp(c);
    }
}

The first thing to note is that I changed the Celsius and Fahrenheit types from struct to class. I did that so that I would have two examples-one using struct and one using class. But a more practical reason for doing so is to share the temp member variable by having the Celsius and Fahrenheit classes derive from the same Temperature base class. I can also now use the inherited (from System.Object) ToString method in the application's output.

The only other difference of note is the addition of a conversion for each temperature scale that takes as an argument a value of the other temperature scale. Notice how similar the code is between the two Celsius conversion methods: -

public static implicit operator Celsius(float temp)
{
    Celsius c;
    c = new Celsius(temp);
    return(c);
}
public static implicit operator Celsius(Fahrenheit f)
{
    Celsius c;
    c = new Celsius(f.temp);
    return(c);
}

The only tasks I had to do differently were change the argument being passed and retrieve the temperature from the passed object instead of a hard-coded value of type float. This is why I noted earlier how easy and formulaic conversion methods are once you know the basics.

Summary

Operator overloading and user-defined conversions are useful for creating intuitive interfaces for your classes. When using overloaded operators, keep in mind the associated restrictions while designing your classes. For example, while you can't overload the = assignment operator, when a binary operator is overloaded its compound assignment equivalent is implicitly overloaded. Follow the design guidelines for deciding when to use each feature. Keep the class's client in mind when determining whether or not to overload an operator or set of operators. With a little insight into how your clients would use your classes, you can use these very powerful features to define your classes such that certain operations can be performed with a more natural syntax.

[Previous] [Contents]