only for RuBoard - do not distribute or recompile Previous Section Next Section

2.9 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
}

2.9.1 Differences Between Classes and Structs

Classes differ from structs in the following ways:

2.9.2 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
  }
}

2.9.3 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.

2.9.4 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;
}
2.9.4.1 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.

2.9.5 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.

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.


2.9.6 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.

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.


2.9.7 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).

2.9.7.1 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.

2.9.7.2 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.

2.9.7.3 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).

2.9.7.4 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);
  }
}
2.9.7.5 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
  }
}
2.9.7.6 Overloading methods

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

[3] 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.

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

2.9.8 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.

2.9.8.1 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.

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
  }
}
2.9.8.2 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 >=.

2.9.8.3 Custom implicit and explicit conversions

As explained in the earlier section 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
2.9.8.4 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");
   }
}
2.9.8.5 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 +).

2.9.9 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.

2.9.9.1 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
}
2.9.9.2 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.

2.9.9.3 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.

2.9.10 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");
  }
}
2.9.10.1 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.

2.9.10.2 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.

2.9.10.3 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(  );

2.9.11 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.

2.9.11.1 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);
  }
}
2.9.11.2 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");
  }
}

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

2.9.12 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.

2.9.13 Nested Types

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

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
}

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.

only for RuBoard - do not distribute or recompile Previous Section Next Section