C H A P T E R  8

Expressions and Operators

Expressions

This chapter defines expressions and describes the operators provided by C#. It also explains how you can define the C# operators to work with your user-defined classes.

An operator is a symbol that represents an operation that returns a single result. An operand is a data element used as input by the operator. An operator does the following:

  • Takes its operands as input
  • Performs an action
  • Returns a value, based on the action

An expression is a string of operators and operands. The C# operators take one, two, or three operands. The following are some of the constructs that can act as operands:

  • Literals
  • Constants
  • Variables
  • Method calls
  • Element accessors, such as array accessors and indexers
  • Other expressions

Expressions can be combined, using operators, to create more complex expressions, as shown in the following expression with three operators and four operands:

Image

Evaluating an expression is the process of applying each operator to its operands, in the proper sequence, to produce a value.

  • The value is returned to the position at which the expression was evaluated. There, it might in turn be used as an operand in an enclosing expression.
  • Besides the value returned, some expressions also have side effects, such as setting a value in memory.

Literals

Literals are numbers or strings typed into the source code that represent a specific, set value of a specific type.

For example, the following code shows literals of six types. Notice, for example, the difference between the double literal and the float literal.

   static void Main()         Literals
   {
                                 
      Console.WriteLine("{0}", 1024);            // int literal
      Console.WriteLine("{0}", 3.1416);          // double literal
      Console.WriteLine("{0}", 3.1416F);         // float literal
      Console.WriteLine("{0}", true);            // boolean literal
      Console.WriteLine("{0}", 'x'),             // character literal
      Console.WriteLine("{0}", "Hi there");      // string literal
   }

The output of this code is the following:


1024
3.1416
3.1416
True
x
Hi there

Because literals are written into the source code, their values must be known at compile time. Several of the predefined types have their own forms of literal:

  • Type bool has two literals: true and false.
  • For reference type variables, literal null means that the variable does not point to data in memory.

Integer Literals

Integer literals are the most commonly used literals. They are written as a sequence of decimal digits, with the following characteristics:

  • No decimal point
  • An optional suffix to specify the type of the integer

For example, the following lines show four literals for the integer 236. Each is interpreted by the compiler as a different type of integer, depending on its suffix.

   236               // int
   236L              // long
   236U              // unsigned int
   236UL             // unsigned long

Integer-type literals can also be written in hexadecimal (hex) form. The digits must be hex digits (0 through F), and the string must be prefaced with either 0x or 0X (numeral 0, letter x).

Figure 8-1 shows the forms of the integer literal formats. Components with names in square brackets are optional.

  
Image

Figure 8-1. The integer literal formats

Table 8-1 lists the integer literal suffixes. For a given suffix, the compiler will interpret the string of digits as the smallest of the four listed integer types that can represent the value without losing data.

For example, take the literals 236 and 5000000000, neither of which has a suffix. Since 236 can be represented with 32 bits, it will be interpreted by the compiler as an int. The second number, however, won’t fit into 32 bits, so the compiler will represent it as a long.

Image

Real Literals

Literals for real numbers consist of the following:

  • Decimal digits
  • An optional decimal point
  • An optional exponent part
  • An optional suffix

For example, the following code shows various formats of literals of the real types:

   float  f1 = 236F;
   double d1 = 236.714;
   double d2 = .35192;
   double d3 = 6.338e-26;

Figure 8-2 shows the valid formats for real literals. Components with names in square brackets are optional. Table 8-2 shows the real suffixes and their meanings.

Image

Figure 8-2. The real literal formats

Image

Image Note Real literals without a suffix are of type double, not float!

Character Literals

A character literal consists of a character representation between two single quote marks. A character representation can be any of the following: a single character, a simple escape sequence, a hex escape sequence, or a Unicode escape sequence.

  • The type of a character literal is char.
  • A simple escape sequence is a backslash followed by a single character.
  • A hex escape sequence is a backslash, followed by an uppercase or lowercase x, followed by up to four hex digits.
  • A Unicode escape sequence is a backslash, followed by an uppercase or lowercase u, followed by up to four hex digits.

For example, the following code shows various formats of character literals:

   char c1 = 'd';                       // Single character
   char c2 = ' ';                      // Simple escape sequence
   char c3 = 'x0061';                  // Hex escape sequence
   char c4 = 'u005a';                  // Unicode escape sequence

