Classes and Structs

Class declaration syntax:

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

Struct declaration syntax:

attributes ? 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, 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).

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

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 typically represent simple types, whereby value-type semantics are desirable (e.g., assignment copies a value rather than a reference).

  • A class fully supports inheritance (see Section 2.7). 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):

class Panda {
  string name;
  static string speciesName = "Ailuropoda melanoleuca";
  // Initializes Panda(see 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

attributes ? access-modifier ?
new?
static?
readonly?
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. A read-only field is always evaluated at runtime, not at compile time. To compile, a nonread-only field must be assigned in its declaration or within the type’s constructor (see Section 2.9.9). Nonread-only fields merely generate a warning when left unassigned.

Constants

attributes ? access-modifier ?
new?
const type [ constant-name = constant-expression ]+;
The type must be a predefined type of: 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.2831853071795862 * 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 1000.

Properties

attributes ? access-modifier ?
[override | new? [ virtual | abstract | static]? ]?
unsafe?
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 statement block with a semicolon. Also see Section 2.8.1.

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.

Tip

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

attributes ? access-modifier ?
[override | new? [virtual | abstract]?]?
unsafe?
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 statement block with a semicolon. Also see Section 2.8.1.

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.

Tip

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) {...}

Methods

Method declaration syntax:

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

Parameter list syntax:

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

Tip

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

All C# code executes in a method or 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 be passed 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;}
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;}
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 also removes ambiguity since parameter modifiers change the signature of a method (see Section 2.9.7.1).

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);
  }
  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 that 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) so long as the signatures are different. 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);
void Foo(out 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

The most common operators to overload are the == and != operators, which are used to implement value equality, as opposed to referential equality. If two objects have the same value, they are considered equivalent, even if they refer to objects in different memory locations.

In the following scenario you should override the virtual Equals method to route its functionality to the == operator. This allows a class to be later treated as one of its base classes (see Section 2.7.2), yet still be tested for value equality. It also provides compatibility with other .NET languages that don’t overload operators.

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;
  }
}
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(only accurate 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 ... {
  ...
  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 {
  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). 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

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

An instance of a 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:

class MyClass {
  public int x;
  public MyClass( ) : this(5) {}
  public MyClass(int v) {
    x = v;
  }
}
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 how 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 is 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

attributes ?
static class-name ( )
statement-block

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 only define 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 Test( ) {
    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, that 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) or declare or access indexers (see Section 2.9.6). A common use of the this variable is to unambiguate a field name from a parameter name:

class Dude {
  string name;
  public Test(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) 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 void new Introduce(Dude a) {
    base.TalkTo(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

attributes ?
~ class-name ( )
statement-block

C# classes can declare destructors. Declaring a C# destructor is simply a syntactic shortcut for declaring a Finalize method (known as a finalizer), and is expanded by the compiler into the following method declaration:

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

Although C# finalizers/destructors appear syntactically similar to C++ destructors, they have vastly different semantics, and it is critical to always be aware of the fact that a C# finalizer/destructor doesn’t behave the same as a C++ destructor.

For this reason we recommend that rather than declaring a C# destructor and relying on the compiler to generate a finalizer under the covers, you explicitly declare a Finalize method when needed.

Finalizers are class-only methods intended to assist in the cleanup of non-memory resources and are generally called by the garbage collector just before reclaiming unused memory.

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

Nested Types

A nested type is one 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, where an inner class has a reference to an instance of the enclosing class.

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

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