- Expressions
- Literals
- Order of Evaluation
- Simple Arithmetic Operators
- The Remainder Operator
- Relational and Equality Comparison Operators
- Increment and Decrement Operators
- Conditional Logical Operators
- Logical Operators
- Shift Operators
- Assignment Operators
- The Conditional Operator
- Unary Arithmetic Operators
- User-Defined Type Conversions
- Operator Overloading
- The typeof Operator
- Other Operators
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:
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 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
andfalse
.- For reference type variables, literal
null
means that the variable does not point to data in memory.
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.
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
.
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.
Note Real literals without a suffix are of type double
, not float
!
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.
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
Note The compiler saves memory by having identical string literals share the same memory location in the heap.
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.
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.
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.
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
The simple arithmetic operators perform the four basic arithmetic operations and are listed in Table 8-6. These operators are binary and left-associative.
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 (%
) 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.
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
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.
A binary expression with a relational or equality operator returns a value of type bool.
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
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 isfalse
, 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
andb
are the same, so a comparison would returntrue
.- 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 returnfalse
.
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.
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++
andy--
.
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.
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
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.
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
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.
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.
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
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
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.
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.
Figure 8-8 illustrates bitwise shift operations.
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
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.
The syntax is as follows:
VariableExpression Operator ExpressionFor 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 ofx
, which is now 10, is assigned toy
.- The assignment to
y
is the right operand for the assignment toz
—leaving all three variables with the value 10.
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
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 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.
The syntax for the conditional operator is shown below. It has a test expression and two result expressions.
Condition
must return a value of typebool
.- If
Condition
evaluates totrue
, thenExpression1
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.
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
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.
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.
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 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
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.
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 namedoperator
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
orstruct
type.- The overload methods for binary operators take two parameters, at least one of which must be of the
class
orstruct
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
andpublic
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;
}
...
}
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
Note Your overloaded operators should conform to the intuitive meanings of the operators.
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 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.
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
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.
3.147.74.211