Chapter 6. Overloading Operators

C# adopted the capability of operator overloading from C++. Just as you can overload methods, you can overload operators such as +, -, *, and so on. In addition to overloading arithmetic operators, you can also create custom conversion operators to convert from one type to another. You can overload other operators to allow objects to be used in Boolean test expressions.

Just Because You Can Doesn't Mean You Should

Overloading operators can make certain classes and structs more natural to use. However, overloading operators in a slipshod way can make code much more difficult to read and understand. You must be careful to consider the semantics of a type's operators. Be careful not to introduce something that is hard to decipher. Always aim for the most readable code, not only for the next fortunate soul who claps eyes with your code, but also for yourself. Have you ever looked at code and wondered, "Who in their right mind wrote this stuff?!" only to find out it was you? I know I have.

Another reason not to overload operators is that not all .NET languages support overloaded operators, because overloading operators is not part of the CLS. Languages that target the CLI aren't required to support operator overloading. For example, Visual Basic 2005 was the first .NET version of the language to support operator overloading. Therefore, it's important that your overloaded operators be syntactic shortcuts to functionality provided by other methods that perform the same operation and can be called by CLS-compliant languages. In fact, I recommend that you design types as if overloaded operators don't exist. Then, later on, you can add overloaded operators in such a way that they simply call the methods you defined that carry the same semantic meaning.

Types and Formats of Overloaded Operators

You define all overloaded operators as public static methods on the classes they're meant to augment. Depending on the type of operator being overloaded, the method may accept either one or two parameters, and it always returns a value. For all operators except conversion operators, one of the parameter types must be of the same type as the enclosing type for the method. For example, it makes no sense to overload the + operator on class Complex if it adds two double values together, and, as you'll see shortly, it's impossible.

A typical + operator for a class Complex could look like the following:

public static Complex operator+( Complex lhs, Complex rhs )

Even though this method adds two instances of Complex together to produce a third instance of Complex, nothing says that one of the parameters cannot be that of type double, thus adding a double to a Complex instance. Now, how you add a double value to a Complex instance and produce another Complex instance is for you to decipher. In general, operator overloading syntax follows the previous pattern, with the + replaced with the operator du jour, and of course, some operators accept only one parameter.

Note

When comparing C# operators with C++ operators, note that C# operator declarations are more similar to the friend function technique of declaring C++ operators because C# operators are not instance methods.

There are essentially three different groups of overloadable operators.

  • Unary operators: Unary operators accept only one parameter. Familiar unary operators include the ++ and -- operators.

  • Binary operators: As the name implies, binary operators accept two parameters and include familiar mathematical operators such as +, -, /, and *, as well as the familiar comparison operators.

  • Conversion operators: Conversion operators define a user-defined conversion. They must have either the operand or the return value type declared the same as the containing class or struct type.

Even though operators are static and public, and thus are inherited by derived classes, operator methods must have at least one parameter in their declaration that matches the enclosing type, making it impossible for the derived type's operator method to match the signature of the base class operator method exactly. For example, the following is not valid:

public class Apple
{
    public static Apple operator+( Apple lhs, Apple rhs ) {
        // Method does nothing and exists only for example.
        return rhs;
    }
}

public class GreenApple : Apple
{
    // INVALID!! - Won't compile.
    public static Apple operator+( Apple lhs, Apple rhs ) {
        // Method does nothing and exists only for example.
        return rhs;
    }
}

If you attempt to compile the previous code, you'll get the following compiler error:

error CS0563: One of the parameters of a binary operator must be the containing type

Operators Shouldn't Mutate Their Operands

You already know that operator methods are static. Therefore, it is highly recommended (read: required) that you do not mutate the operands passed into the operator methods. Instead, you should create a new instance of the return value type and return the result of the operation. Structs and classes that are immutable, such as System.String, are perfect candidates for implementing custom operators. This behavior is natural for operators such as boolean operators, which usually return a type different from the types passed into the operator.

Note

"Now wait just a minute!" some of you from the C++ community may be saying. "How in the world can you implement the postfix and prefix operators ++ and -- without mutating the operand?" The answer lies in the fact that the postfix and prefix operators as implemented in C# are somewhat different than those of C++. All C# operators are static, and that includes the postfix and prefix operators, whereas in C++ they are instance methods that modify the object instance through the this pointer. The beauty of the C# approach is that you don't have to worry about implementing two different versions of the ++ operator in order to support both postfix and prefix incrementing, as you do in C++. The compiler handles the task of making temporary copies of the object to handle the difference in behavior between postfix and prefix. This is yet another reason why your operators must return new instances while never modifying the state of the operands themselves. If you don't follow this practice, you're setting yourself up for some major debugging heartbreak.

Does Parameter Order Matter?

Suppose you create a struct to represent simple complex numbers—say, struct Complex—and you need to add instances of Complex together. It would also be convenient to be able to add a plain old double to the Complex instance. Adding this functionality is no problem, because you can overload the operator+ method such that one parameter is a Complex and the other is a double. That declaration could look like the following:

static public Complex operator+( Complex lhs, double rhs )

With this operator declared and defined on the Complex struct, you can now write code such as the following:

Complex cpx1 = new Complex( 1.0, 2.0 );
Complex cpx2 = cpx1 + 20.0;

This saves you the time of having to create an extra Complex instance with just the real part set to 20.0 in order to add it to cpx1. However, suppose you want to be able to reverse the operands on the operator and do something like the following instead:

Complex cpx2 = 20.0 + cpx1;

If you want to support different orderings of operands of different types, you must provide separate overloads of the operator. If you overload a binary operator that uses different parameter types, you can create a mirror overload—that is, another operator method that reverses the parameters.

Overloading the Addition Operator

Let's take a look at a cursory example of a Complex struct, which is by no means a complete implementation, but merely a demonstration of how to overload operators. Throughout this chapter, I'll build upon this example and add more operators to it:

using System;

public struct Complex
{
    public Complex( double real, double imaginary ) {
        this.real = real;
        this.imaginary = imaginary;
    }

    static public Complex Add( Complex lhs,
                               Complex rhs ) {
        return new Complex( lhs.real + rhs.real,
                            lhs.imaginary  + rhs.imaginary );
    }

    static public Complex Add( Complex lhs,
                               double rhs ) {

        return new Complex( rhs + lhs.real,
                            lhs.imaginary );
    }

    public override string ToString() {
        return String.Format( "({0}, {1})",
                              real,
                              imaginary );
    }

    static public Complex operator+( Complex lhs,
                                     Complex rhs ) {
        return Add( lhs, rhs );
    }

    static public Complex operator+( double lhs,
                                     Complex rhs ) {
        return Add( rhs, lhs );
    }

    static public Complex operator+( Complex lhs,
                                     double rhs ) {
        return Add( lhs, rhs );
    }
private double real;
    private double imaginary;
}

public class EntryPoint
{
    static void Main() {
        Complex cpx1 = new Complex( 1.0, 3.0 );
        Complex cpx2 = new Complex( 1.0, 2.0 );

        Complex cpx3 = cpx1 + cpx2;
        Complex cpx4 = 20.0 + cpx1;
        Complex cpx5 = cpx1 + 25.0;

        Console.WriteLine( "cpx1 == {0}", cpx1 );
        Console.WriteLine( "cpx2 == {0}", cpx2 );
        Console.WriteLine( "cpx3 == {0}", cpx3 );
        Console.WriteLine( "cpx4 == {0}", cpx4 );
        Console.WriteLine( "cpx5 == {0}", cpx5 );
    }
}

Notice that, as recommended, the overloaded operator methods call methods that perform the same operation. In fact, doing so makes supporting both orderings of operator+ that add a double to a Complex a snap.

Tip

If you're absolutely sure that your type will only be used in a C# environment or in a language that supports overloaded operators, then you can forgo this exercise and simply stick with the overloaded operators.

Operators That Can Be Overloaded

Let's take a quick look at which operators you can overload. Unary operators, binary operators, and conversion operators are the three general types of operators. It's impossible to list all of the conversion operators here, because the set is limitless. Additionally, you can use the one ternary operator—the familiar ?: operator—for conditional statements, but you cannot overload it directly. Later, in the "Boolean Operators" section, I describe what you can do to play nicely with the ternary operator. Table 6-1 lists all of the operators except the conversion operators.

Table 6-1. Unary and Binary Operators

Unary Operators

Binary Operators

+

+

-

-

!

*

~

/

++

%

--

&

true and false

|

 

^

 

«

 

»

 

== and !=

 

> and <

 

>= and <=

Comparison Operators

The binary comparison operators == and !=, < and >, and >= and <= are all required to be implemented as pairs. Of course, this makes perfect sense, because I doubt there would ever be a case where you would like to allow users to use operator== and not operator!=. Moreover, if your type allows ordering via implementation of the IComparable interface or its generic counterpart IComparable<T>, then it makes the most sense to implement all comparison operators. Implementing these operators is trivial if you follow the canonical guidelines given in Chapters 4 and 13 by overriding Equals and GetHashCode and implementing IComparable ( and optionally IComparable<T> and IEquatable<T>) appropriately. Given that, overloading the operators merely requires you to call those implementations. Let's look at a modified form of the Complex number that follows this pattern to implement all of the comparison operators:

using System;

public struct Complex : IComparable,
IEquatable<Complex>,
                        IComparable<Complex>
{
    public Complex( double real, double img ) {
        this.real = real;
        this.img = img;
    }

    // System.Object override
    public override bool Equals( object other ) {
        bool result = false;
        if( other is Complex ) {
            result = Equals( (Complex) other );
        }
        return result;
    }

    // Typesafe version
    public bool Equals( Complex that ) {
        return (this.real == that.real &&
                this.img == that.img);
    }

    // Must override this if overriding Object.Equals()
    public override int GetHashCode() {
        return (int) this.Magnitude;
    }

    // Typesafe version
    public int CompareTo( Complex that ) {
        int result;
        if( Equals( that ) ) {
            result = 0;
        } else if( this.Magnitude > that.Magnitude ) {
            result = 1;
        } else {
            result = −1;
        }

        return result;
    }

    // IComparable implementation
    int IComparable.CompareTo( object other ) {
        if( !(other is Complex) ) {
            throw new ArgumentException( "Bad Comparison" );
        }

        return CompareTo( (Complex) other );
    }

    // System.Object override
    public override string ToString() {
return String.Format( "({0}, {1})",
                              real,
                              img );
    }

    public double Magnitude {
        get {
            return Math.Sqrt( Math.Pow(this.real, 2) +
                              Math.Pow(this.img, 2) );
        }
    }

    // Overloaded operators
    public static bool operator==( Complex lhs, Complex rhs ) {
        return lhs.Equals( rhs );
    }

    public static bool operator!=( Complex lhs, Complex rhs ) {
        return !lhs.Equals( rhs );
    }

    public static bool operator<( Complex lhs, Complex rhs ) {
        return lhs.CompareTo( rhs ) < 0;
    }

    public static bool operator>( Complex lhs, Complex rhs ) {
        return lhs.CompareTo( rhs ) > 0;
    }

    public static bool operator<=( Complex lhs, Complex rhs ) {
        return lhs.CompareTo( rhs ) <= 0;
    }

    public static bool operator>=( Complex lhs, Complex rhs ) {
        return lhs.CompareTo( rhs ) >= 0;
    }

    // Other methods omitted for clarity.

    private double real;
    private double img;
}