Table 8-3 shows some of the important special characters and their encodings.

Image

String Literals

String literals use double quote marks rather than the single quote marks used in character literals. There are two types of string literals:

  • Regular string literals
  • Verbatim string literals

A regular string literal consists of a sequence of characters between a set of double quotes. A regular string literal can include the following:

  • Characters
  • Simple escape sequences
  • Hex and Unicode escape sequences

Here’s an example:

   string st1 = "Hi there!";
   string st2 = "Val1 5, Val2 10";
   string st3 = "Addx000ASomeu0007Interest";

A verbatim string literal is written like a regular string literal but is prefaced with an @ character. The important characteristics of verbatim string literals are the following:

  • Verbatim literals differ from regular string literals in that escape sequences are not evaluated. Everything between the set of double quotes—including what would normally be considered escape sequences—is printed exactly as it is listed in the string.
  • The only exception with verbatim literals is sets of contiguous double quotes, which are interpreted as a single double quote character.

For example, the following code compares some regular and verbatim string literals:

   string rst1 = "Hi there!";
   string vst1 = @"Hi there!";

   string rst2 = "It started, "Four score and seven..."";
   string vst2 = @"It started, ""Four score and seven...""";

   string rst3 = "Value 1 5, Val2 10";    // Interprets tab esc sequence
   string vst3 = @"Value 1 5, Val2 10";   // Does not interpret tab

   string rst4 = "C:\Program Files\Microsoft\";
   string vst4 = @"C:Program FilesMicrosoft";

   string rst5 = " Print x000A Multiple u000A Lines";
   string vst5 = @" Print
    Multiple
    Lines";

Printing these strings produces the following output:


Hi there!
Hi there!

It started, "Four score and seven..."
It started, "Four score and seven..."

Value 1          5, Val2         10
Value 1 5, Val2 10

C:Program FilesMicrosoft
C:Program FilesMicrosoft

 Print
 Multiple
 Lines

 Print
 Multiple
 Lines

Image Note The compiler saves memory by having identical string literals share the same memory location in the heap.

Order of Evaluation

An expression can be made up of many nested subexpressions. The order in which the subexpressions are evaluated can make a difference in the final value of the expression.

For example, given the expression 3 * 5 + 2, there are two possible results depending on the order in which the subexpressions are evaluated, as shown in Figure 8-3.

  • If the multiplication is performed first, the result is 17.
  • If the 5 and the 2 are added together first, the result is 21.
Image

Figure 8-3. Simple order of evaluation

Precedence

You know from your grade-school days that in the preceding example, the multiplication must be performed before the addition because multiplication has a higher precedence than addition. But unlike grade-school days, where you had four operators and two levels of precedence, things are a bit more complex with C#, which has more than 45 operators and 14 levels of precedence.

Table 8-4 shows the complete list of operators and the precedence of each. The table lists the highest precedence operators at the top and continues to the lowest precedence operators at the bottom.

Image
Image

Associativity

When the compiler is evaluating an expression where all the operators have different levels of precedence, then each subexpression is evaluated, starting at the one with the highest level, and working down the precedence scale.

But what if two sequential operators have the same level of precedence? For example, given the expression 2 / 6 * 4, there are two possible evaluation sequences:

(2 / 6) * 4 = 4/3

or

2 / (6 * 4) = 1/12

When sequential operators have the same level of precedence, the order of evaluation is determined by operator associativity. That is, given two operators of the same level of precedence, one or the other will have precedence, depending on the operators’ associativity. Some important characteristics of operator associativity are the following and are summarized in Table 8-5:

  • Left-associative operators are evaluated from left to right.
  • Right-associative operators are evaluated from right to left.
  • Binary operators, except the assignment operators, are left-associative.
  • The assignment operators and the conditional operator are right-associative.

Therefore, given these rules, the preceding example expression should be grouped left to right, giving (2 / 6) * 4, which yields 4/3.

Image

You can explicitly set the order of evaluation of the subexpressions of an expression by using parentheses. Parenthesized subexpressions

  • Override the precedence and associativity rules
  • Are evaluated in order from the innermost nested set to the outermost

Simple Arithmetic Operators

The simple arithmetic operators perform the four basic arithmetic operations and are listed in Table 8-6. These operators are binary and left-associative.

Image

The arithmetic operators perform the standard arithmetic operations on all the predefined simple arithmetic types.

The following are examples of the simple arithmetic operators:

   int x1 = 5 + 6;          double d1 = 5.0 + 6.0;
   int x2 = 12 - 3;         double d2 = 12.0 - 3.0;
   int x3 = 3 * 4;          double d3 = 3.0 * 4.0;
   int x4 = 10 / 3;         double d4 = 10.0 / 3.0;

   byte b1 = 5 + 6;
   sbyte sb1 = 6 * 5;

The Remainder Operator

The remainder operator (%) divides the first operand by the second operand, ignores the quotient, and returns the remainder. Table 8-7 gives its description.

The remainder operator is binary and left-associative.

Image

The following lines show examples of the integer remainder operator:

  • 0 % 3 = 0, because 0 divided by 3 is 0 with a remainder of 0.
  • 1 % 3 = 1, because 1 divided by 3 is 0 with a remainder of 1.
  • 2 % 3 = 2, because 2 divided by 3 is 0 with a remainder of 2.
  • 3 % 3 = 0, because 3 divided by 3 is 1 with a remainder of 0.
  • 4 % 3 = 1, because 4 divided by 3 is 1 with a remainder of 1.

The remainder operator can also be used with real numbers to give real remainders.

   Console.WriteLine("0.0f % 1.5f is {0}" , 0.0f % 1.5f);
   Console.WriteLine("0.5f % 1.5f is {0}" , 0.5f % 1.5f);
   Console.WriteLine("1.0f % 1.5f is {0}" , 1.0f % 1.5f);
   Console.WriteLine("1.5f % 1.5f is {0}" , 1.5f % 1.5f);
   Console.WriteLine("2.0f % 1.5f is {0}" , 2.0f % 1.5f);
   Console.WriteLine("2.5f % 1.5f is {0}" , 2.5f % 1.5f);

This code produces the following output:


0.0f % 1.5f is 0               // 0.0 / 1.5 = 0 remainder 0
0.5f % 1.5f is 0.5             // 0.5 / 1.5 = 0 remainder .5
1.0f % 1.5f is 1               // 1.0 / 1.5 = 0 remainder 1
1.5f % 1.5f is 0               // 1.5 / 1.5 = 1 remainder 0
2.0f % 1.5f is 0.5             // 2.0 / 1.5 = 1 remainder .5
2.5f % 1.5f is 1               // 2.5 / 1.5 = 1 remainder 1

Relational and Equality Comparison Operators

The relational and equality comparison operators are binary operators that compare their operands and return a value of type bool. Table 8-8 lists these operators.

The relational and equality operators are binary and left-associative.

Image

A binary expression with a relational or equality operator returns a value of type bool.

Image Note Unlike C and C++, numbers in C# do not have a Boolean interpretation.

   int x = 5;
   if( x )           // Wrong.  x is of type int, not type boolean.
      ...
   if( x == 5 )      // Fine, since expression returns a value of type boolean
      ...

When printed, the Boolean values true and false are represented by the string output values True and False.

   int x = 5, y = 4;
   Console.WriteLine("x == x is {0}" , x == x);
   Console.WriteLine("x == y is {0}" , x == y);

This code produces the following output:


x == x is True
x == y is False

Comparison and Equality Operations

When comparing most reference types for equality, only the references are compared.

  • If the references are equal—that is, if they point to the same object in memory—the equality comparison is true; otherwise, it is false, even if the two separate objects in memory are exactly equivalent in every other respect.
  • This is called a shallow comparison.
  • Figure 8-4 illustrates the comparison of reference types.
  • On the left of the figure, the references held by both a and b are the same, so a comparison would return true.
  • On the right of the figure, the references are not the same, so even if the contents of the two AClass objects were exactly the same, the comparison would return false.
Image

Figure 8-4. Comparing reference types for equality

Objects of type string are also reference types but are compared differently. When strings are compared for equality, they are compared for length and case-sensitive content.

  • If two strings have the same length and the same case-sensitive content, the equality comparison returns true, even if they occupy different areas of memory.
  • This is called a deep comparison.

