Previous Page
Next Page

Understanding Operators

You use operators to combine operands together into expressions. Each operator has its own semantics dependent on the type it works with. For example, the + operator means “add” when used with numeric types, or “concatenate” when used with strings.

Each operator symbol has a precedence. For example, the * operator has a higher precedence than the + operator. This means that the expression a + b * c is the same as a + (b * c).

Each operator symbol also has an associativity to define whether the operator evaluates from left to right or from right to left. For example, the = operator is right-associative (it evaluates from right to left), so a = b = c is the same as a = (b = c).

NOTE
The right-associativity of the = operator allows you to perform multiple assignments in the same statement. For example, you can initialize several variables to the same value like this:
int a, b, c, d, e;
 a = b = c = d = e = 99;
The expression e = 99 is evaluated first. The result of the expression is the value that was assigned (99), which is then assigned to d, c, b, and finally a in that order.

A unary operator is an operator that has just one operand. For example, the increment operator (++) is a unary operator.

A binary operator is an operator that has two operands. For example, the multiplication operator (*) is a binary operator.

Operator Constraints

C# allows you to overload most of the existing operator symbols for your own types. When you do this, the operators you implement automatically fall into a well-defined framework with the following rules:

Overloaded Operators

To define your own operator behavior, you must overload a selected operator. You use method-like syntax with a return type and parameters, but the name of the method is the keyword operator together with the operator symbol you are declaring. For example, here's a user-defined struct called Hour that defines a binary + operator to add together two instances of Hour:

struct Hour
{
    public Hour(int initialValue)
    {
        this.value = initialValue;
    }

    public static Hour operator+ (Hour lhs, Hour rhs)
    {
        return new Hour(lhs.value + rhs.value);
    }
    ...
    private int value;
}

Notice the following:

When you use the + operator on two expressions of type Hour, the C# compiler automatically converts your code into a call to the user-defined operator. The C# compiler converts this:

Hour Example(Hour a, Hour b)
{
    return a + b;
}

Into this:

Hour Example(Hour a, Hour b)
{
    return Hour.operator+(a,b); // pseudocode
}

Note, however, that this syntax is pseudocode and not valid C#. You can use a binary operator only in its standard infix notation (with the symbol between the operands).

There is one final rule you must follow when declaring an operator otherwise your code will not compile: At least one of the parameters should always be of the containing type. In the previous operator+ example for the Hour class, one of the parameters, a or b, must be an Hour object. In this example, both parameters are Hour objects. However, there could be times when you want to define additional implementations of operator+ that add an integer (a number of hours) to an Hour object—the first parameter could be Hour, and the second parameter could be the integer. This rule makes it easier for the compiler to know where to look when trying to resolve an operator invocation, and it also ensures that you can't change the meaning of the built-in operators.

Creating Symmetric Operators

In the previous section, you saw how to declare a binary + operator to add together two instances of type Hour. The Hour struct also has a constructor that creates an Hour from an int. This means that you can add together an Hour and an int—you just have to first use the Hour constructor to convert the int into an Hour. For example:

Hour a = ...;
int b = ...;
Hour sum = a + new Hour(b);

This is certainly valid code, but it is not as clear or as concise as adding together an Hour and an int directly, like this:

Hour a = ...;
int b = ...;
Hour sum = a + b;

To make the expression (a + b) valid, you must specify what it means to add together an Hour (a, on the left) and an int (b, on the right). In other words, you must declare a binary + operator whose first parameter is an Hour and whose second parameter is an int. The following code shows the recommended approach:

struct Hour
{
    public Hour(int initialValue)
    {
        this.value = initialValue;
    }
    ...
    public static Hour operator+ (Hour lhs, Hour rhs)
    {
        return new Hour(lhs.value + rhs.value);
    }

    public static Hour operator+ (Hour lhs, int rhs)
    {
        return lhs + new Hour(rhs);
    }
    ...
    private int value;
}

Notice that all the second version of the operator does is construct an Hour from its int argument, and then call the first version. In this way, the real logic behind the operator is held in a single place. The point is that the extra operator+ simply makes existing functionality easier to use. Also, notice that you should not provide many different versions of this operator, each with a different second parameter type—only cater for the common and meaningful cases, and let the user of the class take any additional steps if an unusual case is required.

This operator+ declares how to add together an Hour as the left-hand operand and an int as the right-hand operator. It does not declare how to add together an int as the left-hand operand and an Hour as the right-hand operand:

int a = ...;
Hour b = ...;
Hour sum = a + b; // compile-time error

This is counter-intuitive. If you can write the expression a + b, you expect to also be able to write b + a. Therefore, you should provide another overload of operator+:

struct Hour
{
    public Hour(int initialValue)
    {
        this.value = initialValue;
    }
    ...
    public static Hour operator+ (Hour lhs, int rhs)
    {
        return lhs + new Hour(rhs);
    }

    public static Hour operator+ (int lhs, Hour rhs)
    {
         return new Hour(lhs) + rhs;
    }
    ...
    private int value;
}
NOTE
C++ programmers should notice that you must provide the overload yourself. The compiler won't write it for you or silently swap the sequence of the two operands to find a matching operator.

Previous Page
Next Page