Chapter 2. C# Language Reference

This chapter walks you through each aspect of the C# language. Many features of C# will be familiar if you have experience with a strongly typed object-oriented language.

Identifiers

Identifiers are names programmers choose for their types, methods, variables, and so on. An identifiermust be a whole word, essentially composed of Unicode characters starting with a letter or underscore. An identifier must not clash with a keyword. As a special case, the @ prefix can be used to avoid such a conflict, but the character isn’t considered part of the identifier that it precedes. For instance, the following two identifiers are equivalent:

Korn
@Korn

C# identifiers are case-sensitive, but for compatibility with other languages, you should not differentiate public or protected identifiers by case alone.

Types

A C# program is written by building new types and leveraging existing types, either those defined in the C# language itself or imported from other libraries. Each type contains a set of data and function members, which combine to form the modular units that are the key building blocks of a C# program.

Type Instances

Generally, you must create instances of a type to use that type. Those data members and function members that require a type to be instantiated are called instance members. Data members and function members that can be used on the type itself are called static members.

Example: Building and Using Types

In this program, we build our own type called Counter and another type called Test that uses instances of the Counter. The Counter type uses the predefined type int, and the Test type uses the static function member WriteLine of the Console class defined in the System namespace:

// Imports types from System namespace, such as Console
using System;
class Counter { // New types are typically classes or structs
  // --- Data members ---
  int value; // field of type int
  int scaleFactor; // field of type int

  // Constructor, used to initialize a type instance
  public Counter(int scaleFactor) { 
    this.scaleFactor = scaleFactor;  
  }
  // Method
  public void Inc(  ) {
    value+=scaleFactor;
  }
  // Property
  public int Count {
    get {return value; }
  }
}
class Test {
  // Execution begins here
  static void Main(  ) {

    // Create an instance of counter type
    Counter c = new Counter(5);
    c.Inc(  );
    c.Inc(  );
    Console.WriteLine(c.Count); // prints "10";

    // Create another instance of counter type
    Counter d = new Counter(7);
    d.Inc(  );
    Console.WriteLine(d.Count); // prints "7";
   }
}

Implicit and Explicit Conversions

Each type has its own set of rules defining how it can be converted to and from other types. Conversions between types may be implicit or explicit. Implicit conversions can be performed automatically, while explicit conversions require a cast usingthe C cast operator, ( ):

int x = 123456; // int is a 4-byte integer
long y = x; // implicit conversion to 8-byte integer
short z =(short)x; // explicit conversion to 2-byte integer

The rationale behind implicit conversions is that they are guaranteed to succeed and not lose information. Conversely, an explicitconversion is required when runtime circumstances determine whether the conversion succeeds or when information might be lost during the conversion.

Most conversion rules are supplied by the language, such as the previous numeric conversions. Occasionally it is useful for developers to define their own implicit and explicit conversions (see Section 2.4 later in this chapter).

Categories of Types

All C# types, including both predefined types and user-defined types, fall into one of three categories: value, reference, and pointer.

Value types

Value types typically represent basic types. Simple types, such as basic numeric types (int, long, bool, etc.) are structs, which are value types. You can expand the set of simple types by defining your own structs. In addition, C# allows you to define enums.

Reference types

Reference types typically represent more complex types with rich functionality. The most fundamental C# reference type is the class, but special functionality is provided by the array, delegate, and interface types.

Pointer types

Pointer types fall outside mainstream C# usage and are used only for explicit memory manipulation in unsafe blocks (see Section 2.17 later in this chapter).

Predefined Types

C# has two categories of predefined types:

  • Value types: integers, floating point numbers, decimal, char, bool

  • Reference types: object, string

Aliases for all these types can be found in the System namespace. For example, the following two statements are semantically identical:

int i = 5;
System.Int32 i = 5;

Integral types

The following table describes integral types:

C# type

System type

Size (bytes)

Signed?

sbyte
System.Sbyte

1

Yes

short
System.Int16

2

Yes

int
System.Int32

4

Yes

long
System.Int64

8

Yes

byte
System.Byte

1

No

ushort
System.UInt16

2

No

uint
System.UInt32

4

No

ulong
System.UInt64

8

No

sbyte, short, int, and long are signed integers; byte, ushort, uint, and ulong are unsigned integers.

For unsigned integers n bits wide, their possible values range from to 2n-1. For signed integers n bits wide, their possible values range from -2n-1 to 2n-1-1. Integer literals can use either decimal or hexadecimal notation:

int x = 5;
ulong y = 0x1234AF; // prefix with 0x for hexadecimal

When an integral literal is valid for several possible integral types, the chosen default type goes in this order:

int 
uint 
long 
ulong.

These suffixes may be appended to a value to explicitly specify its type:

U

uint or ulong

L

long or ulong

UL

ulong

Integral conversions

An implicit conversion between integral types is permitted when the resulting type contains every possible value of the type it is converted from. Otherwise, an explicit conversion is required. For instance, you can implicitly convert an int to a long, but must explicitly convert an int to a short:

int x = 123456;
long y = x; // implicit, no information lost
short z = (short)x; // explicit, truncates x

Floating-point types

C# type

System type

Size (bytes)

float
System.Single

4

double
System.Double

8

A float can hold values from approximately ±1.5 x 10-45 to approximately ±3.4 x 1038 with seven significant figures.

A double can hold values from approximately ±5.0 x 10-324 to approximately ±1.7 x 10308 with 15 to 16 significant figures.

Floating-point types can hold the special values +0, -0, +∞, -∞, or NaN (not a number) that represent the outcome of mathematical operations such as division by zero. float and double implement the specification of the IEEE 754 format types, supported by almost all processors, defined at http://www.ieee.org.

Floating-point literals can use decimal or exponential notation. A float literal requires the suffix “f” or “F”. A double literal may choose to add the suffix “d” or “D”.

float x = 9.81f;
double y = 7E-02; // 0.07
double z = 1e2;   // 100
Floating-point conversions

An implicit conversion from a float to a double loses no information and is permitted but not vice versa. An implicit conversion from an sbyte, short, int, or long (or one of their unsigned counterparts) to a float or double is allowed for readability:

short strength = 2;
int offset = 3;
float x = 9.53f * strength - offset;

If this example used larger values, precision might be lost. However, the possible range of values isn’t truncated because the lowest and highest possible values of a float or double exceed those of any integral type. All other conversions between integral and floating-point types must be explicit:

float x = 3.53f;
int offset = (int)x;

decimal type

C# type

System type

Size (bytes)

decimal
System.Decimal

16

The decimal type can hold values from ±1.0 x 10-28 to approximately ±7.9 x 1028 with 28 to 29 significant figures.

The decimal type holds 28 digits and the position of the decimal point on those digits. Unlike a floating-point value, it has more precision but a smaller range. It is typically useful in financial calculations, in which the combination of its high precision and the ability to store a base10 number without rounding errors is valuable. The number 0.1, for instance, is represented exactly with a decimal, but as a recurring binary number with a floating-point type. There is no concept of +0, -0, +∞, -∞, and NaN for a decimal.

A decimal literal requires the suffix “m” or “M”:

decimal x = 80603.454327m; // holds exact value
decimal conversions

An implicit conversion from all integral types to a decimal type is permitted because a decimal type can represent every possible integer value. A conversion from a decimal to floating-point type or vice versa requires an explicit conversion, since floating-point types have a bigger range than a decimal, and a decimal has more precision than a floating-point type.

char type

C# type

System type

Size (bytes)

char
System.Char

2

The char type represents a Unicode character.

A char literal consists of either a character, Unicode format, or escape character enclosed in single quote marks:

'A' // simple character
'u0041' // Unicode
'x0041' // unsigned short hexadecimal
'
' // escape sequence character

Table 2-1 summarizes the escape characters recognized by C#.

Table 2-1. Escape sequence characters

char

Meaning

Value

'

Single quote

0x0027
"

Double quote

0x0022
\

Backslash

0x005C

Null

0x0000
a

Alert

0x0007


Backspace

0x0008
f

Form feed

0x000C

New line

0x000A

Carriage return

0x000D
	

Horizontal tab

0x0009
v

Vertical tab

0x000B
char conversions

An implicit conversion from a char to most numeric types works; it’s simply dependent on whether the numeric type can accommodate an unsigned short. If it can’t, an explicit conversion is required.

bool type

C# type

System type

Size (bytes)

bool
System.Boolean

1/2

The bool type is a logical value, which can be assigned the literal true or false.

Although Boolean values require only 1 bit (0 or 1), they occupy 1 byte of storage since this is the minimum chunk that most processor architectures can allocate. Each element of an array requires 2 bytes of memory.

bool conversions

No conversions can be made from Booleans to numeric types or vice versa.

object type

C# type

System type

Size (bytes)

object
System.Object

0/ 8 overhead

The object type is the ultimate base type for both value types and reference types. Value types have no storage overhead from object. Reference types, which are stored on the heap, intrinsically require an overhead. In the .NET runtime, a reference-type instance has an 8-byte overhead, which stores the object’s type and temporary information such as its synchronization lock state or whether it has been fixed from movement by the garbage collector. Note that each reference to a reference-type instance uses 4 bytes of storage.

For more information on the System.Object type, see Section 3.1 in Chapter 3.

string type

C# type

System type

Size (bytes)

string
System.String

20 minimum

The C# string represents an immutable sequence of Unicode characters and aliases the System.String class (see Section 3.3 in Chapter 3).

Although string is a class, its use is so ubiquitous in programming that it is given special privileges by both the C# compiler and .NET runtime.

Unlike other classes, a new instance can be created with a string literal:

string a = "Heat";

Strings can also be created with verbatim string literals. Verbatim string literals start with a @, which indicates the string should be used verbatim, even if it spans multiple lines or includes escape characters, i.e., “” (see Table 2-1). In this example the pairs a1 and a2 represent the same string, and the pairs b1 and b2 represent the same string:

public void StringDemo(  ) {
  string a1 = "\\server\fileshare\helloworld.cs";
  string a2 = @"\serverfilesharehelloworld.cs";
  Console.WriteLine(a1==a2); // Prints "True"
  
  string b1 = "First Line
Second Line";
  string b2 = @"First Line
Second Line";
  Console.WriteLine(b1==b2); // Prints "True"
}

Types and Memory

The fundamental difference between value and reference types is how they are stored in memory.

Memory for value types

The memory of a value-type instance simply holds a raw value, like a number or character. Value types are stored either on the stack or inline. A stack is a block of memory that grows each time a method is entered (because its local variables need storage space) and shrinks each time a method exits (because its local variables are no longer needed). Inline just means that the value type is declared as part of a larger object, such as when it is a field in a class or a member of an array.

Memory for reference types

The memory of a reference-type instance holds the address of an object on the heap. A reference type may be null, which means no object is referenced. During a program’s execution, references are assigned to existing or new objects on the heap. An object on the heap remains in memory until the runtime’s garbage collector determines it is no longer referenced, at which time the garbage collector discards the object and releases its memory.

Value types and reference types side by side

To create a value-type or reference-type instance, the constructor for the type may be called with the new keyword. A value-type constructor simply initializes an object. A reference-type constructor creates a new object on the heap and then initializes the object:

// Reference-type declaration
class PointR {
  public int x, y;
}
// Value type declaration
struct PointV {
  public int x, y;
}
class Test {
  public static void Main(  ) {
    PointR a; // Local reference-type variable, uses 4 bytes of
              // memory on the stack to hold address
    PointV b; // Local value-type variable, uses 8 bytes of
              // memory on the stack for x and y
    a = new PointR(  ); // Assigns the reference to address of new
                      // instance of PointR allocated on the
                      // heap. The object on the heap uses 8
                      // bytes of memory for x and y and an
                      // additional 8 bytes for core object
                      // requirements, such as storing the 
                      // object's type & synchronization state
    b = new PointV(  ); // Calls the value type's default
                      // constructor.  The default constructor 
                      // for both PointR and PointV will set 
                      // each field to its default value, which 
                      // will be 0 for both x and y.
    a.x = 7;
    b.x = 7;
  }
}
// At the end of the method the local variables a and b go out of
// scope, but the new instance of a PointR remains in memory until
// the garbage collector determines it is no longer referenced

Assignment to a reference type copies an object reference; assignment to a value type copies an object value:

    ...
    PointR c = a;
    PointV d = b;
    c.x = 9;
    d.x = 9;
    Console.WriteLine(a.x); // Prints 9
    Console.WriteLine(b.x); // Prints 7
  }
}

As with this example, an object on the heap can be pointed at by many variables, whereas an object on the stack or inline can be accessed only via the variable it was declared with.

Unified Type System

C# provides a unified type system, whereby the object class is the ultimate base type for both reference and value types. This means that all types, apart from the occasionally used pointer types, share a basic set of characteristics.

Simple types are value types

In most languages, there is a strict distinction between simple types (int, float, etc.) and user-defined types (Rectangle, Button, etc.). In C#, simple types actually alias structs found in the System namespace. For instance, the int type aliases the System.Int32 struct, and the long type aliases the System.Int64struct, etc. This means simple types have the same features you expect any user-defined type to have. For instance, you can call a method on an int:

int i = 3;
string s = i.ToString(  );

This is equivalent to:

// This is an explanatory version of System.Int32
namespace System {
  struct Int32 {
    ...
    public string ToString(  ) {
      return ...;
    }
  }
}
// This is valid code, but we recommend you use the int alias
System.Int32 i = 5;
string s = i.ToString(  );

Value types expand the set of simple types

Creating an array of 1,000 ints is highly efficient. This line allocates 1,000 ints in one contiguous block of memory:

int[] iarr = new int [1000];

Similarly, creating an array of a value type PointV is also very efficient:

struct PointV {
  public int x, y;
}
PointV[] pvarr = new PointV[1000];

Had you used a reference type PointR, you would have needed to instantiate 1,000 individual points after instantiating the array:

class PointR {
   public int x, y;
}
PointR[] prarr = new PointR[1000];
for( int i=0; i<prarr.Length; i++ )
   prarr[i] = new PointR(  );

In Java, only the simple types (int, float, etc.) can be treated with this efficiency, while in C# you can expand the set of simple types by declaring a struct.

Furthermore, C#’s operators may be overloaded so that operations such as + and -, which are typically applicable to simple types, can also be applied to any class or struct (see Section 2.4 later in this chapter).

Boxing and unboxing value types

In C#, multiple reference types can share the characteristics of a common base type or interface, which allows reference types to be treated generically. This is very powerful. For instance, a method that takes a reference type R for a parameter works for any type that derives from R or implements R (see Section 2.7.2 later in this chapter).

To perform common operations on both reference and value types, each value type has a corresponding hidden reference type, which is automatically created when it is cast to a reference type. This process is called boxing.

In the following example, the Queue class can enqueue and dequeue any object (object is a reference type that is the base type of all types). You can put an int (a value type) in a queue, because an int can be boxed and unboxed to and from its corresponding reference type:

class Queue {
  ...
  void Enqueue(object o) {...}
  object Dequeue(  ) {return ...}
}

Queue q = new Queue(  );
q.Enqueue(9); // box the int
int i = (int)q.Dequeue(  ); // unbox the int

When a value type is boxed, a new reference type is created to hold a copy of the value type. Unboxing copies the value from the reference type back into a value type. Unboxing requires an explicit cast, and a check is made to ensure the specified value type matches the type contained in the reference type. An InvalidCastException is thrown if the check fails.

Tip

Function members of a value type don’t actually override virtual function members of the class object or an implemented interface. However, a boxed value type overrides virtual function members.

Variables

A variable represents a typed storage location. A variable can be a local variable, a parameter, an array element (see Section 2.11 later in this chapter), an instance field, or a static field (see Section 2.9.2 later in this chapter).

Every variable has an associated type, which essentially defines the possible values the variable can have and the operations that can be performed on that variable. C# is strongly typed, which means the set of operations that can be performed on a type is enforced at compile time, rather than at runtime. In addition, C# is type-safe, which, with the help of runtime checking, ensures that a variable can be operated only via the correct type (except in unsafe blocks; see Section 2.17.2 later in this chapter).

Definite Assignment

Variables in C# (except in unsafe contexts) must be assigned a value before they are used. A variable is either explicitly assigned a value or automatically assigned a default value. Automatic assignment occurs for static fields, class instance fields, and array elements not explicitly assigned a value. For example:

using System;
class Test {
  int v;
  // Constructors that initialize an instance of a Test
  public Test(  ) {} // v will be automatically assigned to 0
  public Test(int a) { // explicitly assign v a value
     v = a;
  }
  static void Main(  ) {
    Test[] iarr = new Test [2]; // declare array
    Console.WriteLine(iarr[1]); // ok, elements assigned to null
    Test t;
    Console.WriteLine(t); // error, t not assigned
  }
}

Default Values

The following table shows that, essentially, the default value for all primitive (or atomic) types is zero:

Type

Default value

Numeric

0

Bool

false

Char

''

Enum

0

Reference

null

The default value for each field in a complex (or composite) type is one of these aforementioned values.

Expressions and Operators

An expression is a sequence of operators and operands that specifies a computation. C# has unary operators, binary operators, and one ternary operator. Complex expressions can be built because an operand may itself be an expression, such as the operand (1 + 2) in the following example:

((1 + 2) / 3)

Operator Precedence

When an expression contains multiple operators, the precedence of the operators controls the order in which the individual operators are evaluated. When the operators are of the same precedence, their associativity determines their order of evaluation. Binary operators (except for assignment operators) are left-associative and are evaluated from left to right. The assignment operators, unary operators, and the conditional operator are right-associative, evaluated from right to left.

For example:

1 + 2 + 3 * 4

is evaluated as:

((1 + 2) + (3 * 4))

because * has a higher precedence than +, and + is a left-associative binary operator. You can insert parentheses to change the default order of evaluation. C# also overloads operators, which means the same operator symbols can have different meanings in different contexts (e.g., primary, unary, etc.) or different meanings for different types.

Table 2-2 lists C#’s operators in order of precedence. Operators in the same box have the same precedence, and operators in italic may be overloaded for custom types (see Section 2.9.8 later in this chapter).

Table 2-2. Operator Precedence Table

Category

Operators

Primary

Grouping:(x)
Member access: x.y
Struct pointer member access: ->
Method call: f(x)
Indexing: a[x]
Post increment: x++
Post decrement:x--
Constructor call: new
Array stack allocation: stackalloc
Type retrieval: typeof
Struct size retrieval: sizeof
Arithmetic check on: checked
Arithmetic check off: unchecked

Unary

Positive value of (passive):
+Negative value of:
-Not:!
Bitwise complement: ~
Pre increment: ++x
Pre decrement:-- x
Type cast: (T)x
Value at address: *
Address of value: &

Multiplicative

Multiply: *
Divide: /
Division remainder: %

Additive

Add: +
Subtract: -

Shift

Shift bits left: <<
Shift bits right:>>

Relational

Less than: <
Greater than: >
Less than or equal to:<=
Greater than or equal to: >=
Type equality/compatibility: is
Conditional type conversion: as

Equality

Equals: ==
Not equals: !=

Logical bitwise

And: &

 

Exclusive or: ^

 

Or: |

Logical Boolean

And: &&

 

Or: ||

 

Ternary conditional: ?: e.g. int x = a > b ? 2 : 7; is equivalent to :int x; if (a > b) x = 2; else x = 7;

Assignment

Assign/modify: = *= /= %= += -= <<= >>= &= ^= |=

Arithmetic Overflow Check Operators

Checked/unchecked operator syntax:

checked (expression)
unchecked (expression)

Checked/unchecked statement syntax:

checked statement-or-statement-block
unchecked statement-or-statement-block

The checked operator tells the runtime to generate an OverflowException if an integral expression exceeds the arithmetic limits of that type. The checked operator affects expressions with the ++, --, (unary)-, +, -, *, /, and explicit conversion operators ( ) between integral types (see Section 2.2.5.1 later in this chapter). Here’s an example:

int a = 1000000;
int b = 1000000;

// Check an expression
int c = checked(a*b);

// Check every expression in a statement block
checked {
   ...
   c = a * b;
   ...
}

The checked operator applies only to runtime expressions, because constant expressions are checked during compilation (though this can be turned off with the /checked [+|-] command-line compiler switch). The unchecked operator disables arithmetic checking at compile time and is seldom useful, but it can enable expressions such as this to compile:

const int signedBit = unchecked((int)0x80000000);

Statements

Execution of a C# program is specified by a series of statements that execute sequentially in the textual order in which they appear. All statements in a procedural-based language such as C# are executed for their effect. The two most basic kinds of statement in C# are the declaration and expression statements. C# also provides flow control statements for selection, looping, and jumping. Finally, C# provides statements for special purposes, such as locking memory or handling exceptions.

So that multiple statements can be grouped together, zero or more statements may be enclosed in braces ({ }) to form a statement block. A statement block can be used anywhere a single statement is valid.

Expression Statements

Syntax:

[variable =]? expression;

An expression statement evaluates an expression either by assigning its result to a variable or generating side effects, (i.e., invocation, new, ++, or --). An expression statement ends in a semicolon (;). For example:

x = 5 + 6; // assign result
x++; // side effect
y = Math.Min(x, 20); // side effect and assign result
Math.Min (x, y); // discards result, but ok, there is a side effect
x == y; // error, has no side effect, and does not assign result

Declaration Statements

Variable declaration syntax:

type [variable [ = expression ]?]+ ;

Constant declaration syntax:

const type [variable = constant-expression]+ ;

A declaration statement declares a new variable. You can initialize a variable at the time of its declaration by optionally assigning it the result of an expression.

The scope of a local or constant variable extends to the end of the current block. You can’t declare another local variable with the same name in the current block or in any nested blocks. For example:

bool a = true;
while(a) {
   int x = 5;
   if (x==5) {
      int y = 7;
      int x = 2; // error, x already defined
   }
   Console.WriteLine(y); // error, y is out of scope
}

A constant declaration is like a variable declaration, except that the value of the variable can’t be changed after it has been declared:

const double speedOfLight = 2.99792458E08;
speedOfLight+=10; // error

Empty Statements

Syntax:

;

The empty statement does nothing. It is used as a placeholder when no operations need to be performed, but a statement is nevertheless required. For example:

while(!thread.IsAlive);

Selection Statements

C# has many ways to conditionally control the flow of program execution. This section covers the simplest two constructs, the if-else statement and the switch statement. In addition, C# also provides a conditional operator and loop statements that conditionally execute based on a Boolean expression. Finally, C# provides object-oriented ways of conditionally controlling the flow of execution, namely virtual method invocations and delegate invocations.

if-else statement

Syntax:

if (Boolean-expression)
  statement-or-statement-block
[ else
  statement-or-statement-block ]?

An if-else statement executes code depending on whether a Boolean expression is true. Unlike C, C# permits only a Boolean expression. For example:

int Compare(int a, int b) {
   if (a>b)
      return 1;
   else if (a<b)
      return -1;
   return 0;
}

switch statement

Syntax:

switch (expression) {
[ case constant-expression : statement* ]*
[ default : statement* ]?
}

switch statements let you branch program execution based on the value of a variable. switch statements can result in cleaner code than if you use multiple if statements, because the controlling expression is evaluated only once. For instance:

void Award(int x) {
  switch(x) {
    case 1:
      Console.WriteLine("Winner!");
      break;
    case 2:
      Console.WriteLine("Runner-up");
      break;
    case 3:
    case 4:
      Console.WriteLine("Highly commended");
      break;
    default:
      Console.WriteLine("Don't quit your day job!");
      break;
  }
}

The switch statement can evaluate only a predefined type (including the string type) or enum, though user-defined types may provide an implicit conversion to these types.

After a particular case statement is executed, control doesn’t automatically continue to the next statement or break out of the switch statement. Instead, you must explicitly control execution, typically by ending each case statement with a jump statement. The options are:

  • Use the break statement to jump to the end of the switch statement (this is by far the most common option).

  • Use the goto case <constantexpression> or goto default statements to jump to either another case statement or the default case statement.

  • Use any other jump statement, namely return, throw, continue, or goto label.

Unlike in Java and C++, the end of a case statement must explicitly state where to go next. There is no error-prone “default fall through” behavior, so not specifying a jump statement results in the next case statement being executed:

void Greet(string title) {
  switch (title) {
    case null:
      Console.WriteLine("And you are?");
      goto default;
    case "King":
      Console.WriteLine("Greetings your highness");
      // error, should specify break, otherwise...
    default :
      Console.WriteLine("How's it hanging?");
      break;
  }
}

Loop Statements

C# enables a group of statements to be executed repeatedly using the while, do while, and for statements.

while loops

Syntax:

while (Boolean-expression)
  statement-or-statement-block

while loops repeatedly execute a statement block while a given Boolean expression remains true. The expression is tested before the statement block is executed:

int i = 0;
while (i<5) {
  i++;
}

do-while loops

Syntax:

do 
  statement-or-statement-block
while (Boolean-expression);

do-while loops differ functionally from while loops only in that they test the controlling Boolean expression after the statement block has executed. Here’s an example:

int i = 0;
do
  i++;
while (i<5);

for loops

Syntax:

for (statement?; Boolean-expression?; statement?)
  statement-or-statement-block

for loops can be more convenient than while loops when you need to maintain an iterator value. As in Java and C, for loops contain three parts. The first part is a statement executed before the loop begins, and by convention, it initializes an iterator variable. The second part is a Boolean expression that, while true, permits the statement block to execute. The third part is a statement that is executed after each iteration of the statement block and, by convention, increments an iterator variable. Here’s an example:

for (int i=0; i<10; i++)
  Console.WriteLine(i);

Any of the three parts of the for statement may be omitted. You can implement an infinite loop like this, though alternatively a while (true) statement has the same result:

for (;;)
  Console.WriteLine("Hell ain't so bad");

foreach loops

Syntax:

foreach ( type-value in IEnumerable )
  statement-or-statement-block

It’s common to use for loops to iterate over a collection, so C#, like Visual Basic, includes a foreach statement. For instance, instead of doing this:

for (int i=0; i<dynamite.Length; i++)
  Console.WriteLine(dynamite [i]);

you can do this:

foreach (Stick stick in dynamite)
  Console.WriteLine(stick);

The foreach statement works on any collection (including arrays). Although not strictly necessary, all collections leverage this functionality by implementing the IEnumerable and IEnumerator interfaces, which are explained in Section 3.4 in Chapter 3. Here is an equivalent way to iterate over the collection:

IEnumerator ie = dynamite.GetEnumerator(  );
while (ie.MoveNext(  )) {
  Stick stick = (Stick)ie.Current;
  Console.WriteLine(stick);
}

jump Statements

The C# jump statements are: break, continue, goto, return, and throw. All jump statements obey sensible restrictions imposed by try statements (see Section 2.15 later in this chapter). First, a jump out of a try block always executes the try’s finally block before reaching the target of the jump. Second, a jump can’t be made from the inside to the outside of a finally block.

break statement

Syntax:

break;

The break statement transfers execution from the enclosing while loop, for loop, or switch statement block to the next statement block:

int x = 0;
while (true) {
  x++;
  if (x>5)
    break; // break from the loop
}

continue statement

Syntax:

continue;

The continue statement forgoes the remaining statements in the loop and makes an early start on the next iteration:

int x = 0;
int y = 0;
while (y<100) {
  x++;
  if ((x%7)==0)
    continue; // continue with next iteration
  y++;
}

goto statement

Syntax:

goto statement-label;
goto case-constant;

The goto statement transfers execution to another label within the statement block. A label statement is just a placeholder in a method:

int x = 4;
start:
x++;
if (x==5)
 goto start;

The goto case statement transfers execution to another case label in a switch block (as explained in Section 2.5.4.2).

return statement

Syntax:

return expression?;

The return statement exits a method. If the method is non-void, it must return an expression of the method’s return type:

int CalcX(int a) {
  int x = a * 100;
  return x; // return to the calling method with value
}

throw statement

Syntax:

throw exception-expression?;

The throw statement throws an Exceptionto indicate that an abnormal condition has occurred (see Section 2.15 later in this chapter):

if (w==null)
  throw new Exception("w can't be null");

lock statement

Syntax:

lock (expression)
  statement-or-statement-block

The lock statement is actually a syntactic shortcut for calling the Enter and Exit methods of the Monitor class (see Section 3.8 in Chapter 3).

using statement

Syntax:

using (declaration-expression)
   statement-or-statement-block

Many classes encapsulate non-memory resources, such as file handles, graphics handles, or database connections. These classes implement System.IDisposable, which defines a single parameterless method named Dispose that is called to clean up these resources. The using statement provides an elegant syntax for declaring, then calling, the Dispose method of variables that implement IDisposable. For example:

using (FileStream fs = new FileStream (fileName, FileMode.Open)) {
  // do something with fs
}

This is precisely equivalent to:

FileStream fs = new FileStream (fileName, FileMode.Open);
try {
  // do something with fs
} finally {
  if (fs != null)
  ((IDisposable)fs).Dispose(  );
}

Organizing Types

A C# program is basically a group of types. These types are defined in files, organized by namespaces, compiled into modules, and then grouped into an assembly.

Generally, these organizational units overlap: an assembly can contain many namespaces, and a namespace can be spread across several assemblies. A module can be part of many assemblies, and an assembly can contain many modules. A source file can contain many namespaces, and a namespace can span many source files. For more information, see Section 3.9 in Chapter 3.

Files

File organization is of almost no significance to the C# compiler: an entire project can be merged into a single .cs file and still compile successfully (preprocessor statements are the only exception to this). However, it’s generally tidy to have one type in one file, with a filename that matches the name of the class and a directory name that matches the name of the class’s namespace.

Namespaces

Namespace declaration syntax:

namespace name+a {
  using-statement*
  [namespace-declaration | type-declaration]*b
}

a Dot-delimited.

b No delimiters.

A namespace enables you to group related types into a hierarchical categorization. Generally the first name in a namespace name is the name of your organization, followed by names that group types with finer granularity. For example:

namespace MyCompany.MyProduct.Drawing {
  class Point {int x, y, z;}
  delegate void PointInvoker(Point p);
}

Nesting namespaces

You may also nest namespace declarations instead of using dots. This example is semantically identical to the previous example:

namespace MyCompany {
  namespace MyProduct {
    namespace Drawing {
      class Point {int x, y, z;}
      delegate void PointInvoker(Point p);
    }
  }
}

Using a type with its fully qualified name

The complete name of a type includes its namespace name. To use the Point class from another namespace, you may refer to it with its fully qualified name:

namespace TestProject {
  class Test {
    static void Main(  ) {
      MyCompany.MyProduct.Drawing.Point x;
    }
  }
}

using keyword

The using keyword is a convenient way to avoid using the fully qualified names of types in other namespaces. This example is semantically identical to the previous example:

namespace TestProject {
  using MyCompany.MyProduct.Drawing;
  class Test {
    static void Main(  ) {
      Point x;
    }
  }
}

Aliasing types and namespaces

Type names must be unique within a namespace. To avoid naming conflicts without having to use fully qualified names, C# allows you to specify an alias for a type or namespace. Here is an example:

using sys = System;        // Namespace alias
using txt = System.String; // Type alias
class Test {
  static void Main(  ) {
    txt s = "Hello, World!";
    sys.Console.WriteLine(s); // Hello, World!
    sys.Console.WriteLine(s.GetType(  )); // System.String
  }
}

Global namespace

The outermost level within which all namespaces and types are implicitly declared is called the global namespace. When a type isn’t explicitly declared within a namespace, it can be used without qualification from any other namespace, since it is a member of the global namespace. However, it is always good practice to organize types within logical namespaces

Inheritance

A C# class can inherit from another class to extend or customize that class. A class can inherit only from a single class but can be inherited by many classes, thus forming a class hierarchy. At the root of any class hierarchy is the object class, which all objects implicitly inherit from. Inheriting from a class requires specifying the class to inherit from in the class declaration, using the C++ colon notation:

class Location { // Implicitly inherits from object
  string name;

  // The constructor that initializes Location
  public Location(string name) {
    this.name = name;
  }
  public string Name {get {return name;}}
  public void Display(  ) {
    Console.WriteLine(Name);
  }
}
class URL : Location { // Inherit from Location
  public void Navigate(  ) {
    Console.WriteLine("Navigating to "+Name);
  }
  // The constructor for URL, which calls Location's constructor
  public URL(string name) : base(name) {}
}

URL has all the members of Location and a new member, Navigate:

class Test {
  public static void Main(  ) {
    URL u = new URL("http://microsoft.com");
    u.Display(  );
    u.Navigate(  );
  }
}

Tip

The specialized class and general class are referred to as either the derived class and base class or the subclass and superclass.

Class Conversions

A class D may be implicitly upcast to the class B it derives from, and a class B may be explicitly downcast to a class D that derives from it. For instance:

URL u = new URL("http://microsoft.com");
Location l = u; // upcast
u = (URL)l; // downcast

If the downcast fails, an InvalidCastException is thrown.

as operator

The as operator allows a downcast that evaluates to null to be made if the downcast fails:

u = l as URL;

is operator

The is operator can test if an objectis or derives from a specified class (or implements an interface). It is often used to perform a test before a downcast:

if (l is URL)
  ((URL)l).Navigate(  );

Polymorphism

Polymorphism is the ability to perform the same operation on many types, as long as each type shares a common subset of characteristics. C# custom types exhibit polymorphism by inheriting classes and implementing interfaces (see Section 2.10 later in this chapter).

In the following example, the Show method can perform the operation Display on both a URL and a LocalFile because both types inherit the characteristics of Location:

class LocalFile : Location {
  public void Execute(  ) {
    Console.WriteLine("Executing "+Name);
  }
  // The constructor for LocalFile, which calls Location's constructor
  public LocalFile(string name) : base(name) {}
}
class Test {
  static void Main(  ) {
    URL u = new URL("http://www.microsoft.com");
    LocalFile l = new LocalFile( "C:\LOCAL\README.TXT");
    Show(u);
    Show(l);
  }
  public static void Show(Location loc) {
    Console.Write("Location is: ");
    loc.Display(  );
  }
}

Virtual Function Members

A key aspect of polymorphism is that each type can implement a shared characteristic in its own way. One way to permit such flexibility is for a base class to declare function members as virtual. Derived classes can provide their own implementations for any function members marked virtual in the base class (see Section 2.10 later in this chapter):

class Location {
  public virtual void Display(  ) {
    Console.WriteLine(Name);
  }
    ...
}
class URL : Location {
  // chop off the http:// at the start
  public override void Display(  ) {
    Console.WriteLine(Name.Substring(7));
  }
  ...
}

URL now has a custom way of displaying itself. The Show method of the Test class in the previous section will now call the new implementation of Display. The signatures of the overridden method and the virtual method must be identical, but unlike Java and C++, the override keyword is also required.

Abstract Classes and Members

A class can be declared abstract. An abstract class may have abstractmembers, which are function members without implementation that are implicitly virtual. In earlier examples, we specified a Navigate method for the URL type and anExecute method for the LocalFile type. You can, instead, declare Location an abstract class with an abstract method called Launch:

abstract class Location {
  public abstract void Launch(  );
}
class URL : Location {
  public override void Launch(  ) {
    Console.WriteLine("Run Internet Explorer...");
  }
}
class LocalFile : Location {
  public override void Launch(  ) {
    Console.WriteLine("Run Win32 Program...");
  }
}

A derived class must override all its inherited abstract members or must itself be declared abstract. An abstract class can’t be instantiated. For instance, if LocalFile doesn’t override Launch, LocalFile itself must be declared abstract, perhaps to allow Shortcut and PhysicalFile to derive from it.

Sealed Methods and Sealed Classes

An overridden function member may seal its implementation so it cannot be overridden. In our previous example, we could have made the URL’s implementation of Display sealed. This prevents a class that derives from URL from overriding Display, which provides a guarantee on the behavior of a URL.

public sealed override void Display(  ) {...}

A class can prevent other classes from inheriting from it by specifying the sealed modifier in the class declaration:

sealed class Math {
  ...
}

The most common scenario for sealing a class is when that class comprises only static members, as is the case with the Math class of the FCL. Another effect of sealing a class is that it enables the compiler to seal all virtual method invocations made on that class into faster, nonvirtual method invocations.

Hiding Inherited Members

Aside from its use for calling a constructor, the new keyword can also hide the data members, function members, and type members of a base class. Overriding a virtual method with the new keyword hides, rather than overrides, the base class implementation of the method:

class B {
  public virtual void Foo(  ) {
    Console.WriteLine("In B.");
  }
}
class D : B {
  public override void Foo(  ) {
    Console.WriteLine("In D.");
  }
}
class N : D {
  public new void Foo(  ) {  // hides D's Foo
    Console.WriteLine("In N.");
  }
}
public static void Main(  ) {
  N n = new N(  );
  n.Foo(  ); // calls N's Foo
  ((D)n).Foo(  ); // calls D's Foo
  ((B)n).Foo(  ); // calls D's Foo
}

A method declaration with the same signature as its base class should explicitly state whether it overrides or hides the inherited member.

Versioning Virtual Function Members

In C#, a method is compiled with a flag that is true if the method overrides a virtual method. This flag is important for versioning. Suppose you write a class that derives from a base class in the .NET Framework and then deploy your application to a client computer. The client later upgrades the .NET Framework, and the .NET base class now contains a virtual method that happens to match the signature of one of your methods in the derived class:

public class Base { // written by the library people
  public virtual void Foo(  ) {...} // added in latest update
}
public class Derived : Base { // written by you
  public void Foo(  ) {...} // not intended as an override
}

In most object-oriented languages, such as Java, methods are not compiled with this flag, so a derived class’s method with the same signature is assumed to override the base class’s virtual method. This means a virtual call is made to type Derived’s Foo method, even though Derived’s Foo is unlikely to have been implemented according to the specification intended by the author of type Base. This can easily break your application. In C#, the flag for Derived’s Foo will be false, so the runtime knows to treat Derived’s Foo as new, which ensures that your application will function as it was originally intended. When you get the chance to recompile with the latest framework, you can add the new modifier to Foo, or perhaps rename Foo something else.

Access Modifiers

To promote encapsulation, a type or type member may hide itself from other types or other assemblies by adding one of the following five access modifiers to the declaration:

public

The type or type member is fully accessible. This is the implicit accessibility for enum members (see Section 2.12 later in this chapter) and interface members (see Section 2.10 later in this chapter).

internal

The type or type member in assembly A is accessible only from within A. This is the default accessibility for nonnested types, so it may be omitted.

private

The type member in type T is accessible only from within T. This is the default accessibility for class and struct members, so it may be omitted.

protected

The type member in class C is accessible only from within C or from within a class that derives from C.

protected internal

The type member in class C and assembly A is accessible only from within C, from within a class that derives from C, or from within A. Note that C# has no concept of protected and internal, in which a type member in class C and assembly A is accessible only from within C or from within a class that derives from C and is within A.

Note that a type member may be a nested type. Here is an example that uses access modifiers:

// Assembly1.dll
using System;
public class A {
  private int x=5;
  public void Foo(  ) {Console.WriteLine (x);}
  protected static void Goo(  ) {}
  protected internal class NestedType {}
}
internal class B {
  private void Hoo (  ) {
    A a1 = new A (  ); // ok
    Console.WriteLine(a1.x); // error, A.x is private
    A.NestedType n; // ok, A.NestedType is internal
    A.Goo(  ); // error, A's Goo is protected
  }
}

// Assembly2.exe (references Assembly1.dll)
using System;
class C : A { // C defaults to internal
  static void Main(  ) { // Main defaults to private
    A a1 = new A(  ); // ok
    a1.Foo(  ); // ok
    C.Goo(  ); // ok, inherits A's protected static member
    new A.NestedType(  ); // ok, A.NestedType is protected
    new B(  ); // error, Assembly 1's B is internal
    Console.WriteLine(x); // error, A's x is private
  }
}

Restrictions on Access Modifiers

A type or type member can’t declare itself to be more accessible than any of the types it uses in its declaration. For instance, a class can’t be public if it derives from an internal class, or a method can’t be protected if the type of one of its parameters is internal to the assembly. The rationale behind this restriction is that whatever is accessible to another type is actually usable by that type.

In addition, access modifiers can’t be used when they conflict with the purpose of inheritance modifiers. For example, a virtual (or abstract) member can’t be declared private, since it would then be impossible to override. Similarly, a sealed class can’t define new protected members, since there is no class that can benefit from this accessibility.

Finally, to maintain the contract of a base class, a function member with the override modifier must have the same accessibility as the virtual member it overrides.

Classes and Structs

Class declaration syntax:

attributes? unsafe? access-modifier?
new?
[ abstract | sealed ]?
class class-name 
[: base-class | : interface+ | : base-class, interface+ ]?
{ class-members }

Struct declaration syntax:

attributes? unsafe? access-modifier?
new?
struct struct-name [: interface+]?
{ struct-members }

A class or struct combines data, functions, and nested types into a new type, which is a key building block of C# applications. The body of a class or struct is comprised of three kinds of members: data, function, and type.

Data members

Includes fields, constants, and events. The most common data members are fields. Events are a special case, since they combine data and functionality in the class or struct (see Section 2.14 later in this chapter).

Function members

Includes methods, properties, indexers, operators, constructors, and destructors. Note that all function members are either specialized types of methods or are implemented with one or more specialized types of methods.

Type members

Includes nested types. Types can be nested to control their accessibility (see Section 2.8 later in this chapter).

Here’s an example:

class ExampleClass {
   int x; // data member
   void Foo(  ) {} // function member
   struct MyNestedType  {} // type member
}

Differences Between Classes and Structs

Classes differ from structs in the following ways:

  • A class is a reference type; a struct is a value type. Consequently, structs are typically simple types in which value semantics are desirable (e.g., assignment copies a value rather than a reference).

  • A class fully supports inheritance (see Section 2.7 earlier in this chapter). A struct inherits from object and is implicitly sealed. Both classes and structs can implement interfaces.

  • A class can have a destructor; a struct can’t.

  • A class can define a custom parameterless constructor and initialize instance fields; a struct can’t. The default parameterless constructor for a struct initializes each field with a default value (effectively zero). If a struct declares a constructor(s), all its fields must be assigned in that constructor call.

Instance and Static Members

Data members and function members may be either instance (default) or static members. Instance members are associated with an instance of a type, whereas static members are associated with the type itself. Furthermore, invocation of static members from outside their enclosing type requires specifying the type name. In this example, the instance method PrintName prints the name of a particular Panda, while the static method PrintSpeciesName prints the name shared by all Pandas in the application (AppDomain):

using System;
class Panda {
  string name;
  static string speciesName = "Ailuropoda melanoleuca";
  // Initializes Panda (see the later section "Instance Constructors")
  public Panda(string name) {
    this.name = name;
  }
  public void PrintName(  ) {
    Console.WriteLine(name);
  }
  public static void PrintSpeciesName(  ) {
    Console.WriteLine(speciesName);
  }
}
class Test {
  static void Main(  ) {
    Panda.PrintSpeciesName(  ); // invoke static method
    Panda p = new Panda("Petey");
    p.PrintName(  ); // invoke instance method
  }
}

Fields

Syntax:

attributes? unsafe? access-modifier?
new?
static?
[readonly | volatile]?
type [ field-name [ = expression]? ]+ ;

Fields hold data for a class or struct. Fields are also referred to as member variables:

class MyClass {
  int x;
  float y = 1, z = 2;
  static readonly int MaxSize = 10;
  ...
}

As the name suggests, the readonly modifier ensures a field can’t be modified after it’s assigned. Such a field is termed a read-only field. Assignment to a read-only field is always evaluated at runtime, not at compile time. To compile, a read-only field’s assignment must occur in its declaration or within the type’s constructor (see Section 2.9.9 later in this chapter). Both read-only and non-read-only fields generate a warning when left unassigned.

The volatile modifier indicates that a field’s value may be modified in a multi-threaded scenario, and that neither the compiler nor the runtime should perform optimizations with that field.

Constants

Syntax:

attributes? access-modifier?
new?
const type [ constant-name = constant-expression ]+;

The type must be one of the following predefined types: sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimal, bool, char, string, or enum.

A constant is a field that is evaluated at compile time and is implicitly static. The logical consequence of this is that a constant can’t defer evaluation to a method or constructor and can only be one of a few built-in types (see the preceding syntax definition).

public const double PI = 3.14159265358979323846;

The benefit of a constant is that since it is evaluated at compile time, the compiler can perform additional optimization. For instance:

public static double Circumference(double radius) {
  return 2 * Math.PI * radius;
}

evaluates to:

public static double Circumference(double radius) {
  return 6.28318530717959 * radius;
}

Versioning with constants

A readonly field isn’t optimized by the compiler but is more versionable. For instance, suppose there is a mistake with PI, and Microsoft releases a patch to their library that contains the Math class, which is deployed to each client computer. If software using the Circumference method is already deployed on a client machine, the mistake isn’t fixed until you recompile your application with the latest version of the Math class. With a readonly field, however, this mistake is automatically fixed the next time the client application is executed. Generally this scenario occurs when a field value changes not as a result of a mistake, but simply because of an upgrade, such as a change in the value of the MaxThreads constant from 500 to 1,000.

Properties

Syntax:

attributes? unsafe? access-modifier?
[
  [[sealed | abstract]? override] |
  new? [virtual | abstract | static]?
]?
type property-name { [ 
attributes? get statement-block |  // read-only
attributes? set statement-block |  // write-only 
attributes? get statement-block    // read-write 
attributes? set statement-block
] }

abstract accessors don’t specify an implementation, so they replace a get/set block with a semicolon. Also see Section 2.8.1 earlier in this chapter.

A property can be characterized as an object-oriented field. Properties promote encapsulation by allowing a class or struct to control access to its data and by hiding the internal representation of the data. For instance:

public class Well {
  decimal dollars; // private field
  public int Cents {
    get { return(int)(dollars * 100); }
    set {
      // value is an implicit variable in a set
      if (value>=0) // typical validation code
         dollars = (decimal)value/100;
    }
  }
}
class Test {
   static void Main(  ) {
      Well w = new Well(  );
      w.Cents = 25; // set
      int x = w.Cents; // get
      w.Cents += 10; // get and set(throw a dime in the well)
   }
}

The get accessor returns a value of the property’s type. The set accessor has an implicit parameter value that is of the property’s type.

Note

Many languages loosely implement properties with a get or set method convention, and in fact, C# properties are compiled to get_XXX or set_XXX methods, which is their representation in MSIL; for example:

public int get_Cents {...}
public void set_Cents (int value) {...}

Simple property accessors are inlined by the JIT (just-in-time compiler), which means there is no performance difference between a property access and a field access. Inlining is an optimization that replaces a method call with the body of that method.

Indexers

Syntax:

attributes? unsafe? access-modifier?
[
  [[sealed | abstract]? override] |
  new? [virtual | abstract]?
]?
type this [ attributes? [type arg]+ ] {  
 attributes? get statement-block |  // read-only 
 attributes? set statement-block |  // write-only  
 attributes? get statement-block    // read-write 
 attributes? set statement-block
}

abstract accessors don’t specify an implementation, so they replace a get/set block with a semicolon. Also see Section 2.8.1 earlier in this chapter.

An indexer provides a natural way to index elements in a class or struct that encapsulates a collection, using the open and closed bracket ([]) syntax of the array type. For example:

public class ScoreList {
  int[] scores = new int [5];
  // indexer
  public int this[int index] {
    get {
      return scores[index]; }
    set {
      if(value >= 0 && value <= 10)
        scores[index] = value;
    }
  }
  // property (read-only)
  public int Average {
    get {
      int sum = 0;
      foreach(int score in scores)
        sum += score;
      return sum / scores.Length;
    }
  }
}
class IndexerTest {
  static void Main(  ) {
    ScoreList sl = new ScoreList(  );
    sl[0] = 9;
    sl[1] = 8;
    sl[2] = 7;
    sl[3] = sl[4] = sl[1];
    System.Console.WriteLine(sl.Average);
  }
}

A type may declare multiple indexers that take different parameters.

Note

Indexers are compiled to get_Item (...)/set_Item (...) methods, which is their representation in MSIL:

public Story get_Item (int index) {...}
public void set_Item (int index, Story value) {...}

Consequently, you cannot define a property named Item in a type that has an indexer.

Methods

Method declaration syntax:

attributes? unsafe? access-modifier?
[
  [[sealed | abstract]? override] | 
  new? [ virtual | abstract | static extern? ]?
]?
[ void | type ] method-name (parameter-list)
statement-block

Parameter list syntax:

[ attributes? [ref | out]? type arg ]*
[ params attributes? type[ ] arg ]?

abstract and extern methods don’t contain a method body. Also see Section 2.8.1 earlier in this chapter.

All C# code executes in a method or in a special form of a method (constructors, destructors, and operators are special types of methods, and properties and indexers are internally implemented with get/set methods).

Signatures

A method’s signature is characterized by the type and modifier of each parameter in its parameter list. The parameter modifiers ref and out allow arguments to be passed by reference rather than by value.

Passing arguments by value

By default, arguments in C# are passed by value, which is by far the most common case. This means a copy of the value is created when passed to the method:

static void Foo(int p) {++p;}
public static void Main(  ) {
  int x = 8;
  Foo(x); // make a copy of the value type x
  Console.WriteLine(x); // x will still be 8
}

Assigning p a new value doesn’t change the contents of x, since p and x reside in different memory locations.

ref modifier

To pass by reference, C# provides the parameter modifier ref. Using this modifier allows p and x to refer to the same memory locations:

static void Foo(ref int p) {++p;}
public static void Test(  ) {
  int x = 8;
  Foo(ref x); // send reference of x to Foo
  Console.WriteLine(x); // x is now 9
}

Now, assigning p a new value changes the contents of x. This is usually why you want to pass by reference, though occasionally it is an efficient technique with which to pass large structs. Notice how the ref modifier is required in the method call, as well as in the method declaration. This makes it very clear what’s going on and removes ambiguity since parameter modifiers change the signature of a method (see Section 2.9.7.1 earlier in this chapter).

out modifier

C# is a language that enforces the requirement that variables be assigned before use, so it also provides the out modifier, which is the natural complement of the ref modifier. While a ref modifier requires that a variable be assigned a value before being passed to a method, the out modifier requires that a variable be assigned a value before returning from a method:

using System;
class Test {
  static void Split(string name, out string firstNames, 
                    out string lastName) {
     int i = name.LastIndexOf(' '),
     firstNames = name.Substring(0, i);
     lastName = name.Substring(i+1);
  }
  public static void Main(  ) {
    string a, b;
    Split("Nuno Bettencourt", out a, out b);
    Console.WriteLine("FirstName:{0}, LastName:{1}", a, b);
  }
}

params modifier

The params parameter modifier may be specified on the last parameter of a method so the method can accept any number of parameters of a particular type. For example:

using System;
class Test {
  static int Add(params int[] iarr) {
    int sum = 0;
    foreach(int i in iarr)
      sum += i;
    return sum;
  }
  static void Main(  ) {
    int i = Add(1, 2, 3, 4);
    Console.WriteLine(i); // 10
  }
}

Overloading methods

A type may overload methods (have multiple methods with the same name) as long as the signatures are different.[1]

For example, the following methods can coexist in the same type:

void Foo(int x);
viod Foo(double x);
void Foo(int x, float y);
void Foo(float x, int y);
void Foo(ref int x);

However, the following pairs of methods can’t coexist in the same type, since the return type and params modifier don’t qualify as part of a method’s signature:

void Foo(int x);
float Foo(int x); // compile error
void Goo (int[] x);
void Goo (params int[] x); // compile error

Operators

Overloadable operators:

+ - ! ~ ++ -- + - 
* (binary only) / % & (binary only)
| ^ << >> ++ != > < 
>= <=  ==

Literals doubling as overloadable operators:

true false

C# lets you overload operators to work with operands that are custom classes or structs using operators. An operator is a static method with the keyword operator preceding the operator to be overloaded (instead of a method name), parameters representing the operands, and return type representing the result of an expression.

Implementing value equality

A pair of references exhibit referential equality when both references point to the same object. By default, the == and != operators will compare two reference-type variables by reference. However, it is ocassionally more natural for the == and != operators to exhibit value equality, whereby the comparison is based on the value of the objects that the references point to.

Whenever overloading the == and != operators, you should always override the virtual Equals method to route its functionality to the == operator. This allows a class to be used polymorphically (which is essential if you want to take advantage of functionality such as the collection classes). It also provides compatibility with other .NET languages that don’t overload operators.

Tip

A good guideline for knowing whether to implement the == and != operators is if it is natural for the class to overload other operators too, such as <, >, +, or -; otherwise, don’t bother—just implement the Equals method. For structs, overloading the == and != operators provides a more efficient implementation than the default one.

using System;
class Note {
  int value;
  public Note(int semitonesFromA) {
    value = semitonesFromA;
  }
  public static bool operator ==(Note x, Note y) {
    return x.value == y.value;
  }
  public static bool operator !=(Note x, Note y) {
    return x.value != y.value;
  }
  public override bool Equals(object o) {
    if(!(o is Note))
      return false;
    return this ==(Note)o;
  }
  public static void Main(  ) {
    Note a = new Note(4);
    Note b = new Note(4);
    Object c = a;
    Object d = b;
    
    // To compare a and b by reference:
    Console.WriteLine((object)a ==(object)b); // false 
    
    // To compare a and b by value:
    Console.WriteLine(a == b); // true
    
    // To compare c and d by reference:
    Console.WriteLine(c == d); // false
    
    // To compare c and d by value:
    Console.WriteLine(c.Equals(d)); // true
  }
}

Logically paired operators

The C# compiler enforces the rule that operators that are logical pairs must both be defined. These operators are == and !=; < and >; and <= and >=.

Custom implicit and explicit conversions

As explained in Section 2.2, the rationale behind implicit conversions is they are guaranteed to succeed and not lose information during the conversion. Conversely, an explicit conversion is required either when runtime circumstances determine if the conversion succeeds or if information is lost during the conversion. In the following example, we define conversions between the musical Note type and a double (which represents the frequency in hertz of that note):

...
// Convert to hertz
public static implicit operator double(Note x) {
  return 440*Math.Pow(2,(double)x.value/12);
}

// Convert from hertz (accurate only to nearest semitone)
public static explicit operator Note(double x) {
  return new Note((int)(0.5+12*(Math.Log(x/440)/Math.Log(2))));
}
...

Note n =(Note)554.37; // explicit conversion
double x = n; // implicit conversion

Three-state logic operators

The true and false keywords are used as operators when defining types with three-state logic to enable these types to seamlessly work with constructs that take Boolean expressions, namely the if, do, while, for, and conditional (?:) statements. The System.Data.SQLTypes. SQLBoolean struct provides this functionality:

public struct SQLBoolean ... {
  int value;
  ...
  public SQLBoolean(int value) {
    this.value = value;
  }
  public static bool operator true(SQLBoolean x) {
    return x.value == 1;
  }
  public static bool operator false(SQLBoolean x) {
    return x.value == -1;
  }
  public static SQLBoolean operator !(SQLBoolean x) {
    return new SQLBoolean(- x.value);
  }
  public bool IsNull {
    get { return value == 0;}
  }
  ...
}
class Test {
  public static void Foo(SQLBoolean a) {
    if (a)
      Console.WriteLine("True");
    else if (! a)
      Console.WriteLine("False");
    else
      Console.WriteLine("Null");
   }
}

Indirectly overloadable operators

The && and || operators are automatically evaluated from & and |, so they don’t need to be overloaded. The [] operators can be customized with indexers (see Section 2.9.6 earlier in this chapter). The assignment operator = can’t be overloaded, but all other assignment operators are automatically evaluated from their corresponding binary operators (e.g., += is evaluated from +).

Instance Constructors

Syntax:

attributes? unsafe? access-modifier?
unsafe?
class-name (parameter-list)
[ :[ base | this ] (argument-list) ]?
statement-block

An instance constructor allows you to specify the code to be executed when a class or struct is instantiated. A class constructor first creates a new instance of that class on the heap and then performs initialization, while a struct constructor merely performs initialization.

Unlike ordinary methods, a constructor has the same name as the class or struct in which it is declared, and has no return type:

class MyClass {
  public MyClass(  ) {
    // initialization code
  }
}

A class or struct can overload constructors and may call one of its overloaded constructors before executing its method body using the this keyword:

using System;
class MyClass {
  public int x;
  public MyClass(  ) : this(5) {}
  public MyClass(int v) {
    x = v;
  }
  public static void Main(  ) {
    MyClass m1 = new MyClass(  );
    MyClass m2 = new MyClass(10);
    Console.WriteLine(m1.x); // 5
    Console.WriteLine(m2.x); // 10;
  }
}

If a class does not define any constructors, an implicit parameterless constructor is created. A struct can’t define a parameterless constructor, since a constructor that initializes each field with a default value (effectively zero) is always implicitly defined.

Calling base class constructors

A class constructor must call one of its base class constructors first. In the case where the base class has a parameterless constructor, that constructor is called implicitly. In the case where the base class provides only constructors that require parameters, the derived class constructor must explicitly call one of the base class constructors using the base keyword. A constructor may also call an overloaded constructor (which calls base for it):

class B {
  public int x ;
  public B(int a) {
    x = a;
  }
  public B(int a, int b) {
    x = a * b;
  }
  // Notice that all of B's constructors need parameters
}
class D : B {
  public D(  ) : this(7) {} // call an overloaded constructor
  public D(int a) : base(a) {} // call a base class constructor
}

Field initialization order

Another useful way to perform initialization is to assign fields an initial value in their declaration:

class MyClass {
  int x = 5;
}

Field assignments are performed before the constructor is executed and are initialized in the textual order in which they appear. For classes, every field assignment in each class in the inheritance chain is executed before any of the constructors are executed, from the least derived to the most derived class.

Constructor access modifiers

A class or struct may choose any access modifier for a constructor. It is occasionally useful to specify a private constructor to prevent a class from being constructed. This is appropriate for utility classes made up entirely of static members, such as the System.Math class.

Static Constructors

Syntax:

attributes? unsafe? extern?
static class-name ( ) 
statement-block

Note that extern static constructors don’t specify an implementation, so they replace a statement-block with a semicolon.

A static constructor allows initialization code to be executed before the first instance of a class or struct is created, or before any static member of the class or struct is accessed. A class or struct can define only one static constructor, and it must be parameterless and have the same name as the class or struct:

class Test {
  static Test(  ) {
    Console.WriteLine("Test Initialized");
  }
}

Base class constructor order

Consistent with instance constructors, static constructors respect the inheritance chain, so each static constructor from the least derived to the most derived is called.

Static field initialization order

Consistent with instance fields, each static field assignment is made before any of the static constructors is called, and the fields are initialized in the textual order in which they appear:

class Test {
  public static int x = 5;
  public static void Foo(  ) {}
  static Test(  ) {
    Console.WriteLine("Test Initialized");
  }
}

Accessing either Test.x or Test.Foo assigns 5 to x and prints Test Initialized.

Nondeterminism of static constructor calls

Static constructors can’t be called explicitly, and the runtime may invoke them well before they are first used. Programs shouldn’t make any assumptions about the timing of a static constructor’s invocation. In the following example, Test Initialized can be printed after Test2 Initialized:

class Test2 {
  public static void Foo(  ) {}
  static Test2(  ) {
    Console.WriteLine("Test2 Initialized");
  }
}
...
Test.Foo(  );
Test2.Foo(  );

Self-Referencing

C# provides the keywords for accessing the members of a class itself or of the class from which it is derived, namely the this and base keywords.

this keyword

The this keyword denotes a variable that is a reference to a class or struct instance and is accessible only from within nonstatic function members of the class or struct. The this keyword is also used by a constructor to call an overloaded constructor (see Section 2.9.9 earlier in this chapter) or declare or access indexers (see Section 2.9.6 earlier in this chapter). A common use of the this variable is to unambiguate a field name from a parameter name:

class Dude {
  string name;
  public Dude(string name) {
    this.name = name;
  }
  public void Introduce(Dude a) {
    if (a!=this)
      Console.WriteLine("Hello, I'm "+name);
  }
}

base keyword

The base keyword is similar to the this keyword, except that it accesses an overridden or hidden base-class function member. The base keyword can also call a base-class constructor (see Section 2.9.9 earlier in this chapter) or access a base-class indexer (using base instead of this). Calling base accesses the next most derived class that defines that member. To build upon the example with the this keyword:

class Hermit : Dude {
  public Hermit(string name): base(name) {}
  public new void Introduce(Dude a) {
    base.Introduce(a);
    Console.WriteLine("Nice Talking To You");
  }
}

Tip

There is no way to access a specific base-class instance member, as with the C++ scope resolution :: operator.

Destructors and Finalizers

Syntax:

attributes? unsafe?
~class-name ( )
statement-block

Destructors are class-only methods used to clean up non-memory resources just before the garbage collector reclaims the memory for an object. Just as a constructor is called when an object is created, a destructor is called when an object is destroyed. C# destructors are very different from C++ destructors, primarily because of the garbage collector’s presence. First, memory is automatically reclaimed with a garbage collector, so a destructor in C# is solely used for non-memory resources. Second, destructor calls are non-deterministic. The garbage collector calls an object’s destructor when it determines it is no longer referenced. However, it may determine that this is an undefined period of time after the last reference to the object disappeared.

The C# compiler expands a destructor into a Finalize method override:

protected override void Finalize(  ) {
  ...
  base.Finalize(  );
}

For more details on the garbage collector and finalizers, see Section 3.12 in Chapter 3.

Nested Types

A nested type is declared within the scope of another type. Nesting a type has three benefits:

  • It can access all the members of its enclosing type, regardless of a member’s access modifier.

  • It can be hidden from other types with type-member access modifiers.

  • Accessing a nested type from outside its enclosing type requires specifying the type name (same principle as static members).

Here’s an example of a nested type:

using System;
class A {
  int x = 3; // private member
  protected internal class Nested {// choose any access level
    public void Foo (  ) {
      A a = new A (  );
      Console.WriteLine (a.x); // can access A's private members
    }
  }
}
class B {
  static void Main (  ) {
    A.Nested n = new A.Nested (  ); // Nested is scoped to A
    n.Foo (  );
  }
}
// an example of using "new" on a type declaration
class C : A {
   new public class Nested {} // hide inherited type member
}

Tip

Nested classes in C# are roughly equivalent to static inner classes in Java. There is no C# equivalent to Java’s nonstatic inner classes, in which an inner class has a reference to an instance of the enclosing class.

Interfaces

Syntax:

attributes? unsafe? access-modifier?
new?
interface interface-name [ : base-interface+ ]?
{ interface-members }

An interface is like a class, but with these major differences:

  • An interface provides a specification rather than an implementation for its members. This is similar to a pure abstract class, which is an abstract class consisting of only abstract members.

  • A class and struct can implement multiple interfaces; a class can inherit only from a single class.

  • A struct can implement an interface but can’t inherit from a class.

Earlier we defined polymorphism as the ability to perform the same operations on many types, as long as each type shares a common subset of characteristics. The purpose of an interface is precisely for defining such a set of characteristics.

An interface is comprised of one or more methods, properties, indexers, and events. These members are always implicitly public and implicitly abstract (therefore virtual and nonstatic).

Defining an Interface

An interface declaration is like a class declaration, but it provides no implementation for its members, since all its members are implicitly abstract. These members are intended to be implemented by a class or struct that implements the interface.

Here’s a simple interface that defines a single method:

public interface IDelete {
  void Delete(  );
}

Implementing an Interface

Classes or structs that implement an interface may be said to “fulfill the contract of the interface.” In this example, GUI controls that support the concept of deleting, such as a TextBox or TreeView, or your own custom GUI control, can implement the IDelete interface:

public class TextBox : IDelete {
  public void Delete(  ) {...}
}
public class TreeView : IDelete {
  public void Delete(  ) {...}
}

If a class inherits from a base class, the name of each interface to be implemented must appear after the base-class name:

public class TextBox : Control, IDelete {...}
public class TreeView : Control, IDelete {...}

Using an Interface

An interface is useful when you need multiple classes to share characteristics not present in a common base class. In addition, an interface is a good way to ensure that these classes provide their own implementation for the interface member, since interface members are implicitly abstract.

The following example assumes a form containing many GUI controls, including some TextBox and TreeView controls, in which the currently focused control is accessed with the ActiveControl property. When a user clicks Delete on a menu item or a toolbar button, you test to see if ActiveControl implements IDelete, and if so, cast it to IDelete to call its Delete method:

class MyForm {
   ...
   void DeleteClick(  ) {
      if (ActiveControl is IDelete) {
         IDelete d = (IDelete)ActiveControl;
         d.Delete(  );
      }
   }
}

Extending an Interface

Interfaces may extend other interfaces. For instance:

interface ISuperDelete : IDelete {
   bool CanDelete {get;}
   event EventHandler CanDeleteChanged;
}

In implementing the ISuperDelete interface, an ActiveControl implements the CanDelete property to indicate it has something to delete and isn’t read-only. The control also implements the CanDeleteChanged event to fire an event whenever its CanDelete property changes. This framework lets the application ghost its Delete menu item and toolbar button when the ActiveControl is unable to delete.

Explicit Interface Implementation

If there is a name collision between an interface member and an existing member in the class or struct, C# allows you to explicitly implement an interface member to resolve the conflict. In this example, we resolve a conflict that arises when we implement two interfaces that each define a Delete method:

public interface IDesignTimeControl {
   ...
   object Delete(  );
}
public class TextBox : IDelete, IDesignTimeControl {
   ...
   void IDelete.Delete(  ) {...}
   object IDesignTimeControl.Delete(  ) {...}
   // Note that explicitly implementing just one of them would
   // be enough to resolve the conflict
}

Unlike implicit interface implementations, explicit interface implementations can’t be declared with abstract, virtual, override, or new modifiers. In addition, they are implicitly public, while an implicit implementation requires the use of the public modifier. However, to access the method, the class or struct must be cast to the appropriate interface first:

TextBox tb = new TextBox(  );
IDesignTimeControl idtc = (IDesignTimeControl)tb;
IDelete id = (IDelete)tb;
idtc.Delete(  );
id.Delete(  );

Reimplementing an Interface

If a base class implements an interface member with the virtual (or abstract) modifier, a derived class can override it. If not, the derived class must reimplement the interface to override that member:

public class RichTextBox : TextBox, IDelete {
   // TextBox's IDelete.Delete is not virtual (since explicit
   // interface implementations cannot be virtual)
   public void Delete(  ) {}
}

The implementation in this example lets you use a RichTextBox object as an IDelete object and call RichTextBox’s version of Delete.

Interface Conversions

A class or struct T can be implicitly cast to an interface I that T implements. Similarly, an interface X can be implicitly cast to an interface Y that X inherits from. An interface can be explicitly cast to any other interface or nonsealed class. However, an explicit cast from an interface I to a sealed class or struct T is permitted only if T can implement I. For example:

interface IDelete {...}
interface IDesignTimeControl {...}
class TextBox : IDelete, IDesignTimeControl {...}
sealed class Timer : IDesignTimeControl {...}

TextBox tb1 = new TextBox (  );
IDelete d = tb1; // implicit cast
IDesignTimeControl dtc = (IDesignTimeControl)d;
TextBox tb2 = (TextBox)dtc;
Timer t = (Timer)d; // illegal, a Timer can never implement IDelete

Standard boxing conversions happen when converting between structs and interfaces.

Arrays

Syntax:

type[*]+ array-name = new type [ dimension+ ][*]*;

Note that [*] is the set: [] [,] [,,] ...

Arrays allow a group of elements of a particular type to be stored in a contiguous block of memory. Array types derive from System.Array and are declared in C# using brackets ([]). For instance:

char[] vowels = new char[] {'a','e','i','o','u'};
Console.WriteLine(vowels [1]); // Prints "e"

The preceding function call prints “e” because array indexes start at 0. To support other languages, .NET can create arrays based on arbitrary start indexes, but the FCL libraries always use zero-based indexing. Once an array has been created, its length can’t be changed. However, the System.Collection classes provide dynamically sized arrays, as well as other data structures, such as associative (key/value) arrays (see Section 3.4 in Chapter 3).

Multidimensional Arrays

Multidimensional arrays come in two varieties, rectangular and jagged. Rectangular arrays represent an n-dimensional block; jagged arrays are arrays of arrays:

// rectangular
int [,,] matrixR = new int [3, 4, 5]; // creates one big cube
// jagged
int [][][] matrixJ = new int [3][][];
for (int i = 0; i < 3; i++) {
   matrixJ[i] = new int [4][];
   for (int j = 0; j < 4; j++)
      matrixJ[i][j] = new int [5];
} 
// assign an element
matrixR [1,1,1] = matrixJ [1][1][1] = 7;

Local and Field Array Declarations

For convenience, local and field declarations can omit the array type when assigning a known value, because the type is specified in the declaration:

int[,] array = {{1,2},{3,4}};

Array Length and Rank

Arrays know their own length. For multidimensional arrays, the GetLength method returns the number of elements for a given dimension, which is counted from (the outermost) to the array’s Rank-1 (the innermost):

// one-dimensional
for(int i = 0; i < vowels.Length; i++);
// multidimensional
for(int i = 0; i < matrixR.GetLength(2); i++);

Bounds Checking

All array indexing is bounds-checked by the CLR, with IndexOutOf-RangeException thrown for invalid indexes. As in Java, bounds checking prevents program faults and debugging difficulties while enabling code to be executed with security restrictions.

Tip

Generally the performance hit from bounds checking is minor, and the JIT can perform optimizations such as determining each array index is safe before entering a loop, thus avoiding a check made for each iteration. In addition, C# provides unsafe code to explicitly bypass bounds checking (see Section 2.17 later in this chapter).

Array Conversions

Arrays of reference types can be converted to other arrays using the same logic you apply to its element type (this is called array covariance). All arrays implement System.Array, which provides methods to generic get and set elements regardless of array type.

Enums

Syntax:

attributes? access-modifier?
new?
enum enum-name [ : integer type ]?
{ [attributes? enum-member-name [ = value ]? ]* }

Enums specify a group of named numeric constants:

public enum Direction {North, East, West, South}

Unlike in C, enum members must be used with the enum type name. This resolves naming conflicts and makes code clearer:

Direction walls = Direction.East;

By default, enums are assigned integer constants: 0, 1, 2, etc. You can optionally specify an alternative numeric type to base your enum on and explicitly specify values for each enum member:

[Flags]
public enum Direction : byte {
   North=1, East=2, West=4, South=8
}
Direction walls = Direction.North | Direction.West;
if((walls & Direction.North) != 0)
    System.Console.WriteLine("Can't go north!");

The [Flags] attribute is optional. It informs the runtime that the values in the enum can be bit-combined and should be decoded accordingly in the debugger or when outputting text to the console. For example:

Console.WriteLine(walls); // Displays "North, West"
Console.WriteLine(walls.ToString("d")); // displays "5"

The System.Enum type also provides many useful static methods for enums that allow you to determine the underlying type of an enum, check if a specific value is supported, initialize an enum from a string constant, retrieve a list of the valid values, and perform other common operations such as conversions. Here is an example:

using System;
public enum Toggle : byte { Off=0, On=1 }
class TestEnum {
  static void Main(  ) {
    Type t = Enum.GetUnderlyingType(typeof(Toggle));
    Console.WriteLine(t); // Prints "System.Byte"

    bool bDimmed = Enum.IsDefined(typeof(Toggle), "Dimmed");
    Console.WriteLine(bDimmed); // Prints "False"

    Toggle tog =(Toggle)Enum.Parse(typeof(Toggle), "On");
    Console.WriteLine(tog.ToString("d")); // Prints "1"
    Console.WriteLine(tog); // Prints "On"

    Array oa = Enum.GetValues(typeof(Toggle));
    foreach(Toggle atog in oa) // Prints "Off=0, On=1"
      Console.WriteLine("{0}={1}", atog, atog.ToString("d")); 
  }
}

Enum Operators

The operators relevant to enums are:

==

!=

< >

<=

&

>=

+

-

^

|

-=

+=

=

++

~

sizeof

--

  

Enum Conversions

Enums can be explicitly converted to other enums. Enums and numeric types can be explicitly converted to one another. A special case is the numeric literal 0, which can be implicitly converted to an enum.

Delegates

Syntax:

attributes? unsafe? access-modifier?
new?
delegate
[ void | type ]
delegate-name (parameter-list);

A delegate is a type that defines a method signature so delegate instances can hold and invoke a method or list of methods that match its signature. A delegate declaration consists of a name and a method signature.[2]

Here’s an example:

delegate bool Filter(string s);

This declaration lets you create delegate instances that can hold and invoke methods that return bool and have a single string parameter. In the following example a Filter is created that holds the FirstHalfOfAlphabet method. You then pass the Filter to the Display method, which invokes the Filter:

class Test {
  static void Main(  ) {
    Filter f = new Filter(FirstHalfOfAlphabet);
    Display(new String [] {"Ant","Lion","Yak"}, f);
  }
  static bool FirstHalfOfAlphabet(string s) {
    return "N".CompareTo(s) > 0;
  }
  static void Display(string[] names, Filter f) {
    int count = 0;
    foreach(string s in names)
      if(f(s)) // invoke delegate
        Console.WriteLine("Item {0} is {1}", count++, s);
  }
}

Multicast Delegates

Delegates can hold and invoke multiple methods. In this example, we declare a simple delegate called MethodInvoker, which can hold and then invoke the Foo and Goo methods sequentially. The += method creates a new delegate by adding the right delegate operand to the left delegate operand.

using System;
delegate void MethodInvoker(  );
class Test {
  static void Main(  ) {
     new Test(  ); // prints "Foo", "Goo"
  }
  Test(  ) {
    MethodInvoker m = null;
    m += new MethodInvoker(Foo);
    m += new MethodInvoker(Goo);
    m(  );
  }
  void Foo(  ) {
    Console.WriteLine("Foo");
  }
  void Goo(  ) {
    Console.WriteLine("Goo");
  }
}

A delegate can also be removed from another delegate using the -= operator:

Test(  ) {
  MethodInvoker m = null;
  m += new MethodInvoker(Foo);
  m -= new MethodInvoker(Foo);
  m(  ); // m is now null, throws NullReferenceException
}

Delegates are invoked in the order they are added. If a delegate has a non-void return type, then the value of the last delegate invoked is returned. Note that the += and -= operations on a delegate are not thread-safe.

Tip

To work with the .NET runtime, C# compiles += and -= operations made on a delegate to the static Combine and Remove methods of the System.Delegate class.

Delegates Compared with Function Pointers

A delegate is behaviorally similar to a C function pointer (or Delphi closure) but can hold multiple methods and the instance associated with each nonstatic method. In addition, delegates, like all other C# constructs used outside unsafe blocks, are type-safe and secure, which means you’re protected from pointing to the wrong type of method or a method that you don’t have permission to access.

Delegates Compared with Interfaces

A problem that can be solved with a delegate can also be solved with an interface. For instance, here is how to solve the filter problem using an IFilter interface:

using System;
interface IFilter {
   bool Filter(string s);
}
class Test {
  class FirstHalfOfAlphabetFilter : IFilter {
    public bool Filter(string s) {
      return ("N".CompareTo(s) > 0);
    }      
  }
  static void Main(  ) {
    FirstHalfOfAlphabetFilter f = new FirstHalfOfAlphabetFilter(  );
    Display(new string [] {"Ant", "Lion", "Yak"}, f);
  }
  static void Display(string[] names, IFilter f) {
    int count = 0;
    foreach (string s in names)
      if (f.Filter(s))
        Console.WriteLine("Item {0} is {1}", count++, s);
  }
}

In this case, the problem was slightly more elegantly handled with a delegate, but generally delegates are best used for event handling.

Events

Event handling is essentially a process by which one object can notify other objects that an event has occurred. This process is largely encapsulated by multicast delegates, which have this ability built in.

Defining a Delegate for an Event

The FCL defines numerous public delegates used for event handling, but you can also write your own. For example:

delegate void MoveEventHandler(object source, MoveEventArgs e);

By convention, an event delegate’s first parameter denotes the source of the event, and the delegate’s second parameter derives from System.EventArgs and stores data about the event.