Delegates, which are covered in Chapter 15, are also reference types and also use deep comparison. When delegates are compared for equality, the comparison returns true if both delegates are null or if both have the same number of members in their invocation lists and the invocation lists match.

When comparing numeric expressions, the types and values are compared. When comparing enum types, the comparisons are done on the underlying values of the operands. Enums are covered in Chapter 13.

Increment and Decrement Operators

The increment operator adds 1 to the operand. The decrement operator subtracts 1 from the operand. Table 8-9 lists the operators and their descriptions.

These operators are unary and have two forms, the pre- form and the post- form, which act differently.

  • In the pre- form, the operator is placed before the operand; for example, ++x and --y.
  • In the post- form, the operator is placed after the operand; for example, x++ and y--.

Image

In comparing the pre- and post- forms of the operators

  • The final, stored value of the operand variable after the statement is executed is the same regardless of whether the pre- or post- form of the operator is used.
  • The only difference is the value returned by the operator to the expression.

Table 8-10 shows an example summarizing the behavior.

Image

For example, the following is a simple demonstration of the four different versions of the operators. To show the different results on the same input, the value of the operand x is reset to 5 before each assignment statement.

   int x = 5, y;
   y = x++;   // result: y: 5, x: 6
   Console.WriteLine("y: {0}, x: {1}" , y, x);
   
   x = 5;
   y = ++x;   // result: y: 6, x: 6
   Console.WriteLine("y: {0}, x: {1}" , y, x);
   
   x = 5;
   y = x--;   // result: y: 5, x: 4
   Console.WriteLine("y: {0}, x: {1}" , y, x);
   
   x = 5;
   y = --x;   // result: y: 4, x: 4
   Console.WriteLine("y: {0}, x: {1}" , y, x);

This code produces the following output:


y: 5, x: 6
y: 6, x: 6
y: 5, x: 4
y: 4, x: 4

Conditional Logical Operators

The logical operators are used for comparing or negating the logical values of their operands and returning the resulting logical value. Table 8-11 lists the operators.

The logical AND and logical OR operators are binary and left-associative. The logical NOT is unary.

Image

The syntax for these operators is the following, where Expr1 and Expr2 evaluate to Boolean values:

   Expr1 && Expr2
   Expr1 || Expr2
      !    Expr

The following are some examples:

   bool bVal;
   bVal = (1 == 1) && (2 == 2);      // True, both operand expressions are true
   bVal = (1 == 1) && (1 == 2);      // False, second operand expression is false
   
   bVal = (1 == 1) || (2 == 2);      // True, both operand expressions are true
   bVal = (1 == 1) || (1 == 2);      // True, first operand expression is true
   bVal = (1 == 2) || (2 == 3);      // False, both operand expressions are false
   
   bVal = true;                      // Set bVal to true.
   bVal = !bVal;                     // bVal is now false.

The conditional logical operators operate in “short-circuit” mode, meaning that, if after evaluating Expr1 the result can already be determined, then it skips the evaluation of Expr2. The following code shows examples of expressions in which the value can be determined after evaluating the first operand:

   bool bVal;
   bVal = (1 == 2) && (2 == 2);    // False, after evaluating first expression
   
   bVal = (1 == 1) || (1 == 2);    // True, after evaluating first expression

Because of the short-circuit behavior, do not place expressions with side effects (such as changing a value) in Expr2, since they might not be evaluated. In the following code, the post-increment of variable iVal would not be executed, because after the first subexpression is executed, it can be determined that the value of the entire expression is false.

   bool bVal; int iVal = 10;
   
   bVal = (1 == 2) && (9 == iVal++);       // result:  bVal = False, iVal = 10
                              
            False           Never evaluated

Logical Operators

The bitwise logical operators are often used to set the bit patterns for parameters to methods. Table 8-12 lists the bitwise logical operators.

These operators, except for the bitwise negation operator, are binary and left-associative. The bitwise negation operator is unary.

Image

The binary bitwise operators compare the corresponding bits at each position in each of their two operands, and they set the bit in the return value according to the logical operation.

Figure 8-5 shows four examples of the bitwise logical operations.

Image

Figure 8-5. Examples of bitwise logical operators