public class EntryPoint
{
    static void Main() {
        Complex cpx1 = new Complex( 1.0, 3.0 );
        Complex cpx2 = new Complex( 1.0, 2.0 );

        Console.WriteLine( "cpx1 = {0}, cpx1.Magnitude = {1}",
                           cpx1, cpx1.Magnitude );
        Console.WriteLine( "cpx2 = {0}, cpx2.Magnitude = {1}
",
                           cpx2, cpx2.Magnitude );
Console.WriteLine( "cpx1 == cpx2 ? {0}", cpx1 == cpx2 );
        Console.WriteLine( "cpx1 != cpx2 ? {0}", cpx1 != cpx2 );
        Console.WriteLine( "cpx1 <  cpx2 ? {0}", cpx1 < cpx2 );
        Console.WriteLine( "cpx1 >  cpx2 ? {0}", cpx1 > cpx2 );
        Console.WriteLine( "cpx1 <= cpx2 ? {0}", cpx1 <= cpx2 );
        Console.WriteLine( "cpx1 >= cpx2 ? {0}", cpx1 >= cpx2 );
    }
}

Notice that the operator methods merely call the methods that implement Equals and CompareTo. Also, I've followed the guideline of providing type-safe versions of the two methods introduced by implementing IComparable<Complex> and IEquatable<Complex>, because the Complex type is a value type and I want to avoid boxing if possible.[16] Additionally, I implemented the IComparable.CompareTo method explicitly to give the compiler a bigger type-safety hammer to wield by making it harder for users to call the wrong one (the type-less one) inadvertently. Anytime you can utilize the compiler's type system to sniff out errors at compile time rather than runtime, you should do so. Had I not implemented IComparable.CompareTo explicitly, then the compiler would have happily compiled a statement where I attempt to compare an Apple instance to a Complex instance. Of course, you would expect an InvalidCastException at runtime if you were to attempt something so silly, but again, always prefer compile-time errors over runtime errors.

Conversion Operators

Conversion operators are, as the name implies, operators that convert objects of one type into objects of another type. Conversion operators can allow implicit conversion as well as explicit conversion. Implicit conversion is done with a simple assignment, whereas explicit conversion requires the familiar casting syntax with the target type of the conversion provided in parentheses immediately preceding the instance being assigned from.

There is an important restriction on implicit operators. The C# standard requires that implicit operators do not throw exceptions and that they're always guaranteed to succeed with no loss of information. If you cannot meet that requirement, then your conversion must be an explicit one. For example, when converting from one type to another, there's always the possibility for loss of information if the target type is not as expressive as the original type. Consider the conversion from long to short. Clearly, it's possible that information could be lost if the value in the long is greater than the highest value a short can represent (short.MaxValue). Such a conversion must be an explicit one and require the user to use the casting syntax. Now, suppose you were going the other way and converting a short into a long. Such a conversion will always succeed, so therefore it can be implicit.

Note

Performing explicit conversions from a type with larger storage to a type with smaller storage may result in a truncation error if the original value is too large to be represented by the smaller type. For example, if you explicitly cast a long into a short, you may trigger an overflow situation. By default, your compiled code will silently perform the truncation. If you compile your code with the /checked+ compiler option, it actually would throw a System.OverflowException if your explicit conversion from a long to a short caused an overflow. I recommend that you lean toward building with /checked+ turned on.

Let's see what kind of conversion operators you should provide for Complex. I can think of at least one definite case, and that's the conversion from double to Complex. Definitely, such a conversion should be an implicit one. Another consideration is from Complex to double. Clearly, this conversion requires an explicit conversion. (Casting a Complex to double makes no sense anyway and is only shown here for the sake of example, thus you can choose to return the magnitude rather than just the real portion of the complex number when casting to a double.) Let's look at an example of implementing conversion operators:

using System;

public struct Complex
{
    public Complex( double real, double imaginary ) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // System.Object override
    public override string ToString() {
        return String.Format( "({0}, {1})", real, imaginary );
    }

    public double Magnitude {
        get {
            return Math.Sqrt( Math.Pow(this.real, 2) +
                              Math.Pow(this.imaginary, 2) );
        }
    }

    public static implicit operator Complex( double d ) {
        return new Complex( d, 0 );
    }

    public static explicit operator double( Complex c ) {
        return c.Magnitude;
    }

    // Other methods omitted for clarity.

    private double real;
    private double imaginary;
}

