only for RuBoard - do not distribute or recompile |
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.
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).
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.
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 }
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.
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 } }
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.
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; }
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.
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.
|
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.
|
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).
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.
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.
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).
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); } }
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 } }
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
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.
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.
|
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 } }
The C# compiler enforces the rule that operators that are logical pairs must both be defined. These operators are == and !=; < and >; and <= and >=.
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
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"); } }
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 +).
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.
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 }
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.
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.
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"); } }
Consistent with instance constructors, static constructors respect the inheritance chain, so each static constructor from the least derived to the most derived is called.
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.
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( );
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.
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); } }
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"); } }
|
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.
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 }
|
only for RuBoard - do not distribute or recompile |