The following code implements the preceding examples:

   const byte x = 12, y = 10;
   sbyte a;

   a = x & y;                   //  a = 8
   a = x | y;                   //  a = 14
   a = x ^ y;                   //  a = 6
   a = ~x;                      //  a = -13

Shift Operators

The bitwise shift operators shift the bit pattern either right or left a specified number of positions, with the vacated bits filled with 0s or 1s. Table 8-13 lists the shift operators.

The shift operators are binary and left-associative. The syntax of the bitwise shift operators is shown here. The number of positions to shift is given by Count.

   Operand << Count                         // Left shift
   Operand >> Count                         // Right shift

Image

For the vast majority of programming in C#, you don’t need to know anything about the hardware underneath. If you’re doing bitwise manipulation of signed numbers, however, it can be helpful to know about the numeric representation. The underlying hardware represents signed binary numbers in a form called two’s complement. In two’s-complement representation, positive numbers have their normal binary form. To negate a number, you take the bitwise negation of the number and add 1 to it. This process turns a positive number into its negative representation, and vice versa. In two’s complement, all negative numbers have a 1 in the leftmost bit position. Figure 8-6 shows the negation of the number 12.

Image

Figure 8-6. To get the negation of a two’s-complement number, take its bitwise negation and add 1.

The underlying representation is important when shifting signed numbers because the result of shifting an integral value one bit to the left is the same as multiplying it by two. Shifting it to the right is the same as dividing it by two.

If, however, you were to shift a negative number to the right and the leftmost bit were to be filled with a 0, it would produce the wrong result. The 0 in the leftmost position would indicate a positive number. But this is incorrect, because dividing a negative number by 2 doesn’t produce a positive number.

To address this situation, when the operand is a signed integer, if the leftmost bit of the operand is a 1 (indicating a negative number), bit positions opening up on the left are filled with 1s rather than 0s. This maintains the correct two’s-complement representation. For positive or unsigned numbers, bit positions opening up on the left are filled with 0s.

Figure 8-7 shows how the expression 14 << 3 would be evaluated in a byte. This operation causes the following:

  • Each of the bits in the operand (14) is shifted three places to the left.
  • The three bit positions vacated on the right end are filled with 0s.
  • The resulting value is 112.
Image

Figure 8-7. Example of left shift of three bits

Figure 8-8 illustrates bitwise shift operations.

Image

Figure 8-8. Bitwise shifts

The following code implements the preceding examples:

   int a, b, x = 14;

   a = x << 3;              // Shift left
   b = x >> 3;              // Shift right

   Console.WriteLine("{0} << 3 = {1}" , x, a);
   Console.WriteLine("{0} >> 3 = {1}" , x, b);

This code produces the following output:


14 << 3 = 112
14 >> 3 = 1

Assignment Operators

The assignment operators evaluate the expression on the right side of the operator and use that value to set the value of the variable expression on the left side of the operator. Table 8-14 lists the assignment operators.

The assignment operators are binary and right-associative.

Image

The syntax is as follows:

   VariableExpression Operator Expression

For simple assignment, the expression to the right of the operator is evaluated, and its value is assigned to the variable on the left.

   int x;
   x = 5;
   x = y * z;

Remember that an assignment expression is an expression, and therefore returns a value to its position in the statement. The value of an assignment expression is the value of the left operand, after the assignment is performed. So, in the case of expression x = 10, the value 10 is assigned to variable x. The value of x, which is now 10, becomes the value of the whole expression.

Since an assignment is an expression, it can be part of a larger expression, as shown in Figure 8-9. The evaluation of the expression is as follows:

  • Since assignment is right-associative, evaluation starts at the right and assigns 10 to variable x.
  • That expression is the right operand of the assignment to variable y, so the value of x, which is now 10, is assigned to y.
  • The assignment to y is the right operand for the assignment to z—leaving all three variables with the value 10.
Image

Figure 8-9. An assignment expression returns the value of its left operand after the assignment has been performed.

The types of objects that can be on the left side of an assignment operator are the following. They are discussed later in the text.

  • Variables (local variables, fields, parameters)
  • Properties
  • Indexers
  • Events

Compound Assignment

Frequently, you’ll want to evaluate an expression and add the results to the current value of a variable, as shown here:

   x = x + expr;