public class EntryPoint
{
    static void Main() {
Complex cpx1 = new Complex( 1.0, 3.0 );
        Complex cpx2 = 2.0;         // Use implicit operator.

        double d = (double) cpx1;   // Use explicit operator.

        Console.WriteLine( "cpx1 = {0}", cpx1 );
        Console.WriteLine( "cpx2 = {0}", cpx2 );
        Console.WriteLine( "d = {0}", d );
    }
}

The syntax in the Main method uses conversion operators. However, be careful when implementing conversion operators to make sure that you don't open up users to any surprises or confusion with your implicit conversions. It's difficult to introduce confusion with explicit operators when the users of your type must use the casting syntax to get it to work. After all, how can users be surprised when they must provide the type to convert to within parentheses? On the other hand, inadvertent use or misguided use of implicit conversion can be the source of much confusion. If you write a bunch of implicit conversion operators that make no semantic sense, I guarantee your users will find themselves in a confusing spot one day when the compiler decides to do a conversion for them when they least expect it. For example, the compiler could do an implicit conversion when trying to coerce an argument on a method call during overload resolution. Even if the conversion operators do make semantic sense, they can still provide plenty of surprises, because the compiler will have the liberty of silently converting instances of one type to another when it feels it's necessary.

Unlike C++ where single parameter constructors behave like implicit conversion operators by default, C# requires that you explicitly write an implicit operator on the types that you define.[17] However, in order to provide these conversions, you must bend the rules of method overloading ever so slightly for this one case. Consider the case where Complex provides another explicit conversion operator to convert to an instance of Fraction as well as to an instance of double. This would give Complex two methods with the following signatures:

public static explicit operator double( Complex d )
public static explicit operator Fraction( Complex f )

These two methods take the same type, Complex, and return another type. However, the overload rules clearly state that the return type doesn't participate in the method signature. Going by those rules, these two methods should be ambiguous and result in a compiler error. In fact, they are not ambiguous, because a special rule exists to allow the return type of conversion operators to be considered in these signatures. Incidentally, the implicit and explicit keywords don't participate in the signature of conversion operator methods. Therefore, it's impossible to have both implicit and explicit conversion operators with the same signature. Naturally, at least one of the types in the signature of a conversion operator must be the enclosing type. It is invalid for a type Complex to implement a conversion operator from type Apples to type Oranges.

Boolean Operators

It makes sense for some types to participate in Boolean tests, such as within the parentheses of an if block or with the ternary operator ?:. In order for this to work, you have two alternatives. The first is that you can implement two conversion operators, known as operator true and operator false. You must implement these two operators in pairs to allow the Complex number to participate in Boolean test expressions. Consider the following modification to the Complex type, where you now want to use it in expressions where a value of (0, 0) means false and anything else means true:

using System;

public struct Complex
{
    public Complex( double real, double imaginary ) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // System.Object override
    public override string ToString() {
        return String.Format( "({0}, {1})",
                              real,
                              imaginary );
    }

    public double Magnitude {
        get {
            return Math.Sqrt( Math.Pow(this.real, 2) +
                              Math.Pow(this.imaginary, 2) );
        }
    }

    public static bool operator true( Complex c ) {
        return (c.real != 0) || (c.imaginary != 0);
    }

    public static bool operator false( Complex c ) {
        return (c.real == 0) && (c.imaginary == 0);
    }

    // Other methods omitted for clarity.

    private double real;
    private double imaginary;
}

public class EntryPoint
{
    static void Main() {
        Complex cpx1 = new Complex( 1.0, 3.0 );
        if( cpx1 ) {
            Console.WriteLine( "cpx1 is true" );
        } else {
            Console.WriteLine( "cpx1 is false" );
        }

        Complex cpx2 = new Complex( 0, 0 );
        Console.WriteLine( "cpx2 is {0}", cpx2 ? "true" : "false" );
    }
}

