Operators and Expressions in C#

The C# expression syntax is based upon the C++ expression syntax.

1. 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. You can change the default prece­dence by grouping elements with parentheses, like so:

int value = 1 + 2 * 3;                     // 1 + (2 * 3) = 7

value = (1 + 2) * 3;                       // (1 + 2) * 3 = 9

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

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

2. Built-in Operators

For numeric operations in C#, the int, uint, long, ulong, float, double, and decimal types typi­cally have built-in operators. Because other types don’t have built-in operators, you must first convert an expression to one of the types that has 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 smaller sizes (such as two short values) can’t be added without them being converted to int, and such an operation will return int.

The result of this is that when operations are done with numeric types that don’t have a built-in operator but can be implicitly converted to a type that does, the result of the operation will be “larger” than the two types that provided input into the operator. This requires the result to be explicitly cast back to the “smaller” type.[1] [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

}

}

3. User-Defined Operators

You can declare user-defined operators for classes or structs, and they function in the same manner in which the built-in operators function. In other words, you can define the + operator on a class or struct so that an expression such as 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.

4. Numeric Promotions

See Chapter 15 for information on the rules for numeric promotion.

5. Arithmetic Operators

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

Unary Plus (+) over

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

Unary Minus (-) over

Unary minus works only on types that have a valid negative representation, and it returns the value of the operand subtracted from zero.

Bitwise Complement (~) over

The ~ operator returns the bitwise complement of a value.

Addition (+) over

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

Numeric Addition

The two operands are added together. If the expression is evaluated in a checked context and the sum is outside the range of the result type, an OverflowException is thrown. The following code demonstrates this:

using System; class Test {

public static void Main()

{

byte val1 = 200; byte val2 = 201;

byte sum = (byte) (val1 + val2);                  // no exception

checked {

byte sum2 = (byte) (val1 + val2);             // exception

}

}

}

String Concatenation

You can perform string concatenation 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 aren’t of type string will be automatically converted to a string by calling the virtual ToString() method on the object.

Subtraction (-) over

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 (*) over

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 (/) over

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

Remainder (%) over

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 >>) over

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 0 if x is non-negative and to 1 if x is negative.

Increment and Decrement (++ and –) over

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

Increment and decrement can be used either as a prefix operator, where the variable is modified before it’s read, or as a postfix operator, where the value is returned before it’s modified. For 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 isn’t required for an increment of a short:

s++;

6. Relational and Logical Operators

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

Logical Negation (!) over

The ! operator returns the negation of a Boolean value.

Relational Operators over

C# defines the relational operations shown in Table 14-2.

These operators return a result of type bool.

When performing a comparison between two reference-type objects, the compiler will first look for 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== and != are overloaded. If they aren’t overloaded, there’s 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 29 for more information.

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

Logical Operators over

C# defines the logical operators shown in Table 14-3.

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 following expression, b is evaluated only if a is true:

a && b

In the following expression, b is evaluated only if a is false:

a || b

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;

In this example, the control expression (x < 10) is evaluated. If it’s true, the value of the operator is the first expression following the question mark, which is 15 in this case. If the control expression is false, the value of the operator is the expression following the colon, which is 5 in this case.

7. Assignment Operators

Assignment operators assign a value to a variable. They come in two forms: the simple assign­ment and the compound assignment.

Simple Assignment

You can perform simple assignment in C# using the single equals (=) 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 following are the compound operators:

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

The compound operator x <op>= y is evaluated exactly as if it were written as x = x <op> y with 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’s performed only once.

Under normal conversion rules, if x and y are both short integers, evaluating x = x + 3; would produce a compile-time error, because addition is performed on int values and the int result isn’t implicitly converted to a short. In this case, however, because short can be implicitly converted to int and it’s possible to write the following, the operation is permitted:

x = 3;

8. Type Operators

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

8.1. typeof

The typeof operator returns the type of the object, which is an instance of the System.Type class. Typeof 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 you’ve obtained the type object for a type, you can query it using reflection to obtain information about the type. See Chapter 38 for more information.

8.2. is

The is operator determines 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 if an object supports IAnnoy, and then calls the PokeSister() function if the object supports the interface.

8.3. as

The as operator is 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, you could replace these lines:

if (o is IAnnoy)

{

IAnnoy annoyer = (IAnnoy) o;

annoyer.PokeSister(sister);

}

with the following ones:

IAnnoy annoyer = o as IAnnoy;

if (annoyer != null)

annoyer.PokeSister(sister);

Note that you can’t use the as operator with boxed value types. The following doesn’t work, because there’s no way to get a null value as a value type:

int value = o as int;

9. 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 conver­sions. 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 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, you can control the behavior 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.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *