CHAPTER 13

image

Operators and Expressions

The C# expression syntax is very similar to the C/C++ expression syntax.

Operator Precedence

When an expression contains multiple operators, the precedence of the operators controls the order in which the elements of the expression are evaluated. The default precedence can be changed by grouping elements with parentheses.

int value1 = 1 + 2 * 3;        // 1 + (2 * 3) = 7
int value2 = (1 + 2) * 3;      // (1 + 2) * 3 = 9

In C#, all binary operators are left-associative, which means that operations are performed left to right, except for the assignment and conditional (?:) operators, which are performed right to left.

Table 13-1 summarizes all operators in precedence from highest to lowest.

Table 13-1. Operators in Precedence Order

Category Operators
Primary (x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked default delegate
Unary + -! ∼++x --x(T)x
Multiplicative * /%
Additive + -
Shift << >>
Relational < ><= >=is as
Equality == !=
Logical AND &
Logical XOR ^
Logical OR |
Conditional AND &&
Conditional OR ||
Null coalescing ??
Conditional ?:
Assignment = *=/= %=+= -=<<= > >=&= ^=|=
Anonymous function/lambda (T x) = > y

Built-in Operators

For numeric operations in C#, there are typically built-in operators for the int, uint, long, ulong, float, double, and decimal types. Because there aren’t built-in operators for other types, expressions must first be converted to one of the types for which there is an operator before the operation is performed.

A good way to think about this is to consider that an operator (+ in this case)1 has the following built-in overloads:

int operator +(int x, int y);
uint operator +(uint x, uint y);
long operator +(long x, long y);
ulong operator +(ulong x, ulong y);
float operator +(float x, float y);
double operator +(double x, double y);

Notice that these operations all take two parameters of the same type and return that type. For the compiler to perform an addition, it can use only one of these functions. This means that smaller sizes (such as two short values) cannot be added without them being converted to int, and such an operation will return an int.

The result is that when operations are done with numeric types that can be converted implicitly to int (those types that are “smaller” than int), the result will have to be cast to store it in the smaller type.2

// error
class Test
{
    public static void Main()
    {
       short s1 = 15;
       short s2 = 16;
       short ssum = (short) (s1 + s2);   // cast is required

       int i1 = 15;
       int i2 = 16;
       int isum = i1 + i2;               // no cast required
    }
}

User-Defined Operators

User-defined operators may be declared for classes or structs, and they function in the same manner in which the built-in operators function. In other words, the + operator can be defined on a class or struct so that an expression like a + b is valid. In the following sections, the operators that can be overloaded are marked with “over” in subscript. See Chapter 26 for more information.

Numeric Promotions

Numeric promotions occur when a variable is converted from a smaller type (such as a 2-byte short integer) to a larger type (such as a 4-byte int integer). See Chapter 15 for information on the rules for numeric promotion.

Arithmetic Operators

The following sections summarize the arithmetic operations that can be performed in C#. The floating-point types have very specific rules that they follow.3 For full details, see the CLR. If executed in a checked context, arithmetic expressions on nonfloating types may throw exceptions.

Unary Plus (+)

For unary plus, the result is simply the value of the operand.

Unary Minus (−)

Unary minus works only on types for which there is a valid negative representation, and it returns the value of the operand subtracted from zero.

Bitwise Complement (∼)

The ∼ operator is used to return the bitwise complement of a value.

Addition (+)

In C#, the + sign is used both for addition and for string concatenation.

Numeric Addition

The two operands are added together.

String Concatenation

String concatenation can be performed between two strings or between a string and an operand of type object.4 If either operand is null, an empty string is substituted for that operand.

Operands that are not of type string will be automatically converted to a string by calling the virtual ToString() method on the object.

Subtraction (−)

The second operand is subtracted from the first operand. If the expression is evaluated in a checked context and the difference is outside the range of the result type, an OverflowException is thrown.

Multiplication (*)

The two operands are multiplied together. If the expression is evaluated in a checked context and the result is outside the range of the result type, an OverflowException is thrown.

Division (/)

The first operand is divided by the second operand. If the second operand is zero, a DivideByZero exception is thrown.

Remainder (%)

The result x % y is computed as x – (x / y) * y using integer operations. If y is zero, a DivideByZero exception is thrown.

Shift (<< and >>)

For left shifts, the high-order bits are discarded, and the low-order empty bit positions are set to zero.

For right shifts with uint or ulong, the low-order bits are discarded, and the high-order empty bit positions are set to zero.

For right shifts with int or long, the low-order bits are discarded, and the high-order empty bit positions are set to zero if x is non-negative and are set to 1 if x is negative.

Increment and Decrement (++ and --)

The increment operator increases the value of a variable by 1, and the decrement operator decreases the value of the variable by 1.5

Increment and decrement can be used either as a prefix operator, where the variable is modified before it is read, or as a postfix operator, where the value is returned before it is modified.

Here’s an example:

int k = 5;
int value = k++;    // value is 5
value = −-k;    // value is still 5
value = ++k;    // value is 6

Note that increment and decrement are exceptions to the rule about smaller types requiring casts to function. A cast is required when adding two shorts and assigning them to another short.

short s = (short) a + b;

Such a cast is not required for an increment of a short.6

s++;

Relational and Logical Operators

Relational operators are used to compare two values, and logical operators are used to perform bitwise operations on values.

Logical Negation (!)

The ! operator is used to return the negation of a Boolean value.

Relational Operators over

C# defines the following relational operations, shown in Table 13-2.

Table 13-2. C# Relational Operators

Operation Description
a == b Returns true if a is equal to b
a != b Returns true if a is not equal to b
a < b Returns true if a is less than b
a <= b Returns true if a is less than or equal to b
a > b Returns true if a is greater than b
a>= b Returns true if a is greater than or equal to b

These operators return a result of type bool.

When performing a comparison between two reference-type objects, the compiler will first look for user-defined relational operators defined on the objects (or base classes of the objects). If it finds no applicable operator and the relational is == or !=, the appropriate relational operator will be called from the object class. This operator compares whether the two operands reference the same instance, not whether they have the same value.

For value types, the process is the same if the operators == and != are overloaded. If they aren’t overloaded, there is no default implementation for value types, and an error is generated.

The overloaded versions of == and != are closely related to the Object.Equals() member. See Chapter 32 for more information.

For the string type, the relational operators are overloaded so that == and != compare the values of the strings, not the references.

To compare references of two instances that have overloaded relational operators, cast the operators to object.

if ((object) string1 == (object) string 2) // reference comparison

Logical Operators

C# defines the following logical operators, as listed in Table 13-3.

Table 13-3. C# Logical Operators

Operator Description
& Bitwise AND of the two operands
| Bitwise OR of the two operands
^ Bitwise exclusive OR (XOR) of the two operands
&& Logical AND of the two operands
|| Logical OR of the two operands

The operators &, |, and ^ are usually used on integer data types, though they can also be applied to the bool type.

The operators && and || differ from the single-character versions in that they perform short-circuit evaluation. In the expression

a && b

b is evaluated only if a is true. In the expression

a || b

b is evaluated only if a is false.

Conditional Operator (?:)

Sometimes called the ternary or question operator, the conditional operator selects from two expressions based on a Boolean expression.

int value = (x <10) ? 15 : 5;

This is equivalent to the following:

int value;
if (x <10)
{
    value = 15;
}
else
{
    value = 5;
}

image Tip  The conditional operator is great for examples like this; it saves lines and is easier to read. Be careful with complex statements that include method calls or calculations; they are often much clearer with a traditional if/else statement.

Null Coalescing Operator (??)

The null coalescing operator is used to provide a default value for a null value. This example

string s = name ?? " <unknown> ";

is equivalent to the following:

string s;
if (name ! = null)
{
    s = name;
}
else
{
    s = " <unknown> ";
}

For more on nullable types, see Chapter 27.

Assignment Operators

Assignment operators are used to assign a value to a variable. There are two forms: the simple assignment and the compound assignment.

Simple Assignment

Simple assignment is done in C# using the single equal (=) sign. For the assignment to succeed, the right side of the assignment must be a type that can be implicitly converted to the type of the variable on the left side of the assignment.

Compound Assignment

Compound assignment operators perform some operation in addition to simple assignment. The compound operators are the following:

+=   -=   *=   /=   %=   &=   |=   ^=   <<=   >>=

The compound operator

x <op>= y

is evaluated exactly as if it were written as

x = x <op> y

with these two exceptions:

  • x is evaluated only once, and that evaluation is used for both the operation and the assignment.
  • If x contains a function call or array reference, it is performed only once.

Under normal conversion rules, if x and y are both short integers, then evaluating

x = x + 3;

would produce a compile-time error, because addition is performed on int values, and the int result is not implicitly converted to a short. In this case, because short can be implicitly converted to int, it is possible to write the following:

x = 3;

Type Operators

Rather than dealing with the values of an object, the type operators are used to deal with the type of an object.

typeof

The typeof operator returns the type of the object, which is an instance of the System.Type class. The typeof operator is useful to avoid having to create an instance of an object just to obtain the type object. If an instance already exists, a type object can be obtained by calling the GetType() function on the instance.

Once the type object is obtained for a type, it can be queried using reflection to obtain information about the type. See the “Deeper Reflection” section in Chapter 40 for more information.

is

The is operator is used to determine whether an object reference can be converted to a specific type or interface. The most common use of this operator is to determine whether an object supports a specific interface.

using System;
interface IAnnoy
{
    void PokeSister(string name);
}
class Brother: IAnnoy
{
    public void PokeSister(string name)
    {
       Console.WriteLine("Poking {0}", name);
    }
}
class BabyBrother
{
}
class Test
{
    public static void AnnoyHer(string sister, params object[] annoyers)
    {
       foreach (object o in annoyers)
       {
            if (o is IAnnoy)
            {
                IAnnoy annoyer = (IAnnoy) o;
                annoyer.PokeSister(sister);
            }
       }
    }
    public static void Main()
    {
       Test.AnnoyHer("Jane", new Brother(), new BabyBrother());
    }
}

This code produces the following output:

Poking: Jane

In this example, the Brother class implements the IAnnoy interface, and the BabyBrother class doesn’t. The AnnoyHer() function walks through all the objects that are passed to it, checks to see whether an object supports IAnnoy, and then calls the PokeSister() function if the object supports the interface.

as

The as operator is very similar to the is operator, but instead of just determining whether an object is a specific type or interface, it also performs the explicit conversion to that type. If the object can’t be converted to that type, the operator returns null. Using as is more efficient than the is operator, since the as operator needs to check the type of the object only once, while the example using is checks the type when the operator is used and again when the conversion is performed.

In the previous example, this code

            if (o is IAnnoy)
            {
                IAnnoy annoyer = (IAnnoy) o;
                annoyer.PokeSister(sister);
            }

can be replaced with this:

            IAnnoy annoyer = o as IAnnoy;
            if (annoyer ! = null)
            {
                annoyer.PokeSister(sister);
            }

Note that the as operator can’t be used with boxed value types. This example

int value = o as int;

doesn’t work, because there’s no way to get a null value of a value type.

Checked and Unchecked Expressions

When dealing with expressions, it’s often difficult to strike the right balance between the performance of expression evaluation and the detection of overflow in expressions or conversions. Some languages choose performance and can’t detect overflow, and other languages put up with reduced performance and always detect overflow.

In C#, the programmer is able to choose the appropriate behavior for a specific situation. This is done using the checked and unchecked keywords.

Code that depends upon the detection of overflow can be wrapped in a checked block.

using System;
class Test
{
    public static void Main()
    {
       checked
       {
            byte a = 55;
            byte b = 210;
            byte c = (byte) (a + b);
       }
    }
}

When this code is compiled and executed, it will generate an OverflowException.

Similarly, if the code depends on the truncation behavior, the code can be wrapped in an unchecked block.

using System;
class Test
{
    public static void Main()
    {
       unchecked
       {
            byte a = 55;
            byte b = 210;
            byte c = (byte) (a + b);
       }
    }
}

For the remainder of the code, the behavior can be controlled with the /checked + compiler switch. Usually, /checked + is turned on for debug builds to catch possible problems and then turned off in retail builds to improve performance.

Type Inference (var)

C# allows method variables that are initialized to be declared using var instead of the type of the expression. Here’s an example:

int age = 33;
var height = 72;

Both age and height are of type int; in the first case, the type is set explicitly, and in the second case, the type is inferred from the type of the expression.

Type inference was added to C# because it is not possible to specify the name of an anonymous type, and therefore there needed to be some way to declare a variable of such a type. Type inference is often used in Linq; see Chapter 28 for more information.

Best Practices

There is a tension in the use of var between the simplicity of expression that it allows and the ambiguity that it can create. I recommend using var only with Linq and in cases where its use would prevent saying the same thing twice. This example

Dictionary<string, Guid> personIds = new Dictionary<string,Guid> ();

lists the same type twice, and this alternative

var personIds = new Dictionary<string,Guid> ();

is shorter and easier to type, and it is still very clear what the type of personIds is. However, the use of var is not recommended in other situations. This example

var personIds = CreateIdLookup();

does not provide any clue about what the type of personIds is, and the code is therefore harder to understand.

1 There are also overloads for string, but that’s outside the scope of this example.

2 You may object to this, but you really wouldn’t like the type system of C# if it didn’t work this way. It is, however, a considerable departure from C++.

3 They conform to IEEE 754 arithmetic.

4 Since any type can convert to object, this means any type.

5 In unsafe mode, pointers increment and decrement by the size of the pointed-to object. See Chapter 40 for more details.

6 In other words, there are predefined increment and decrement functions for the types smaller than int and uint.

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

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