Working with Operators

Working with Operators

C# provides you with a large set of operators and gives you a great deal of control over the behavior of those operators in an expression. This section focuses on how to control the behavior of operators. First we’ll look at operator precedence and executing code in a checked context, and then we’ll look at an example that shows you how to overload operators to work with a new value type.

Understanding Operator Precedence

Each operator has a defined precedence. When you combine multiple operators to create a single expression, it’s important to understand how operator precedence will affect the evaluation of the expression. Table 4-8 lists the C# operators, categorized according to precedence, from highest to lowest.

The Primary category refers to basic expressions that form a tight binding between operator and operand. The notation f(x) refers to a function call invocation, where x is passed as an argument. As you saw in Chapter 2 and Chapter 3, the new operator is used to allocate an object. The checked and unchecked operators, discussed in the next section, are used to mark whether an expression tests for math overflow conditions.

In the Unary category, the (T)x notation signifies a cast expression, where a value x is cast to type T.

tip

You can use parentheses to alter the order in which operators are evaluated. Expressions within parentheses are always evaluated first. If you need an operator with lower precedence to be evaluated ahead of an operator with higher precedence, just group the necessary expression using parentheses, like this:

decimal first = 3.2M;
decimal second = 4.3M;
decimal pi = 3.1415926535M;
decimal answer = pi * (first + second);

Another property of operators is the way they’re associated with their operands. Most unary operators are associated with the operands to their right; the only exception is the unary postfix operators.

The binary operators are usually left-associative, meaning that operations are carried out from left to right. The exception is the assignment operator, which is associated with the operand to its right. This exception allows assignment operators to be chained together, as shown earlier in this chapter in the section “Setting Variables with the Assignment Operators.” The conditional ternary operator is also right-associative.

Using the checked and unchecked Keywords

Math operations can sometimes result in overflow or underflow errors. Some languages check every math operation to guard against these errors, but this type of runtime checking increases the cost of every math operation. Some languages perform no runtime checking, expecting that error checking will be inserted by the programmer wherever such checks are warranted. This approach often leads to errors at runtime. C# offers keywords that are used to explicitly enable or disable automatic checking of math operations.

The checked keyword is used to mark a block or an expression as checked for arithmetic overflow errors, as shown in the following code. If an operation overflows its destination, an OverflowException exception will be thrown.

try
{
    checked
    {
        short max = 32767; // Maximum value for a short
        ++max;
    }
}
catch(OverflowException exc)
{
    
    

}

The unchecked keyword marks a block or an expression as not checked for overflow errors. If no keyword is present, the behavior depends on the current compiler settings—normally set to unchecked.

Defining Operators for Your Types

When defining your own class or structure types, you also can define how operators work with those types. Providing operators for your types allows them to be used more like the built-in types. It also helps make your types more intuitive. For example, the default behavior of the = operator is to test for object equality rather than value equality. As discussed in Chapter 3, the string type overloads the relational operators and tests the string value instead of the object location. This is the behavior that most users would expect of a string class, and it’s a good example of providing a useful set of operators for a class.

Many of the operators, such as the dot operator, can’t be overloaded. Table 4-9 lists the operators that are available for overloading in C#.

Table 4-9.  Operators That Can Be Overloaded in C#

Operator Type

Operators

Unary

+ - ! ~ ++ -- true false

Binary

+ - * / % & ¦ ^ << >> == != > < >= <=