You can see the two operators for applying the true and false tests to the Complex type. Notice the syntax looks almost the same as regular operators, except that it includes the return type of bool. I'm not quite sure why this is necessary, because you can't provide a type other than bool as the return type. If you do, the compiler will quickly tell you that the only valid return type from operator true or operator false is a bool. Nevertheless, you must supply the return type for these two operators. Also, notice that you cannot mark these operators explicit or implicit, because they're not conversion operators. Once you define these two operators on the type, you can use instances of Complex in Boolean test expressions, as shown in the Main method.

Alternatively, you can choose to implement a conversion to type bool to achieve the same result. Typically, you'll want to implement this operator implicitly for ease of use. Consider the modified form of the previous example using the implicit bool conversion operator rather than operator true and operator false:

using System;

public struct Complex
{
    public Complex( double real, double imaginary ) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // System.Object override
    public override string ToString() {
        return String.Format( "({0}, {1})",
                              real,
                              imaginary );
    }

    public double Magnitude {
        get {
            return Math.Sqrt( Math.Pow(this.real, 2) +
                              Math.Pow(this.imaginary, 2) );
        }
    }

    public static implicit operator bool( Complex c ) {
        return (c.real != 0) || (c.imaginary != 0);
    }

    // Other methods omitted for clarity.

    private double real;
    private double imaginary;
}

public class EntryPoint
{
    static void Main() {
        Complex cpx1 = new Complex( 1.0, 3.0 );
        if( cpx1 ) {
            Console.WriteLine( "cpx1 is true" );
        } else {
Console.WriteLine( "cpx1 is false" );
        }

        Complex cpx2 = new Complex( 0, 0 );
        Console.WriteLine( "cpx2 is {0}", cpx2 ? "true" : "false" );
    }
}

The end result is the same with this example. Now, you may be wondering why you would ever want to implement operator true and operator false rather than just an implicit bool conversion operator. The answer lies in whether it is valid for your type to be converted to a bool type or not. With the latter form, where you implement the implicit conversion operator, the following statement would be valid:

bool f = cpx1;

This assignment would work because the compiler would find the implicit conversion operator at compile time and apply it. However, if you were extremely tired the night you coded this line and really meant to assign f from a completely different variable, it might be a long time before you find the bug. This is one example of how gratuitous use of implicit conversion operators can get you in trouble.

The rule of thumb is this: Provide only enough of what is necessary to get the job done and no more. If all you want is for your type—in this case, Complex—to participate in Boolean test expressions, only implement operator true and operator false. Do not implement the implicit bool conversion operator unless you have a real need for it. If you do happen to have a need for it, and thus implement the implicit bool conversion operator, you don't have to implement operator true and operator false, because they would be redundant. If you do provide all three, the compiler will go with the implicit conversion operator rather than operator true and operator false, because invoking one is not more efficient than the other, assuming you code them the same.

Summary

In this chapter, I covered some useful guidelines for overloading operators, including unary, binary, and conversion operators. Operator overloading is one of the features that makes C# such a powerful and expressive .NET language. However, just because you can do something doesn't mean that you should. Misuse of implicit conversion operators and improperly defined semantics in other operator overloads has proven time and time again to be the source of great user confusion (and that user could be the author of the type) as well as unintended behavior. When it comes to overloading operators, provide only enough of what is necessary, and don't go counter to the general semantics of the various operators. The CLS doesn't require overloaded operator support, thus not all .NET languages support overloaded operators. Therefore, it's important to always provide explicitly named methods that provide the same functionality. Sometimes, those methods are already defined in system interfaces, such as IComparable or IComparable<T>. Never isolate functionality strictly within overloaded operators unless you're 100% sure that your code will be consumed by .NET languages that do support operator overloading.

In the next chapter, I'll cover the intricacies and tricks involved in creating exception-safe and exception-neutral code in the .NET Framework.



[16] I describe this guideline in more detail in Chapter 5 in the section, "Explicit Interface Implementation with Value Types."

[17] Yes, I realize the implications of my explicit, and possible confusing, use of the words implicit and explicit. I explicitly hope that I have not implicitly confused anyone.

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

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