Examining Operators

Examining Operators

Operators are used in different ways in C# and can be divided into three groups, depending on the number of operands (the variables or constants that operators act on) used with the operator, as follows:

  • Unary operators  Used with a single operand. Examples of unary operators include the increment operator (++), which is used to increment scalar values, and the negation operator (!), which is used to invert a Boolean value.

  • Binary operators  Used with two operands. Examples of binary operators include the multiplication operator (*) and the division operator (/).

  • Ternary operator  The lone ternary operator (?:) works with three operands to form a conditional expression.

In addition to categorizing operators by the number of operands they work with, you can categorize them by the type of work they perform, as follows:

  • Relational operators

  • Logical operators

  • Arithmetic operators

  • Bitwise operators

  • Assignment operators

  • Conditional operator

  • Type-information operators

  • Direct memory access operators

  • Other operators

The following sections describe these operator categories in more detail.

Comparing Objects with the Relational Operators

The relational operators are used to create expressions that compare two objects and return a Boolean value, as in the following code snippet:

int peopleYears = 1;
int dogYears = 7;
// Compare and store the result in a Boolean variable.
bool result = dogYears > peopleYears;

This expression tests dogYears and peopleYears to determine whether they’re equal, creating an expression that has a Boolean value. The result of the expression can be stored, as in this example, or combined with other expressions.

The six relational operators defined in C# are listed in Table 4-1.

The comparison operator (==) tests for equality between two operands, as shown here:

bool result = (myAge == yourAge);

A common mistake is to use the assignment operator (=) instead of the == operator. The Microsoft Visual C# .NET compiler will usually detect inappropriate use of the = operator and flag it as an error. For example, the following code will compile in C++, but not in Visual C# .NET:

// Valid C++, not C#
bool result = myAge = yourAge;

The error message returned by the compiler complains that the result of the assignment (an int value) can’t be implicitly converted to a bool. This is an example of how the more restrictive rules governing type conversion found in C# protect you from common programming mistakes. Type conversion is discussed in more detail later in this chapter, in the section “Converting Types.”

The inequality operator (!=) tests two operands for inequality, returning true if the operands aren’t equal and false if they’re equal, as shown here:

bool notMaximumHeight = (height != maxHeight);

The less than (<) and greater than (>) relational operators compare the relative order of two operands, as follows:

bool isLess = (height < maxHeight);
bool isGreater = (height > minHeight);

The less than or equal to (<=) and greater than or equal to (>=) operators are similar to the < and > operators, except that they’ll evaluate as true when the operands are equal.

Forming Logical Expressions with the Logical Operators

The logical operators work with Boolean operands to form logical expressions. C# defines eight logical operators, listed in Table 4-2.

Table 4-2.  Logical Operators in C#

Operator

Description

!

NOT (negation)

&&

AND (short-circuit)

&

AND (full evaluation)

¦¦

OR (short-circuit)

¦

OR (full evaluation)

^

XOR (exclusive OR)

true

Tests for a true value (used when overloading)

false

Tests for a false value (used when overloading)

The NOT (negation) operator (!) works with a single operand and inverts a Boolean value, as shown here:

bool notTrue = !true;

C# shares the AND (short-circuit) operator (&&) with C and C++. This operator works with two operands, evaluating as true if both operands are true:

bool hasTwoWheels = true;
bool hasPedals = true;
bool isBicycle = (hasTwoWheels && hasPedals);

The OR (short-circuit) operator (¦¦) works with two operands, like the && operator, but evaluates to true if any of the operands are true.

An interesting property of the && operator and the ¦¦ operator is that they short-circuit, meaning that if the result can be determined from the first operand, the second operand isn’t evaluated. Short-circuiting isn’t normally a problem, as the following code illustrates:

bool hasAge = (testAge && GetAge());

In this code, the && operator will short-circuit if testAge is false. Because the && operator requires both operands to be true, if the first operand is false, there’s no need to continue to evaluate the expression and invoke the GetAge method.