Storing Data for an Event with EventArgs

You can define subclasses of EventArgs to include information relevant to a particular event:

public class MoveEventArgs : EventArgs {
  public int newPosition;
  public bool cancel;
  public MoveEventArgs(int newPosition) {
    this.newPosition = newPosition;
  }
}

Declaring and Firing an Event

A class or struct can declare an event by applying the event modifier to a delegate field. In this example, the Slider class has a Position property that fires a Move event whenever its Position changes:

class Slider {
  int position;
  public event MoveEventHandler Move;
  public int Position {
    get { return position; }
    set {
      if (position != value) { // if position changed
        if (Move != null) { // if invocation list not empty
          MoveEventArgs args = new MoveEventArgs(value);
          Move(this, args); // fire event
          if (args.cancel)
            return;
        }
        position = value;
      }
    }  
  }
}

The event keyword promotes encapsulation by ensuring that only the += and -= operations can be performed on the delegate. This means other classes can register themselves to be notified of the event, but only the Slider can invoke the delegate (fire the event) or clear the delegate’s invocation list.

Acting on an Event with Event Handlers

You can act on an event by adding an event handler to an event. An event handler is a delegate that wraps the method you want invoked when the event is fired.

In the next example, we want our Form to act on changes made to a Slider’s Position. You do this by creating a MoveEventHandler delegate that wraps the event-handling method, the slider.Move method. This delegate is added to the Move event’s existing list of MoveEventHandlers (which starts off empty). Changing the position on the Slider object fires the Move event, which invokes the slider.Move method:

class Form {
  static void Main(  ) {
    Slider slider = new Slider(  );
    // register with the Move event
    slider.Move += new MoveEventHandler(slider_Move);
    slider.Position = 20;
    slider.Position = 60;
  }
  static void slider_Move(object source, MoveEventArgs e) {
    if(e.newPosition < 50)
      Console.WriteLine("OK");
    else {
      e.cancel = true;
      Console.WriteLine("Can't go that high!");
    }
  }
}

Typically, the Slider class would be enhanced to fire the Move event whenever its Position is changed by a mouse movement, keypress, or other user action.

Event Accessors

Syntax:

attributes? unsafe? access-modifier?
[
  [[sealed | abstract]? override] |
  new? [virtual | static]?
]? 
event delegate type event-property accessor-name
{   
attributes? add statement-block   
attributes? remove statement-block
}

Note that abstract accessors don’t specify an implementation, so they replace an add/remove block with a semicolon.

Similar to the way properties provide controlled access to a field, event accessors provide controlled access to an event. Consider the following field declaration:

public event MoveEventHandler Move;

Except for the underscore prefix added to the field (to avoid a name collision), this is semantically identical to:

private MoveEventHandler _Move;
public event MoveEventHandler Move {
  add {
    _Move += value;
  }
  remove {
    _Move -= value;
 }
}

The ability to specify a custom implementation of add and remove handlers for an event allows a class to proxy an event generated by another class, thus acting as a relay for an event rather than as the generator of that event. Another advantage of this technique is to eliminate the need to store a delegate as a field, which can be costly in terms of storage space. For instance, a class with 100 event fields would store 100 delegate fields, even though maybe only four of those events are actually assigned. Instead, you can store these delegates in a dictionary and add and remove the delegates from that dictionary (assuming the dictionary holding four elements uses less storage space than 100 delegate references).

Tip

The add and remove parts of an event are compiled to add_XXX and remove_XXX methods.

try Statements and Exceptions

Syntax:

try statement-block
[catch (exception type value?)? statement-block]+ |
finally statement-block |
[catch (exception type value?)? statement-block]+
finally statement-block

try Statement

The purpose of a try statement is to simplify dealing with program execution in exceptional circumstances. A try statement does two things. First, it lets exceptions thrown during the try block’s execution be caught by the catch block. Second, it ensures that execution can’t leave the try block without first executing the finally block. A try block must be followed by one or more catch blocks, a finally block, or both.

Exceptions

C# exceptions are objects that contain information representing the occurrence of an exceptional program state. When an exceptional state has occurred (e.g., a method receives an illegal value), an exception object may be thrown, and the call stack is unwound until the exception is caught by an exception handling block.

In the following example, we have an Account class. An exceptional state for the account class is when its balance is below zero, which occurs when the Withdraw method receives a withdraw amount larger than the current balance. Our test class makes our customer tiffanyTaylor perform several actions that involve despositing and withdrawing from an account. When she attempts to withdraw more money than she has in her account, a FundException is thrown, which we will catch so we can notify the user and display her current account balance.

using System;

public class FundException : Exception {
  public FundException(decimal balance) :
    base("Total funds are "+balance+" dollars") {}
}

class Account {
  decimal balance;
  public decimal Balance {
    get {return balance;}
  }
  public void Deposit(decimal amount) {
    balance += amount;
  }
  public void Withdraw(decimal amount) {
    if (amount > balance)
      throw new FundException(balance);
    balance -= amount;
  }
}

class Customer {
  Account savingsAccount = new Account(  );
  public void SellBike(  ) {
    savingsAccount.Deposit (1000);
  }
  public void BuyCar(  ) {
    savingsAccount.Withdraw (30000);
  }
  public void GoToMovie(  ) {
    savingsAccount.Withdraw (20);
  }
}

class Test {
  static void Main(  ) {
    Customer tiffanyTaylor = new Customer(  );
    bool todaysTasksDone = false;
    try {
      tiffanyTaylor.SellBike(  );
      tiffanyTaylor.BuyCar(  );
      tiffanyTaylor.GoToMovie(  );
      todaysTasksDone = true;
    }
    catch (FundException ex) {
      Console.WriteLine(ex.Message);
    }
    finally {
      Console.WriteLine(todaysTasksDone);
    }
  }
}

At the point when the FundException is thrown, the call stack is comprised of three methods: Withdraw, BuyCar, and Main. Each of these methods will in succession return immediately to its calling method until one of them has a catch block that can handle the exception, in this case Main. Without a try statement, the call stack would have been unwound completely, and our program would have terminated without displaying what caused the problem and without letting the user know whether today’s tasks were completed.

catch

A catch clause specifies which exception type (including derived types) to catch. An exception must be of type System.Exception or of a type that derives from System.Exception. Catching System.Exception provides the widest possible net for catching errors, which is useful if your handling of the error is totally generic, such as with an error-logging mechanism. Otherwise, you should catch a more specific exception type to prevent your catch block from dealing with a circumstance it wasn’t designed to handle (for example, an out-of-memory exception).

Omitting the exception variable

Specifying only an exception type without a variable name allows an exception to be caught when we don’t need to use the exception instance, and merely knowing its type will suffice. The previous example could have been written like this:

catch(FundException) { // don't specify variable
  Console.WriteLine("Problem with funds");
}

Omitting the catch expression

You may also completely omit the catch expression. This will catch an exception of any type, even types that are not derived from System.Exception (these could be thrown by non-CLS-compliant languages). The previous example could have been written like this:

catch {
  Console.WriteLine("Something went wrong...");
}

The fact that most exceptions do inherit from System.Exception is a CLS convention, not a CLR requirement.

Specifying multiple catch clauses

When declaring multiple catch clauses, only the first catch clause with an exception type that matches the thrown exception executes its catch block. It is illegal for an exception type B to precede an exception type D if B is a base class of D, since it would be unreachable.

try {...}
catch (NullReferenceException) {...}
catch (IOException) {...}
catch {...}

finally

finally blocks are always executed when control leaves the try block. A finally block is executed at one of the following times:

  • Immediately after the try block completes

  • Immediately after the try block prematurely exits with a jump statement (e.g., return, goto) and immediately before the target of the jump statement

  • Immediately after a catch block executes

finally blocks add determinism to a program’s execution by ensuring that particular code always gets executed.

In our main example, if some other exception occurred such as a memory exception, the finally block would have still been executed. This ensures that the user would know whether today’s tasks were completed. The finally block can also be used to gracefully restore program state when an exception occurs. Had our example used a connection to a remote account object, it would have been appropriate to close the account in the finally block.

Key Properties of the System.Exception Class

You will most frequently use the following properties of System.Exception:

StackTrace

This is a string representing all the methods called from the origin of the exception to the catch block.

Message

This is a string with a description of the error.

InnerException

Sometimes it is useful to catch an exception, then throw a new, more specific exception. For instance, you can catch an IOException, and then throw a DocumentLoadException that contains more specific information on what went wrong. In this scenario, the DocumentLoadException should include the IOException as the InnerException argument in its constructor, which is assigned to the InnerException property. This cascading exception structure can be particularly useful for debugging.

Tip

In C# all exceptions are runtime exceptions; there is no equivalent of Java’s compile-time exceptions.

Attributes

Syntax:

[[target:]? attribute-name (
positional-param+ |
[named-param = expression]+ |
positional-param+, [named-param = expression]+)?]

Attributes are language constructs that can decorate code elements (e.g., assemblies, modules, types, members, return values, and parameters) with additional information.

In every language, you specify information associated with the types, methods, parameters, and other elements of your program. For example, a type can specify a list of interfaces that it derives from, or a parameter can specify how its values are to be passed with modifiers such as the ref modifier in C#. The limitation of this approach is that you can only associate information with code elements using the predefined constructs that the language itself provides.

Attributes allow programmers to add to the types of information associated with these code elements. For example, serialization in the .NET Framework uses various serialization attributes applied to types and fields to define how these code elements are serialized. This is more flexible than requiring the language to have special syntax for serialization.

Attribute Classes

An attribute is defined by a class that inherits (directly or indirectly) from the abstract class System.Attribute. When specifying an attribute on an element, the attribute name is the name of the type. By convention the derived type name ends with the word “Attribute”, but this suffix isn’t required.

In this example we specify that the Foo class is serializable using the Serializable attribute:

[Serializable]
public class Foo {...}

The Serializable attribute is actually a type declared in the System namespace, as follows:

class SerializableAttribute : Attribute {...}

We could also specify the Serializable attribute using its fully qualified typename, as follows:

[System.SerializableAttribute]
public class Foo {...}

The preceding two examples that use the Serializable attribute are semantically identical.

Tip

The C# language and the FCL include a number of predefined attributes. For more information on the other attributes included in the FCL, and on creating your own attributes, see Section 3.11 in Chapter 3.

Named and Positional Parameters

Attributes can take parameters, which specify additional information on the code element beyond the mere presence of the attribute.

In this next example, we use the WebPermission attribute to specify that methods in class Foo cannot make web connections to any URL starting with http://www.oreilly.com/. This attribute allows you to include parameters that specify a security action and a URL:

[WebPermission(SecurityAction.Deny, 
    ConnectPattern="http://www.oreilly.com/.*")]
public class Foo {...}

Attribute parameters fall into one of two categories: positional and named parameters. In the preceding example, SecurityAction.Deny is a positional parameter, and ConnectPattern="http://www.oreilly.com/.*" is a named parameter.

The positional parameters for an attribute correspond to the parameters passed to one of the attribute type’s public constructors. The named parameters for an attribute correspond to the set of public read/write or write-only instance properties and fields on the attribute type.

Since the parameters used when specifying an attribute are evaluated at compile time, they are generally limited to constant expressions.

Explicitly Specifying Attribute Targets

Implicitly, the target of an attribute is the code element it immediately precedes. However, sometimes it is necessary to explicitly specify that the attribute applies to a particular target. The possible targets are assembly, module, type, method, property, field, param, event, and return.

Here is an example that uses the CLSCompliant attribute to specify the level of CLS compliance for an entire assembly:

[assembly:CLSCompliant(true)]

Specifying Multiple Attributes

You can specify multiple attributes on a single code element. Each attribute can be listed within the same pair of square brackets (separated by a comma), in separate pairs of square brackets, or any combination of the two.

Consequently, the following three examples are semantically identical:

[Serializable, Obsolete, CLSCompliant(false)]
public class Bar {...}

[Serializable] 
[Obsolete] 
[CLSCompliant(false)]
public class Bar {...}

[Serializable, Obsolete] 
[CLSCompliant(false)]
public class Bar {...}

Unsafe Code and Pointers

C# supports direct memory manipulation via pointers within blocks of code marked unsafe and compiled with the /unsafe compiler option. Pointer types are primarily useful for interoperability with C APIs but may also be used for accessing memory outside the managed heap or for performance-critical hotspots.

Pointer Types

For every value type or pointer type V in a C# program, there is a corresponding C# pointer type named V*. A pointer instance holds the address of a value. That value is considered to be of type V, but pointer types can be (unsafely) cast to any other pointer type. Table 2-3 summarizes the principal pointer operators supported by the C# language.

Table 2-3. Principal pointer operators

Operator

Meaning

&

The address-of operator returns a pointer to the address of a value.

*

The dereference operator returns the value at the address of a pointer.

->