The compound assignment operators allow a shorthand method for avoiding the repetition of the left-side variable on the right side under certain common circumstances. For example, the following two statements are semantically equivalent, but the second is shorter and just as easy to understand.

   x = x + (y – z);
   x += y – z;

The other compound assignment statements are analogous:

                                     Notice the parentheses.                                               
   x *= y – z;      // Equivalent to x = x *
(y – z)
   x /= y – z;      // Equivalent to x = x / (y – z)
     ...

The Conditional Operator

The conditional operator is a powerful and succinct way of returning one of two values, based on the result of a condition. Table 8-15 shows the operator.

The conditional operator is ternary.

Image

The syntax for the conditional operator is shown below. It has a test expression and two result expressions.

  • Condition must return a value of type bool.
  • If Condition evaluates to true, then Expression1 is evaluated and returned. Otherwise, Expression2 is evaluated and returned.
   Condition ? Expression1 : Expression2

The conditional operator can be compared with the if...else construct. For example, the following if...else construct checks a condition, and if the condition is true, the construct assigns 5 to variable intVar. Otherwise, it assigns it the value 10.

   if ( x < y )                                       // if...else
      intVar = 5;
   else
      intVar = 10;

The conditional operator can perform the same operation in a less verbose form, as shown in the following statement:

   intVar = x < y  ?  5  :  10;                       // Conditional operator

Placing the condition and each return expression on separate lines, as in the following code, makes the intent very easy to understand.

   intVar = x < y
            ?  5
            :  10 ;

Figure 8-10 compares the two forms shown in the example.

Image

Figure 8-10. The conditional operator vs. if...else

For example, the following code uses the conditional operator three times—once in each of the WriteLine statements. In the first instance, it returns either the value of x or the value of y. In the second two instances, it returns either the empty string or the string “not”.

   int x = 10, y = 9;
   int highVal = x > y                            // Condition
                    ? x                           // Expression 1
                    : y;                          // Expression 2
   Console.WriteLine("highVal:  {0} " , highVal);

   Console.WriteLine("x is{0} greater than y" ,
                         x > y                    // Condition
                         ? ""                     // Expression 1
                         : " not" );              // Expression 2
   y = 11;
   Console.WriteLine("x is{0} greater than y" ,
                         x > y                    // Condition
                         ? ""                     // Expression 1
                         : " not" );              // Expression 2

This code produces the following output:


highVal:  10

x is greater than y
x is not greater than y

Image Note The if...else statement is a flow-of-control statement. It should be used for doing one or the other of two actions. The conditional operator returns an expression. It should be used for returning one or the other of two values.

Unary Arithmetic Operators

The unary operators set the sign of a numeric value. They are listed in Table 8-16.

  • The unary positive operator simply returns the value of the operand.
  • The unary negative operator returns the value of the operand subtracted from 0.

Image

For example, the following code shows the use and results of the operators:

   int x = +10;         // x = 10
   int y = -x;          // y = -10
   int z = -y;          // z = 10

User-Defined Type Conversions

User-defined conversions are discussed in greater detail in Chapter 16, but I’ll mention them here as well because they are operators.

  • You can define both implicit and explicit conversions for your own classes and structs. This allows you to convert an object of your user-defined type to some other type, and vice versa.
  • C# provides implicit and explicit conversions.
    • With an implicit conversion, the compiler automatically makes the conversion, if necessary, when it is resolving what types to use in a particular context.
    • With an explicit conversion, the compiler will make the conversion only when an explicit cast operator is used.

The syntax for declaring an implicit conversion is the following. The public and static modifiers are required for all user-defined conversions.

         Required                     Target               Source
                                                                 
   public static implicit operator TargetType ( SourceType Identifier )
   {
         ...
      return ObjectOfTargetType;
   }

The syntax for the explicit conversion is the same, except that explicit is substituted for implicit.

The following code shows an example of declarations for conversion operators that will convert an object of type LimitedInt to type int, and vice versa.

   class LimitedInt                   Target     Source
   {
                                                      
      public static implicit operator int (LimitedInt li)    // LimitedInt to int
      {
         return li.TheValue;
      }                               Target         Source
                                                      
      public static implicit operator LimitedInt (int x)     // int to LimitedInt
      {
         LimitedInt li = new LimitedInt();
         li.TheValue = x;
         return li;
      }

      private int _theValue = 0;
      public int TheValue{ ... }
   }