But what if you require all of your operands to be evaluated? C# includes operators that work exactly like && and ¦¦, except that they guarantee full evaluation of both operands. The AND (full evaluation) operator (&) is a logical AND with full evaluation, and the OR (full evaluation) operator (¦) is a logical OR with full evaluation. In the following code, the methods IsWindow and IsVisible are always called:

bool windowReady = (IsWindow() & IsVisible()); 

The XOR (exclusive OR) operator (^) is used to determine whether exactly one of two operands is true, as shown in the following code. If neither of the operands is true, or if both are true, the expression evaluates as false.

bool decisionReady = (IsGoingLeft() ^ IsGoingRight());

This brings us to the true and false operators. These operators are rarely invoked directly. If you’re implementing your own types, however, you can overload these operators to control how logical operations are evaluated against your type. The section “Creating a New Value Type,” later in this chapter, discusses creating your own types and overloading these operators in more detail.

As mentioned, implicit conversion from scalar to Boolean types isn’t allowed in C#. This means that common C and C++ constructions such as the following aren’t allowed:

int answer = 42;
if(answer){ ... }

To prevent a host of common programming errors, C# requires you to be slightly more explicit about your intentions, as shown here:

int answer = 42;
if(answer != 0){ ... }
Doing Math with the Arithmetic Operators

The arithmetic operators are used to create expressions that perform math operations. The operators for creating basic math expressions such as addition, multiplication, division, and other operations are listed in Table 4-3.

Table 4-3.  Arithmetic Operators in C#

Operator

Description

+

Addition

-

Subtraction

*

Multiplication

%

Modulo (Remainder)

/

Division

++

Increment

--

Decrement

The addition operator (+) is used to add two operands. The resulting expression contains the value of the operation, as follows:

int answer = 5 + 37;

When the operands have different types, the C# compiler must apply type conversion rules to ensure that the operation occurs in a predictable manner. In the following example, the first operand is converted to the float type, and then the + operator is called. The resulting expression has the float type.

float answer = 5 + 37.5;

The subtraction operator (-) is used to subtract the second operand from the first, as shown here:

int answer = 52 - 5;

The multiplication operator (*) is used to multiply two operands:

int answer = 21 * 2;

The division operator (/) is used to divide the first operand by the second, as shown here, with the resulting expression containing the operation result:

int answer = 84 / 2;

When two scalar operands are encountered, the / operator returns a scalar result, rounded down to the lower whole number. To determine the remainder, if any, use the modulo operator (%).

The % operator is used to divide the first operand by the second, with the resulting expression containing the remainder of the operation, as shown here:

int answer = 142 % 50;

The ++ and -- operators work with a single operand, incrementing or decrementing the value of the operand. You can use either the prefix or postfix versions of these operators. The difference is subtle but important if you’re testing for the resulting value of the expression. A prefix increment or decrement operation increments the value of the operand, and the resulting expression is the changed value of the operand, as shown here:

int n = 41;
int answer = ++n; // answer = 42, n = 42;

Although a postfix increment or decrement operation increments the value of the operand, the resulting expression has the value of the operand before the operator was applied, as shown here:

int n = 41;
int answer = n++; // answer = 41, n = 42;

In some cases, the prefix increment and decrement operations will be more efficient than the postfix versions, because the implementation of those methods doesn’t require that a temporary value be created.

The arithmetic operators can cause your program to throw an exception. An attempt to divide by 0 will result in a DivideByZeroException exception being thrown. Other operations that result in an overflow condition, in which a variable is assigned a value outside the range of values that the variable can hold, might result in an OverflowException exception being thrown if the code is running in a checked context.

Managing Bit Patterns with the Bitwise Operators

The bitwise operators are used to manipulate bit patterns for scalar types. These operations are useful when you’re interacting directly with the operating system or with legacy code that depends on masking values stored in a scalar type.

Table 4-4 lists the bitwise operators offered in C#.

Table 4-4.  Bitwise Operators in C#

Operator

Description

>>

Right shift

<<

Left shift

&

AND

¦

OR

^

XOR (exclusive OR)

~

Complement

note

Several of the operators in Table 4-4 are shared with the logical operators discussed earlier and shown in Table 4-2. When the Visual C# .NET compiler detects that one of these operators is being used, it invokes the correct operator based on the types of the surrounding operands.