The pointer-to-member operator is a syntactic shortcut, in which x->y is equivalent to (*x).y.

Unsafe Code

By marking a type, type-member, or statement block with the unsafe keyword, you’re permitted to use pointer types and perform C++-style pointer operations on memory within that scope. Here is an example that uses pointers with a managed object:

unsafe void RedFilter(int[,] bitmap) {
  const int length = bitmap.Length;
  fixed (int* b = bitmap) {
    int* p = b;
    for(int i = 0; i < length; i++)
      *p++ &= 0xFF;
  }
}

Unsafe code typically runs faster than a corresponding safe implementation, which in this case would have required a nested loop with array indexing and bounds checking. An unsafe C# method can be faster than calling an external C function too, since there is no overhead associated with leaving the managed execution environment.

The fixed Statement

Syntax:

fixed ([value type | void ]* name = [&]? expression )
  statement-block

The fixed statement is required to pin a managed object, such as the bitmap in the previous pointer example. During the execution of a program, many objects are allocated and deallocated from the heap. In order to avoid the unnecessary waste or fragmentation of memory, the garbage collector moves objects around. Pointing to an object would be futile if its address can change while referencing it, so the fixed statement tells the garbage collector to pin the object and not move it around. This can impact the efficiency of the runtime, so fixed blocks should be used only briefly, and preferably, heap allocation should be avoided within the fixed block.

C# returns a pointer only from a value type, never directly from a reference type. Arrays and strings are an exception to this, but only syntactically, since they actually return a pointer to their first element (which must be a value type), rather than the objects themselves.

Value types declared inline within reference types require the reference type to be pinned, as follows:

using System;
class Test {
  int x;
  static void Main(  ) {
    Test test = new Test(  );
    unsafe {
       fixed(int* p = &test.x) { // pins Test
         *p = 9;
       }
       System.Console.WriteLine(test.x);
    }
  }
}

Pointer to Member Operator

In addition to the & and * operators, C# also provides the C++-style -> operator, which can be used on structs:

using System;
struct Test {
   int x;
   unsafe static void Main(  ) {
      Test test = new Test(  );
      Test* p = &test;
      p->x = 9;
      System.Console.WriteLine(test.x);
   }
}

The stackalloc Keyword

Memory can be allocated in a block on the stack explicitly using the stackalloc keyword. Since it is allocated on the stack, its lifetime is limited to the execution of the method in which it is used, just as with other local variables. The block may use [] indexing but is purely a value type with no additional self-describing information or bounds checking, which an array provides:

int* a = stackalloc int [10];
for (int i = 0; i < 10; ++i)
   Console.WriteLine(a[i]); // print raw memory

void*

Rather than pointing to a specific value type, a pointer may make no assumptions about the type of the underlying data. This is useful for functions that deal with raw memory. An implicit conversion exists from any pointer type to a void*. A void* cannot be dereferenced, and arithmetic operations cannot be performed on void pointers. For example:

class Test {
  unsafe static void Main (  ) {
    short[  ] a = {1,1,2,3,5,8,13,21,34,55};
      fixed (short* p = a) {
        // sizeof returns size of value-type in bytes
        Zap (p, a.Length * sizeof (short));
      }
    foreach (short x in a)
      System.Console.WriteLine (x); // prints all zeros
  }
  unsafe static void Zap (void* memory, int byteCount) {
    byte* b = (byte*)memory;
      for (int i = 0; i < byteCount; i++)
        *b++ = 0;
  }
}

Pointers to Unmanaged Code

Pointers are also useful for accessing data outside the managed heap, such as when interacting with C DLLs or COM or when dealing with data not in the main memory, such as graphics memory or a storage medium on an embedded device.

Preprocessor Directives

Preprocessor directives supply the compiler with additional information about regions of code. The most common preprocessor directives are the conditional directives, which provide a way to include or exclude regions of code from compilation. For example:

#define DEBUG
using System;
class MyClass {
  int x;
  public void Foo(  ) {
  # if DEBUG
  # warning "Debug mode is ON"
    Console.WriteLine("Testing: x = {0}", x);
  # endif
  }
}

In this class, the statement in Foo is compiled conditionally, dependent upon the presence of the user-selected DEBUG symbol. If you remove the DEBUG symbol, the statement isn’t compiled. Preprocessor symbols can be defined within a source file as just shown and can be passed to the compiler with the /define:symbol command-line option.

The #error and #warning symbols prevent accidental misuse of conditional directives by making the compiler generate a warning or error given an undesirable set of compilation symbols.

Preprocessor Directives

The C# language supports the preprocessor directives shown in Table 2-4.

Table 2-4. Preprocessor directives

Preprocessor directive

Action

#define symbol

Defines symbol

#undef symbol

Undefines symbol

#if symbol [operator symbol2] ...

symbol to test; operator: ==, !=, &&, || followed by #else, #elif, #endif

#else

Executes code to subsequent #endif

#elif symbol [operator symbol2]

Combines #else branch and #if test

#endif

Ends conditional directives

#warning text

text: warning text to appear in compiler output

#error text

text: error message to appear in compiler output

#line number [file]

number specifies line in source code; file is the filename to appear in computer output

#region name

Marks beginning of outline

#end region

Ends an outline region

XML Documentation

C# offers three different styles of source code documentation: single-line comments, multiline comments, and documentation comments.

C/C++-Style Comments

Single- and multiline comments use the C++ syntax: // and /*...*/:

int x = 3; // this is a comment
MyMethod(  ); /* this is a
comment that spans two lines */

The disadvantage of this style of commenting is that there is no predetermined standard for documenting your types. Consequently, it can’t be easily parsed to automate the production of documentation. C# improves on this by allowing you to embed documentation comments in the source and by providing an automated mechanism for extracting and validating documentation at compile time.

Documentation Comments

Documentation comments are similar to C# single-line comments but start with /// and can be applied to any user-defined type or member. These comments can include embedded XML tags as well as descriptive text. These tags allow you to mark up the descriptive text to better define the semantics of the type or member and also to incorporate cross-references.

These comments can then be extracted at compile time into a separate output file containing the documentation. The compiler validates the comments for internal consistency, expands cross-references into fully qualified type IDs, and outputs a well-formed XML file. Further processing is left up to you, although a common next step is to run the XML through an XSL/T, generating HTML documentation.

Here is an example documentation for a simple type:

// Filename: DocTest.cs
using System;
class MyClass {
  /// <summary>
  /// The Foo method is called from
  ///   <see cref="Main">Main</see> 
  /// </summary>
  /// <mytag>Secret stuff</mytag>
  /// <param name="s">Description for s</param>
  static void Foo(string s) { Console.WriteLine(s); }
  static void Main(  ) { Foo("42"); }
}

XML Documentation Files

When the preceding source file is run through the compiler with the /doc:<filename> command-line options, this XML file is generated:

<?xml version="1.0"?>
<doc>
  <assembly>
    <name>DocTest</name>
  </assembly>
  <members>
    <member name="M:MyClass.Foo(System.String)">
      <summary>
      The Foo method is called from
        <see cref="M:MyClass.Main">Main</see> 
      </summary>
      <mytag>Secret stuff</mytag>
      <param name="s">Description for s</param>
     </member>
  </members>
</doc>

The <?xml...>, <doc>, and <members> tags are generated automatically and form the skeleton for the XML file. The <assembly> and <name> tags indicate the assembly that this type lives in. Every member preceded by a documentation comment is included in the XML file via a <member> tag with a name attribute that identifies the member. Note that the cref attribute in the <see> tag has also been expanded to refer to a fully qualified type and member. The predefined XML documentation tags embedded in the documentation comments are also included in the XML file. The tags have been validated to ensure that all parameters are documented, that the names are accurate, and that any cross-references to other types or members can be resolved. Finally, any additional user-defined tags are transferred verbatim.

Predefined XML Tags

This section lists the predefined set of XML tags that can be used to mark up the descriptive text:

<summary>, <remarks>
<summary>description</summary>
<remarks>description</remarks>

These tags describe a type or member. Typically, <summary>contains the description of a member, and <remarks> contains a full description of a type.

<param>
<param name="name">description</param>

This tag describes a parameter on a method. The name attribute is mandatory and must refer to a parameter on the method. If this tag is applied to any parameter on a method, all parameters on that method must be documented. You must enclose name in double quotation marks ("").

<returns>
<returns>description</returns>

This tag describes the return values for a method.

<exception>
<exception [cref="type"]>description</exception>

This tag describes the exceptions a method may throw. If present, the optional cref attribute should refer to the type of exception. You must enclose the type name in double quotation marks ("").

<permission>
<permission [cref="type"]>description</permission>

This tag describes the permission requirements for a type or member. If present, the optional cref attribute should refer to the type that represents the permission set required by the member, although the compiler doesn’t validate this. You must enclose the type name in double quotation marks ("").

<example>, <c>, <code>
<example>description</example> 
<c>code</c>
<code>code</code>

These tags provide a description and sample source code explaining the use of a type or member. Typically, the <example> tag provides the description and contains the <c> and <code> tags, although these can also be used independently. If you need to include an inline code snippet, use the <c> tag. If you need to include multiline snippets, use the <code> tag.

<see>, <seealso>
<see cref="member">text</see>
<seealso cref="member">text</seealso>

These tags identify cross-references in the documentation to other types or members. Typically, the <see> tag is used inline within a description, while the <seealso> tag is broken out into a separate “See Also” section. These tags are useful because they allow tools to generate cross-references, indexes, and hyperlinked views of the documentation. Member names must be enclosed by double quotation marks ("").

<value>
<value>description</value>

This tag describes a property on a class.

<paramref>
<paramref name="name"/>

This tag identifies the use of a parameter name within descriptive text, such as <remarks> or <summary>. The name must be enclosed by double quotation marks ("").

<list>, <para>
<list type=[ bullet| number| table]>
 <listheader>
 <term>name</term>
 <description>description</description>
 </listheader>
 <item>
 <term>name</term>
 <description>description</description>
 </item> 
</list> 
<para>text</para>

These tags provide hints to documentation generators on how to format the documentation.

<include>
<include file='filename' path='path-to-element'>

This tag specirfies an external file that contains documentation and an XPath path to a specific element in that file. For example, a path of docs[@id="001"]/* would retrieve whatever is inside of <docs id="001"/>. The filename and path must be enclosed by single quotation marks ('').

User-Defined Tags

There is little that is special about the predefined XML tags recognized by the C# compiler, and you are free to define your own. The only special processing done by the compiler is on the <param> tag (where it verifies the parameter name and confirms that all the parameters on the method are documented) and the cref attribute (where it verifies that the attribute refers to a real type or member and expands it to a fully qualified type or member ID). The cref attribute can also be used in your own tags and is verified and expanded just as it is in the predefined <exception>, <permission>, <see>, and <seealso> tags.

Type or Member Cross-References

Type names and type or member cross-references are translated into IDs that uniquely define the type or member. These names are composed of a prefix that defines what the ID represents and a signature of the type or member. Table 2-5 lists the set of type and member prefixes.

Table 2-5. XML type ID prefixes

Prefix

Applied to

N

Namespace

T

Type (class, struct, enum, interface, delegate)

F

Field

P

Property (includes indexers)

M

Method (includes special methods)

E

Event

!

Error

The rules describing how the signatures are generated are well-documented, although fairly complex.

Here is an example of a type and the IDs that are generated:

// Namespaces do not have independent signatures
namespace NS {
  // T:NS.MyClass
  class MyClass {
    // F:NS.MyClass.aField
    string aField;
    // P:NS.MyClass.aProperty
    short aProperty {get {...} set {...}}
    // T:NS.MyClass.NestedType
    class NestedType {...};
    // M:NS.MyClass.X(  )
    void X(  ) {...}
    // M:NS.MyClass.Y(System.Int32,System.Double@,System.Decimal@)
    void Y(int p1, ref double p2, out decimal p3) {...}
    // M:NS.MyClass.Z(System.Char[],System.Single[0:,0:])
    void Z(char[] p1, float[,] p2) {...}
    // M:NS.MyClass.op_Addition(NS.MyClass,NS.MyClass)
    public static MyClass operator+(MyClass c1, MyClass c2) {...}
    // M:NS.MyClass.op_Implicit(NS.MyClass)~System.Int32
    public static implicit operator int(MyClass c) {...}
    // M:NS.MyClass.#ctor
    MyClass(  ) {...}
    // M:NS.MyClass.Finalize
    ~MyClass(  ) {...}
    // M:NS.MyClass.#cctor
    static MyClass(  ) {...}
  }
}


[1] A minor exception to this rule is that two otherwise identical signatures cannot coexist if one parameter has the ref modifier and the other parameter has the out modifier.

[2] The signature of a delegate method includes its return type and allows the use of a params modifier in its parameter list, expanding the list of elements that characterize an ordinary method signature. The actual name of the target method is irrelevant to the delegate.

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

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