For example, the following code reiterates and uses the two type-conversion operators just defined. In Main, an int literal is converted into a LimitedInt object, and in the next line, a LimitedInt object is converted into an int.

   class LimitedInt
   {
      const int MaxValue = 100;
      const int MinValue = 0;

      public static implicit operator int(LimitedInt li)       // Convert type
      {
         return li.TheValue;
      }

      public static implicit operator LimitedInt(int x)        // Convert type
      {
         LimitedInt li = new LimitedInt();
         li.TheValue = x;
         return li;
      }

      private int _theValue = 0;
      public int TheValue                                      // Property
      {
         get { return _theValue; }
         set
         {
            if (value < MinValue)
               _theValue = 0;
            else
               _theValue = value > MaxValue
                              ? MaxValue
                              : value;
         }
      }
   }

   class Program
   {
      static void Main()                           // Main
      {
         LimitedInt li = 500;                      // Convert 500 to LimitedInt
         int value     = li;                       // Convert LimitedInt to int

         Console.WriteLine("li: {0}, value: {1}" , li.TheValue, value);
      }
   }

This code produces the following output:


li: 100, value: 100

Explicit Conversion and the Cast Operator

The preceding example code showed the implicit conversion of the int to a LimitedInt type and the implicit conversion of a LimitedInt type to an int. If, however, you had declared the two conversion operators as explicit, you would have had to explicitly use cast operators when making the conversions.

A cast operator consists of the name of the type to which you want to convert the expression, inside a set of parentheses. For example, in the following code, method Main casts the value 500 to a LimitedInt object.

                               Cast operator
                                           
               LimitedInt li = (LimitedInt) 500;

For example, here is the relevant portion of the code, with the changes marked:

In both versions of the code, the output is the following:


li: 100, value: 100

There are two other operators that take a value of one type and return a value of a different, specified type. These are the is operator and the as operator. These are covered at the end of Chapter 16.

Operator Overloading

The C# operators, as you’ve seen, are defined to work using the predefined types as operands. If confronted with a user-defined type, an operator simply would not know how to process it. Operator overloading allows you to define how the C# operators should operate on operands of your user-defined types.

  • Operator overloading is available only for classes and structs.
  • You can overload an operator x for use with your class or struct by declaring a method named operator x that implements the behavior (for example, operator +, operator -, and so on).
    • The overload methods for unary operators take a single parameter of the class or struct type.
    • The overload methods for binary operators take two parameters, at least one of which must be of the class or struct type.
         public static LimitedInt operator -(LimitedInt x)               // Unary
         public static LimitedInt operator +(LimitedInt x, double y)     // Binary

The declaration of an operator overload method requires the following:

  • The declaration must use both the static and public modifiers.
  • The operator must be a member of the class or struct for which it is an operator.

For example, the following code shows two of the overloaded operators of class LimitedInt: the addition operator and the negation operator. You can tell that it is negation, not subtraction, because the operator overload method has only a single parameter and is therefore unary, whereas the subtraction operator is binary.

   class LimitedInt Return
   {
           Required       Type    Keyword  Operator  Operand
                                                              
      public static LimitedInt operator + (LimitedInt x, double y)
      {
         LimitedInt li = new LimitedInt();
         li.TheValue = x.TheValue + (int)y;
         return li;
      }

      public static LimitedInt operator - (LimitedInt x)
      {
         // In this strange class, negating a value just sets it to 0.
         LimitedInt li = new LimitedInt();
         li.TheValue = 0;
         return li;
      }
      ...
  }

Restrictions on Operator Overloading

Not all operators can be overloaded, and there are restrictions on the types of overloading that can be done. The important things you should know about the restrictions on operator overloading are described later in the section.

Only the following operators can be overloaded. Prominently missing from the list is the assignment operator.

Overloadable unary operators: +, -, !, ~, ++, --, true, false

Overloadable binary operators: +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <=

The increment and decrement operators are overloadable. But unlike the predefined versions, there is no distinction between the pre- and post-usage of the overloaded operator.