The left-shift operator (<<) shifts the bits of the scalar value a specified number of bits to the left, as shown in Figure 4-1.

Figure 4-1.
The << operator, which moves the bit pattern to the left.

Any bits that are shifted out of the variable are discarded. New bits with 0 values are inserted to fill in the space left by the shift operation, as follows:

int answer = 21 << 1; // answer = 42

The right-shift operator (>>) shifts the bits of a scalar value to the right, in a manner similar to the << operator, as shown here:

int answer = 84 >> 1; // answer = 42

The bitwise AND operator (&) combines a scalar value and a bit mask by performing a logical AND operation on a bit-by-bit basis. This operation results in a new bit pattern, as shown in Figure 4-2.

Figure 4-2.
The & operator, which is used to mask bits in a scalar value by performing bitwise AND operations.

Where the original value and the bit mask both have a corresponding bit enabled, the resulting bit pattern will have that bit enabled. All other bits will be set to 0, as shown here:

int n = 0x007F;
int x = 0x00AA;
int answer = n & x;
Console.WriteLine(answer); // Displays 42

The bitwise OR operator (¦) combines a scalar value and a bit mask much like the & operator does, except that the ¦ operator performs a logical OR operation, as shown in Figure 4-3.

Figure 4-3.
The ¦ operator, which is used to combine bits in a scalar value by performing bitwise OR operations.

Where the original scalar value or the bit mask have a bit enabled, the resulting bit pattern will have the corresponding bit enabled, as shown here:

int n = 0x0020;
int x = 0x002A;
int answer = n ¦ x;
Console.WriteLine(answer); // Displays 42

The bitwise XOR (exclusive OR) operator (^) is similar to the & and ¦ operators, except that the ^ operator performs an XOR operation on each bit, and the resulting bit pattern has a bit set only if the original scalar value or the bit mask has its corresponding bit set, as shown in Figure 4-4.

Figure 4-4.
The ^ operator, which is used to perform bitwise XOR ­operations.

If both (or neither) have the bit set, the resulting bit pattern has its bit cleared.

The bitwise complement operator (~) generates a bitwise complement for a scalar value. Any bits that are enabled in the original scalar value are disabled in the resulting bit pattern, and any bits that are disabled are enabled.

Setting Variables with the Assignment Operators

Several types of assignment operators are available in C#, the most common of which is the simple assignment operator (=). Table 4-5 lists these assignment operators, which provide a concise method for combining common operations with the = operator.

Table 4-5.  Assignment Operators in C# 

Operator

Description

=

Assignment

+=

Addition assignment

-=

Subtraction assignment

*=

Multiplication assignment

/=

Division assignment

%=

Remainder assignment

<<=

Left-shift assignment

>>=

Right-shift assignment

&=

Bitwise-AND assignment

¦=

Bitwise-OR assignment

^=

Bitwise-XOR assignment

The = operator is used to assign the value of one operand (the right operand) to a second operand (the left operand). The value of the resulting expression is the value of the right operand, so assignment operations can be chained together as shown here:

y = x = n = 42;

In this expression, y is set to the value 42. All assignment operations are subject to rules regarding type conversions, which are discussed later in this chapter in the section “Converting Types.”

The addition assignment operator (+=) combines addition and assignment, adding the first operand to the second and then storing the result in the first operand, as shown here:

y = 40;
y += 2;

This example is equivalent to the following code:

y = 40;
y = y + 2; 

The other operators that combine arithmetic operations with assignment work much like the += operator. The subtraction assignment (-=), multiplication assignment (*=), division assignment (/=), and remainder assignment (%=) operators provide a convenient and concise way to show that a value is being applied to an existing operand.

The left-shift assignment (<<=) and right-shift assignment (>>=) operators combine a shift operation with an assignment operation, storing the result of the shift operation in the first operand, as shown here:

int n = 21;
n <<= 1;
Console.WriteLine(n); // Displays 42

The remaining assignment operators are based on operators that are shared between logical and bitwise operations. These assignment operators behave differently depending on whether they’re used with scalar or Boolean operands.

