Understanding Operators


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:

  • You cannot change the precedence and associativity of an operator. The precedence and associativity are based on the operator symbol (for example, +) and not on the type (for example, int) on which the operator symbol is being used. Hence, the expression a + b * c is always the same as a + (b * c), regardless of the type of a, b, and c.

  • You cannot change the multiplicity of an operator (the number of operands). For example, * (the symbol for multiplication), is a binary operator. If you declare a * operator for your own type, it must be a binary operator.

  • You cannot invent new operator symbols. For example, you can't create a new operator symbol, such as ** for raising one number to the power of another number. You'd have to create a method for that.

  • You can't change the meaning of operators when applied to built-in types. For example, the expression 1 + 2 has a predefined meaning and you're not allowed to override this meaning. If you could do this, things would be too complicated!

  • There are some operator symbols that you can't overload. For example, you can't overload the dot operator (member access). Again, if you could do this, it would lead to unnecessary complexity.

    TIP
    You can use indexers to simulate [ ] as an operator. Similarly, you can use properties to simulate = (assignment) as an operator, and you can use delegates to simulate a function call as an operator.

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:

  • The operator is public. All operators must be public.

  • The operator is static. All operators must be static. Operators are never polymorphic, and cannot use the virtual, abstract, override, or sealed modifiers.

  • A binary operator (such as + shown above) has two explicit arguments and a unary operator has one explicit argument (C++ programmers should note that operators never have a hidden this parameter).

    TIP
    When declaring highly stylized functionality (such as operators), it is useful to adopt a naming convention for the parameters. For example, developers often use lhs and rhs (acronyms for left-hand side and right-hand side, respectively) for binary operators.

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.

Operators and the Common Language Specification

Not all languages that execute use the common language runtime (CLR) support or understand operator overloading. For this reason, the Common Language Specification (CLS) requires that if you overload an operator, you should provide an alternative mechanism that supports the same functionality as the CLR. For example, suppose you implement operator+ for the Hour struct:

public static Hour operator+ (Hour lhs, int rhs) {     ... }

You should also provide an Add method that achieves the same thing:

public static Hour Add(Hour lhs, int rhs) {     ... }




Microsoft Visual C# 2005 Step by Step
Microsoft® Visual C#® 2005 Step by Step (Step By Step (Microsoft))
ISBN: B002CKYPPM
EAN: N/A
Year: 2005
Pages: 183
Authors: John Sharp

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net