You cannot do the following things with operator overloading:

  • Create a new operator
  • Change the syntax of an operator
  • Redefine how an operator works on the predefined types
  • Change the precedence or associativity of an operator

Image Note Your overloaded operators should conform to the intuitive meanings of the operators.

Example of Operator Overloading

The following example shows the overloads of three operators for class LimitedInt: negation, subtraction, and addition.

   class LimitedInt {
      const int MaxValue = 100;
      const int MinValue = 0;

      public static LimitedInt operator -(LimitedInt x)
      {
         // In this strange class, negating a value just sets its value to 0.
         LimitedInt li = new LimitedInt();
         li.TheValue = 0;
         return li;
      }

      public static LimitedInt operator -(LimitedInt x, LimitedInt y)
      {
         LimitedInt li = new LimitedInt();
         li.TheValue = x.TheValue - y.TheValue;
         return li;
      }

      public static LimitedInt operator +(LimitedInt x, double y)
      {
         LimitedInt li = new LimitedInt();
         li.TheValue = x.TheValue + (int)y;
         return li;
      }

      private int _theValue = 0;
      public int TheValue
      {
         get { return _theValue; }
         set
         {
            if (value < MinValue)
               _theValue = 0;
            else
               _theValue = value > MaxValue
                              ? MaxValue
                              : value;
         }
      }
   }
                                                                               
   class Program {
      static void Main() {
         LimitedInt li1 = new LimitedInt();
         LimitedInt li2 = new LimitedInt();
         LimitedInt li3 = new LimitedInt();
         li1.TheValue = 10; li2.TheValue = 26;
         Console.WriteLine(" li1: {0}, li2: {1}" , li1.TheValue, li2.TheValue);

         li3 = -li1;
         Console.WriteLine("-{0} = {1}" , li1.TheValue, li3.TheValue);

         li3 = li2 - li1;
         Console.WriteLine(" {0} - {1} = {2}" ,
                    li2.TheValue, li1.TheValue, li3.TheValue);

         li3 = li1 - li2;
         Console.WriteLine(" {0} - {1} = {2}" ,
                    li1.TheValue, li2.TheValue, li3.TheValue);
      }
   }

This code produces the following output:


li1: 10, li2: 26
-10 = 0
 26 - 10 = 16
 10 - 26 = 0

The typeof Operator

The typeof operator returns the System.Type object of any type given as its parameter. From this object, you can learn the characteristics of the type. (There is only one System.Type object for any given type.) You cannot overload the typeof operator. Table 8-17 lists the operator’s characteristics.

The typeof operator is unary.

Image

The following is an example of the syntax of the typeof operator. Type is a class in the System namespace.

   Type t = typeof ( SomeClass )

For example, the following code uses the typeof operator to get information on a class called SomeClass and to print the names of its public fields and methods.

   using System.Reflection;  // Use the Reflection namespace to take full advantage
                             // of determining information about a type.
   class SomeClass
   {
      public int  Field1;
      public int  Field2;

      public void Method1() { }
      public int  Method2() { return 1; }
   }

   class Program
   {
      static void Main()
      {
         Type t = typeof(SomeClass);
         FieldInfo[]  fi = t.GetFields();
         MethodInfo[] mi = t.GetMethods();

         foreach (FieldInfo f in fi)
            Console.WriteLine("Field : {0}" , f.Name);
         foreach (MethodInfo m in mi)
            Console.WriteLine("Method: {0}" , m.Name);
      }
   }

This code produces the following output:


Field : Field1
Field : Field2
Method: Method1
Method: Method2
Method: ToString
Method: Equals
Method: GetHashCode
Method: GetType

The typeof operator is also called by the GetType method, which is available for every object of every type. For example, the following code retrieves the name of the type of the object:

   class SomeClass
   {
   }

   class Program
   {
      static void Main()
      {
         SomeClass s = new SomeClass();

         Console.WriteLine("Type s: {0}" , s.GetType().Name);
      }
   }

This code produces the following output:


Type s: SomeClass

Other Operators

The operators covered in this chapter are the standard operators for the built-in types. There are other special-usage operators that are dealt with later in the book, along with their operand types. For example, the nullable types have a special operator called the null coalescing operator, which is described in Chapter 25 along with a more in-depth description of nullable types.

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

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