The bitwise-AND assignment operator (&=) combines an AND operation with assignment. When used with Boolean operands, the first operand and the second operand are combined to form a logical AND expression, and the resulting value is stored in the first operand. When &= is used with scalar values, a bitwise AND operation is performed, as is done for the & operator. The resulting value is stored in the first operand. The following code demonstrates the use of the &= operator:

int n = 0x007F;
n &= 0x00AA;
Console.WriteLine(n); // Displays 42

The bitwise-OR assignment operator (¦=) and bitwise-XOR assignment operator (^=) have characteristics that are similar to the &= operator. The ¦= operator combines an OR operation with an assignment, and the ^= operator combines XOR and an assignment. Both of these operators tailor their behavior to suit the type to which they are applied.

Using the Conditional Operator

The conditional operator (?:) works with three operands, as shown here:

bool isTrue = true;
string result = isTrue? "true": "false";
Console.WriteLine(result); // Displays true

Because it’s the only operator that works with three operands, the ?: operator is sometimes called the ternary operator. The first operand must be a Boolean value or expression. If true, the value of the conditional expression is the second operand; otherwise, the conditional expression’s value is the third operand.

When used judiciously, the ?: operator can result in concise, expressive code. Used to the extreme, it can result in code that’s difficult to maintain and read. For example, the following code compiles and runs as expected, but it’s not as clear as the preceding example:

bool isTrue = true;
Console.WriteLine(isTrue? "true": "false");
Obtaining Type Information

C# includes operators that are used to determine information about the type of an object at runtime. These operators are listed in Table 4-6.

Table 4-6.  Type Information Operators in C#

Operator

Description

is

Tests for a specific type

typeof

Tests for type information

sizeof

Tests for the size of a value type

The is operator is used to test an object to determine whether the object is a specific type, as shown in the following code. The resulting expression has a Boolean value.

public void DisplayObject(object obj)
{
    if(obj is BattingAverage)
    {
        // obj refers to a BattingAverage object.
        
    

    }
}

The typeof operator is used to represent runtime type information about a class, as shown in the following code. This operator is applied to a type name rather than to an object. The resulting expression is a System.Type object.

Type theType = typeof(BattingAverage);

The sizeof operator returns the size of a value type, as shown in the following code. (You can’t determine the size of a reference type.) To use sizeof, your code must be executing in the unsafe context. The unsafe keyword, which you can apply as a modifier to the callable members of a type, denotes an unsafe context that extends from the parameter list to the end of the function. Unsafe code is described in more detail in Chapter 10.

int size = sizeof(BattingAverage);
Addressing Memory Directly

C# also provides operators that are used to address memory directly. These operators can be used only in code executing in the unsafe context. The operators used for direct memory access are listed in Table 4-7.

Table 4-7.  Direct Memory Access Operators in C#

Operator

Description

*

Pointer indirection

->

Member access

[]

Index

&

Address of

Using Other Operators

Several additional operators are used with C#. The first of these is the dot operator (.). The dot operator is used to access members of a namespace, class, structure, or enumeration, as in System.Console.WriteLine.

The dot operator is used much as it is in C or C++. But unlike in those languages, in C# the dot operator is the only member access operator offered. The member access operator (->), which combines pointer dereferencing and member access, is never used except when you’re working with unsafe code, and the scope resolution operator (::) doesn’t exist in C#.

As you saw in Chapter 3, the index operator ([]) is used to access a member element in an array. It’s also used to access an indexer, if one is provided by a class. Declaring an indexer for your class enables you to use the class like an index. Providing indexers for your classes is discussed in Chapter 7. The cast operator (()) is used to attempt to coerce a type change for a value, as shown in the following code. To use the () operator, enclose the new type in parentheses to the left of the value to be cast. If the value can’t be cast into the new type, an InvalidCastException exception is thrown.

public void DisplayObject(object obj)
{
    try
    {
        BattingAverage avg = (BattingAverage)obj;
        
    

    }
    catch(InvalidCastException exc)
    {
        
    

    }
}


Part III: Programming Windows Forms