The ternary operator can’t be overloaded, nor can the index or cast operator. (In C#, you define how the index operator works by providing an indexer for your class.) Instead of overloading the cast operator, in C# you provide explicit type conversion methods, as discussed later in this chapter, in the section “Performing Explicit Conversions.”

You’re free to implement specialized versions of the comparison operators; however, operators must be implemented in pairs. The > operator must be implemented together with the < operator. The >= operator must be ­implemented in tandem with the <= operator. The == and != operators must always be implemented together. In addition, if you implement == and !=, the Visual C# .NET compiler will issue a warning if you don’t override the Object.Equals and Object.GetHashCode methods.

By default, the Object.Equals method tests reference equality. If you override this method to test for logical equivalence, you’re expected to implement the following basic behavior:

  • If Equals is passed a reference to the current object, it should return true, except for some specific floating-point cases.

  • If Equals is passed a null reference, it must return false.

  • If the objects Equals works with are unmodified, Equals must return the same result.

  • For floating-point types, when comparing two objects that evaluate as NaN, Equals will return true.

  • The Equals method must be reflexive—that is, if x.Equals(y) is true, y.Equals(x) must be true.

  • The Equals method is transitive—that is, if x.Equals(y) is true, and y.Equals(z) is true, x.Equals(z) must be true.

The GetHashCode method is a hash function used to enable proper distribution in a hash table or similar collection type. When implementing this method, keep in mind that an ideal hash function is one that returns the same hash value for logically equivalent objects. In the example in the next section, the hash function calls ToString to generate a string representation of the object and then uses the string type’s implementation of GetHashCode to generate a hash value.

Creating a New Value Type

To demonstrate how operators can be redefined for a type, let’s consider a new value type that models (in a simplified way) a baseball batting average. The basic idea behind a batting average is that it represents the number of hits divided by the number of at bats for a baseball player. (Of course, like so many things in baseball, an explanation of exactly what constitutes a hit or an at bat could take hours, and sometimes seems to depend on the mood of the official scorer, but that’s another story.)

The following code shows an initial version of the BattingAverage value type. Like all value types, it’s declared as a structure.

using System;
namespace MSPress.CSharpCoreRef.BattingAverageExample
{
    public struct BattingAverage
    {
        // Constructs a new BattingAverage value type instance
        public BattingAverage(int atBats, int hits)
        {
            TestValid(atBats, hits);
            _atBats = atBats;
            _hits = hits;
        }
        /// Returns the batting average, subject to floating-
        /// point rounding
        public float Average()
        {
            if(_atBats == 0)
                return 0.0f;
            else
                return (float)_hits / (float)_atBats;
        }
        /// Tests for valid hits and at bats, throwing an exception
        /// if the parameters are invalid
        private static void TestValid(int testAtBats, int testHits)
        {
            if(testAtBats < 0)
            {
                string msg = "At bats must not be negative";
                throw new ArgumentOutOfRangeException(msg);
            }
            if(testAtBats < testHits)
            {
                string msg = "Hits must not exceed at bats";
                throw new ArgumentOutOfRangeException(msg);
            }
        }
        public int _atBats;
        public int _hits;
    }
}

The BattingAverage value type uses two private fields to represent the number of at bats and hits used to calculate the batting average. Methods are used to validate and calculate the batting average.

As defined in the preceding code, the BattingAverage type can be used to calculate and store batting average information. However, comparing Batting­Average objects is awkward in this first version of the class, as shown here:

if(firstBatter.Average() < secondBatter.Average())
{
    // secondBatter's average is higher...
}

Instead of requiring clients of the BattingAverage class to explicitly invoke the Average method and compare the results, it’s more intuitive to overload the comparison operators and allow for a simplified syntax that’s similar to the built-in types, as follows:

if(firstBatter < secondBatter)
{
    // secondBatter's average is higher...
}

The following methods overload all of the relational operators and the object class’s Equals and GetHashCode methods: The complete code listing is available on the companion CD.

// Equality operator for batting averages
public static bool operator ==(BattingAverage left,
                               BattingAverage right)
{
    if((object)left == null)
        return false;
    else
        return left.Equals(right);
}

// Inequality operator for batting averages
public static bool operator !=(BattingAverage left,
                               BattingAverage right)
{
    return !(left == right);
}

// Override of the Object.Equals method; returns
// true if the current object is equal to another
// object passed as a parameter
public override bool Equals(object other)
{
    bool result = false;
    if(other != null)
    {
        if((object)this == other)
        {
            result = true;
        }
        else if(other is BattingAverage)
        {
            BattingAverage otherAvg = (BattingAverage)other;
            result = Average() == otherAvg.Average();
        }
    }
    return result;
}

// Converts the batting average to a string, and then
// uses the string to generate the hash code
public override int GetHashCode()
{
    return ToString().GetHashCode();
}

// Compares two operands using the greater than operator
public static bool operator >(BattingAverage left,
                              BattingAverage right)
{
    return left.Average() > right.Average();
}

// Compares two operands using the less than or equal to
// operator
public static bool operator <=(BattingAverage left,
                               BattingAverage right)
{
    return left.Average() <= right.Average();
}

// Compares two operands using the less than operator
public static bool operator <(BattingAverage left,
                              BattingAverage right)
{
    return left.Average() < right.Average();
}

// Compares two operands using the greater than or
// equal to operator
public static bool operator >=(BattingAverage left,
                               BattingAverage right)
{
    return left.Average() >= right.Average();
}

// Returns true if the object is logically false
public static bool operator false(BattingAverage avg)
{
    return avg.Average() == 0;
}

// Returns true if the object is logically true
public static bool operator true(BattingAverage avg)
{
    return avg.Average() != 0;
}

// Performs an AND operation on two batting averages
public static BattingAverage operator &(BattingAverage left,
                                        BattingAverage right)
{
    if(left.Average() == 0 ¦¦ right.Average() == 0)
        return new BattingAverage();
    else
        return new BattingAverage(left._atBats + right._atBats,
                                  left._hits + left._hits);
}

// Performs an OR operation on two batting averages
public static BattingAverage operator ¦(BattingAverage left,
                                        BattingAverage right)
{
    if(left.Average() != 0)
        return new BattingAverage(left._atBats, left._hits);
    else if(right.Average() != 0)
        return new BattingAverage(right._atBats, right._hits);
    else
        return new BattingAverage();
}

The relational operators for BattingAverage encapsulate the Average method and allow clients to compare BattingAverage objects with each other naturally, just like built-in types such as int or string.

Controlling the Behavior of && and ¦¦

Now that we’ve covered the basics of operator overloading, let’s take on a more advanced topic: overloading the && and ¦¦ operators. Although the && and ¦¦ operators can’t be overloaded directly, you can affect how they’re evaluated. When the && operator is encountered in an expression, the C# compiler generates code that calls the true, false, and & operators. We’ll examine the way these operators are invoked in a minute. For now, let’s look at how the true, false, and & operators work.

The true and false operators return Boolean values that indicate whether an object has the value specified by the operator. The true operator returns true when the object has a logically true value; otherwise, it returns false. The false operator returns true if the object has a false value; otherwise, it returns false. The & operator returns a new object of the same type as the operands; this object is the result of performing a logical AND with the operands. For the Batting­Average value type, the true and false operators are defined as follows:

public static bool operator false(BattingAverage avg)
{
    return avg.Average() == 0;
}

public static bool operator true(BattingAverage avg)
{
    return avg.Average() != 0;
}

The operators consider any batting average greater than 0 to be logically true; a batting average of 0 is considered logically false.

The & operator for the BattingAverage type looks like this:

public static BattingAverage operator &(BattingAverage left,
                                        BattingAverage right)
{
    if(left.Average() == 0 ¦¦ right.Average() == 0)
        return new BattingAverage();
    else
        return new BattingAverage(left._atBats + right._atBats,
                                  left._hits + right._hits);
}

If either of the BattingAverage objects has a 0 average, a new, empty Batting­Average object is created and returned. Otherwise, a new BattingAverage object that combines the two operands is created and returned.

For the purpose of tracing how the Visual C# .NET compiler creates the && expression, consider the following code:

BattingAverage a;
BattingAverage b;

    

bool isTrue = (a && b);

Behind the scenes, the compiler combines the true, false, and & operators in the following way to evaluate the && operator:

if(BattingAverage.false(a) != true)
{
    return BattingAverage.true(BattingAverage.operator&(a, b));
}
else
{
    return BattingAverage.true(a);
}   

The first operand, a, is tested by invoking the false operator. If the operand isn’t false, the & operator is invoked to combine both a and b operands in an AND operation. The result of that operation is passed to the type’s true operator, and the result of that operation is kept as the result of the && expression. If the first operand is false, the operand is passed to the type’s true operator, and the result of that operation is used as the result of the && expression.

In a similar way, when the ¦¦ operator is used like this:

bool isTrue = (a ¦¦ b);

the Visual C# .NET compiler will generate code equivalent to the following:

if(BattingAverage.true(a) == true)
{
    return BattingAverage.true(a);
}
else
{
    return BattingAverage.true(BattingAverage.operator¦(a, b));
}


Part III: Programming Windows Forms