Members

Members are the programming elements and constructs that are contained in namespaces, classes, structs, and interfaces. Members are divided into three categories: functional, data, and type. Functional members are those that contain executable code, data members are constant or variable values, and type members are nested type declarations.

Member Types and Declaration Context

Table 5-11 contrasts the member types available in Java and C#. A Java developer will be familiar with many of the C# member types, but C# also adds some new ones. For the C# member types, we identify their valid declaration contexts.

Table 5-11. A Cross-Language Comparison of Member Types

  

C# Member Context

  

Java Member

C# Member

Namespace

Class

Struct

Interface

Functional Members

     

Constructor

Instance constructor

N/A

N/A

Instance initializer

N/A

N/A

N/A

N/A

N/A

Static initializer

Static constructor

N/A

N/A

Finalizer

Destructor

N/A

N/A

N/A

Method

Method

N/A

N/A

Property

N/A

N/A

Event

N/A

N/A

Indexer

N/A

N/A

Operator

N/A

N/A

Data Members

     

Constant

Constant

N/A

N/A

Field

Field

N/A

N/A

Type Members

     

Class

Class

N/A

Interface

Interface

N/A

N/A

Delegate

N/A

N/A

Struct

N/A

N/A

Enum

N/A

Versioning and Inheritance of Members

In Java, all methods not declared static or final are implicitly declared virtual. A derived class that implements a method with the same signature and return type as an inherited virtual method implicitly overrides that method. When a method is invoked on an object, the most overridden version of the method available to the runtime type of the object is used irrespective of the reference type used to refer to the object. This behavior is the basis of polymorphism.

This approach can cause problems in derived classes when releasing new versions of a base class:

  • If a new version of a base class introduces a method with the same signature and return type as a method already declared in a derived class, any attempts to invoke that method will result in the invocation of the overridden method in the derived class. This will almost certainly give results different from those intended by the base class developers. If the method is marked as final to avoid this, the derived class will fail to compile.

  • If a new version of the base class introduces a method with the same signature but different return type from a method already declared in a derived class, the derived class will fail to compile.

Although not common, these problems are more probable when deriving from third-party classes, where it’s not possible to coordinate versioning. To overcome these problems, C# offers two alternative approaches to member inheritance: overriding and hiding.

Overriding

Overriding is the same as the default behavior of Java; however, this is not the default C# behavior. The virtual modifier must be used to explicitly declare a member as virtual. When a derived class declares a new implementation of an inherited virtual member, the override modifier must be used to explicitly confirm the programmer’s intention to override the inherited member. If the override modifier is not used, the new member hides the inherited member, and a compiler warning occurs. If a derived class attempts to override a nonvirtual inherited member, a compiler error will occur.

Hiding

By default, members are not virtual. A derived class that implements a member with the same name and signature as a nonvirtual inherited member must use the new modifier to explicitly declare the programmer’s intention to hide the inherited member. Although hiding is the default behavior of C#, a compiler warning will occur if the new modifier is not used.

Hiding breaks the polymorphic behavior provided by virtual members. When invoking nonvirtual members, the type of variable used to reference an object determines which member gets invoked. No attempt is made to execute a more derived version of the member based on the runtime type of the object.

In Java, hiding is possible only with static methods, and not instance methods. As a Java developer learning C#, it can be frustrating having to remember to make members virtual and overridden, but the support for both overriding and hiding inherited members provided by C# gives a level of flexibility unavailable in Java.

New Virtual Members

It’s possible to use the new modifier in conjunction with the virtual modifier. This results in a new point of specialization in the inheritance hierarchy. Methods derived from the class implementing a new virtual member will inherit the virtual member. Classes further up the inheritance chain will still perceive the member to be nonvirtual.

Sealed Members

A member that is overriding an inherited virtual member can use the sealed modifier to ensure that no further derived classes can override the member. This is similar to the use of the final modifier in Java. However, the sealed modifier is valid only in conjunction with the override modifier. This means that the original declaration of a member cannot guarantee that it won’t be overridden. Only through marking an entire class as sealed can the programmer ensure that a member cannot be overridden.

Base Class Member Access

A derived class can access members in a superclass by using the base keyword. This has the same function and syntax as the super keyword in Java.

Static Members

C# static members are not accessible through instances of the containing type; accessing a static member is possible only through reference to its containing type.

Member Attributes

Attributes are applicable to the declaration of all member types. We’ll save detailed discussion of them until the Attributes section in Chapter 6.

Member Modifiers

Each member type can take a subset of the valid modifiers, depending on the context in which it’s declared. The valid modifiers for each member type are detailed in the member sections that follow. Details of each modifier type can be found in the Modifiers section in Chapter 4.

Constants

C# and Java constants represent a fixed value that is determined at compilation. However, a C# constant is a unique member type, whereas a Java constant is a field modified using the final modifier. In declaration and behavior, Java constants are more like C# readonly fields, which we’ll discuss in the Fields section coming up.

Declaration

Constant declarations have the following syntax:

[attributes] [modifiers] const type identifier = value;

More than one constant can be declared on a single line, each having the same type and accessibility but different values. For example:

public const int MyConstant = 60;
private const byte A = 5, B = 10, C = 20;
protected const string MyName = "Allen", HisName = null;
internal const MyClass X = null;
protected internal const int MyOtherConstant = MyConstant * 2;

Constants can be declared as any of the inbuilt simple types as well as string, enum, or any reference type. However, the only valid values for reference type constants are string literals and null.

A constant must be assigned a value at declaration, or a compile-time error will occur. No default values are assigned by the compiler. This value can depend on other constants as long as no circular reference is created.

Modifiers

The modifiers applicable to constants depend on the context of their declaration. Table 5-12 summarizes the available modifiers for each context.

Table 5-12. Constant Declaration Modifier Availability

 

Constant Declaration Context

 
 

Member of Class

Member of Struct

Accessibility

  

public

protected

N/A

private

internal

protected internal

N/A

Inheritance

  

new

N/A

abstract

N/A

N/A

sealed

N/A

(implicit)

virtual

N/A

N/A

override

N/A

N/A

Other

  

readonly

N/A

N/A

volatile

N/A

N/A

static

(implicit)

N/A

extern

N/A

N/A

Fields

Like Java fields, C# fields represent changeable, variable values and are used to hold object state.

Declaration

Field declarations have the following syntax:

[attributes] [modifiers] type identifier = value;

A field can be declared with any reference or value type. As with Java, it’s possible to declare more than one field per declaration. Each field will have the same type and accessibility. For example:

public int MyField;
public byte X = 5, Y, Z = 10;

As before, fields can be initialized at declaration. However, if a field is not initialized, the compiler will assign it a default value based on its type. Details of default values for each type can be found in the Types section earlier in this chapter.

Modifiers

The modifiers applicable to fields depend on the context of their declaration. Table 5-13 summarizes the available modifiers for each context.

Table 5-13. Field Declaration Modifier Availability

 

Field Declaration Context

 
 

Member of Class

Member of Struct

Accessibility

  

public

protected

N/A

private

internal

protected internal

N/A

Inheritance

  

new

N/A

abstract

N/A

N/A

sealed

N/A

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

  

readonly [a]

volatile [a]

static

extern

N/A

N/A

[a] readonly and volatile are mutually exclusive.

Fields as Struct Members

A compile-time error occurs if an attempt is made to assign an initial value to a field that is a member of a struct. For example:

public struct MyStruct {
    //This will cause a compile time error
    int MyInt = 6;
}

As long as the new keyword is used to instantiate a struct, all the member fields will be initialized to their default values. If the new keyword is not used, each field must be manually assigned a value before an attempt is made to use the fields.

Read-Only Fields

Fields marked with the readonly modifier are the C# equivalent of Java constants. Read-only fields can be assigned a value only at declaration or in a constructor appropriate to the type of field: instance or static. Because read-only fields are unchangeable once initialized, a compile-time error occurs if an attempt is made to pass a read-only field as an out or ref parameter outside the context of a constructor.

The use of both static and readonly modifiers provides a useful alternative to the C# constant in situations in which a type is needed that is not supported by the constant member type, or in which the value cannot be determined at compile time. In the following example, the anObject field cannot be a constant but can be read-only. Although the anInteger field could be a constant, we would have to assign it a value at declaration instead of waiting until the SomeOtherClass was instantiated.

public class SomeClass {
    // SomeClass implementation
}

public class SomeOtherClass {
    readonly SomeClass anObject;
    readonly int anInteger;

    public SomeOtherClass () {
        anObject = new SomeClass();
        anInteger = 5;
    }
}

Static and Instance Constructors

Constructors serve the same purpose in C# as they do in Java, although C# uses different syntax for calling superclass and overloaded constructors. C# also extends the use of constructors to the initialization of structs. More significantly, C# does not support instance initialization blocks, requiring that any instance initialization be carried out within the context of a constructor. With static initialization blocks, C# applies a more constructorlike syntax; this is discussed later in this section.

Declaration

Constructor declarations have the following form:

[attributes] [modifiers] identifier ([parameters])[:initializer([parameters])] {body}

The following Java code demonstrates a class named MyClass with a public constructor that passes control to a constructor of its superclass:

class MyClass extends MySuperClass{

    public MyClass(int x, String y) {
        super(x, y);
        //other constructor code
    }
}

Java constructors use the super keyword to pass control to a superclass constructor, which must be the first statement in the constructor. C# adopts a different syntax, using the base keyword to identify the superclass constructor to call, as highlighted in the following example:

class MyClass : MySuperClass {

    public MyClass(int x, string y): base(x, y) {
        //other constructor code
    }
}

The this keyword is used instead of base to call an overloaded constructor in the same class, as highlighted in the following example:

class MyClass : MySuperClass {

    public MyClass(string y): this(5, y) {
        //any other constructor code will be executed
        //after the call to the overloaded constructor
    }
    public MyClass(int x, string y): base(x, y) {
        //other constructor code will be executed after
        //the call to the superclass constructor
    }
}

Modifiers

The modifiers applicable to constructors depend on the context of their declaration. Table 5-14 summarizes the available modifiers for each context.

Table 5-14. Constructor Declaration Modifier Availability

 

Constructor Declaration Context

 
 

Member of Class

Member of Struct

Accessibility

  

public

protected

N/A

private

internal

protected internal

N/A

Inheritance

  

new

N/A

N/A

abstract

N/A

N/A

sealed

N/A

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

  

readonly

N/A

N/A

volatile

N/A

N/A

static

extern

Static Constructors

C# static constructors are the equivalent of Java static initialization blocks. The syntax of a C# static constructor is similar to that of an instance constructor, supporting attributes and the extern modifier. No other modifiers are supported, including access modifiers. For example:

public class MyClass {
    // This is a static constructor
    static MyClass() {
        // Static Constructor code goes here
    }
}

Only one static constructor can be defined per class; Java supports multiple static initialization blocks.

Destructors

Destructors are the C# equivalent of Java finalizers. To implement a finalizer in Java, a class overrides the java.lang.Object.Finalize method. In C#, a destructor is a method with a name equal to the class name, preceded by a tilde (~). Destructors are used by the garbage collector to allow the CLR to reclaim resources. The following example demonstrates the declaration of a destructor:

public class MyClass {
    // The following method is the destructor for MyClass
    ~MyClass() {
        // Destructor Code
    }
}

Although destructors are methods, the following limitations apply:

  • Classes can define only one destructor.

  • Structs cannot define destructors. Structs are allocated memory on the stack, not the managed heap. Stack memory is freed as soon as a struct goes out of scope; hence, the garbage collector is not involved.

  • A destructor never takes parameters or returns a value.

  • The only modifier applicable to a destructor is extern.

  • Destructors are not inherited by derived classes.

  • Destructors cannot be explicitly called; they can be invoked only by the garbage collector.

  • Destructors implicitly call the Finalize method on the object base class.

Warning

As in Java, destructors must be used with care and planning. The incorrect use of destructors can introduce unexpected behavior into an application. Destructors are covered in more detail in Appendix D.

IDisposable

As an alternative to destructors, C# provides the System.IDisposable interface for clearing up object resources without waiting for the automated garbage collection process to call a destructor. A class that implements IDisposable should correctly release any resources it holds when its Dispose method is called.

C# provides the using statement, which is shorthand syntax for acquiring and disposing of objects that implement the IDisposable interface. See the Statements section in Chapter 4 for complete details of the using statement.

Methods

The only difference between C# and Java is the additional types of parameters available.

Declaration

Method declarations have the following syntax:

[attributes] [modifiers] return-type identifier (parameter-list) {body}

For example, a protected static method named MyMethod that returns an integer and takes a string and a bool as parameters is declared as follows:

protected static int MyMethod (string s, bool b) {
    // implementation code
}

Modifiers

The modifiers applicable to methods depend on the context of their declaration. Table 5-15 summarizes the available modifiers for each context.

Table 5-15. Method Declaration Modifier Availability

 

Method Declaration Context

 
 

Member of Class

Member of Struct

Member of Interface

Accessibility

   

public

(implicit)

protected

N/A

N/A

private

N/A

internal

N/A

protected internal

N/A

N/A

Inheritance

   

new

N/A

abstract

N/A

(implicit)

sealed

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

extern

N/A

When applying modifiers to method declarations, the following restrictions apply:

  • The static, virtual, and override modifiers are mutually exclusive.

  • The new and override modifiers are mutually exclusive.

  • If the method is abstract, it cannot be static, virtual, or extern.

  • If the method is private, it cannot be virtual, override, or abstract.

  • The sealed modifier must be accompanied by override.

Parameters

All parameters are passed by value in Java. In addition to pass-by-value parameters, C# supports two new parameter types: reference and output parameters. C# also provides a mechanism called parameter arrays, which is used to pass a variable number of arguments to a method.We discuss parameter arrays in the following section. See the Variables section later in this chapter for a complete coverage of value, reference, and output parameters.

Parameter arrays

Parameter arrays are a feature with no direct parallel in Java. The use of a parameter array allows the caller of a method to provide a variable number of arguments when invoking a method; these are passed into the method in the form of an array.

A method is declared to use a parameter array using the params keyword as follows:

public void MyMethod(string someParam, params byte[] args) {
    // implementation code
}

As illustrated here, if a method takes multiple parameters, the parameter array must be the rightmost parameter declared. The params declaration must indicate the type of array that the method expects to receive; the array can be composed of simple or reference types. Within the body of the method, the params argument is processed as a normal array. The method just shown can be invoked in the following ways:

byte[] b = {5, 2, 6, 8} ;
string test = "hello" ;

MyMethod(test);           // call with no parameters
MyMethod(test, 3);        // call with a single parameter
MyMethod(test, 2,7,1);    // call with more than one parameter
MyMethod(test, b);        // call using an actual array

Events

Events are a formalization of the observer pattern, providing a generic mechanism through which a collection of registered listeners is notified when events occur. For example, program elements might need to know when a user clicks a button or closes a window. Events leverage delegates as the mechanism for event distribution. Objects interested in receiving event notifications register a delegate instance with the event. When triggered, the event invokes all registered delegates. We discussed delegates in the Types section earlier in this chapter.

Like delegates, events do not provide functionality that cannot be implemented in Java using the appropriate patterns and interfaces; the observer pattern is used extensively throughout the Java class library. However, events do provide a clean syntax, freeing the programmer from implementing listener management and event distribution mechanisms.

Declaration

An event declaration takes the following form:

[attributes] [modifiers] event type identifier [{access-declarations}];

The event type must be an already defined and accessible delegate type. The optional access-declarations element provides the functionality for adding and removing event listeners. If this element is omitted, the compiler provides a default implementation suitable for most purposes. We’ll discuss custom access-declarations later in this section.

Event invocation and usage

An event can be triggered only from within the type that declared it, irrespective of the accessibility modifiers applied to the event. An event is triggered as if invoking a delegate of the type used to declare the event. Triggering an event causes all delegate instances registered with the event to be invoked with the specified argument values. It makes no sense to trigger an event that has no registered listeners. An event will evaluate to null if it has no registered listeners. An if statement can be used to determine whether it’s necessary to trigger an event. To remove all registered listeners from an event, set the value of the event to null.

The following code demonstrates the use of events. The example defines a TempSource class that has a read-only property Temp for setting the current temperature. TempSource notifies all registered listeners when the temperature is set. We define a TempListener class to listen to temperature change events and output a message to the console in response.

using System;

// Declare a delegate for event notifications
public delegate void
    TemperatureEventHandler (string source, int temp);

// Declare the event source object
public class TempSource {
    private string Name;
    private int temp = 0;
    public event TemperatureEventHandler TemperatureChange;

    //Constructor takes a name for the Temperature Source
    public TempSource (string name) {
        Name = name;
    }

    // Declare a property to set the current temperature
    public int Temp {
        set {
            temp = value; // set the temperature

            // Raise the event if there are any listeners
            if (TemperatureChange != null) {
          TemperatureChange(Name, temp);
            }
        }
    }

    // Declare a method to remove all registered listeners
    public void Reset() {
        TemperatureChange = null;
    }
}

// Declare the event listener
public class TempListener {
    private string Name;

    // Constructor that takes Listener name and an array of source
    public TempListener(string name, params TempSource[] sources) {

        Name = name;

        // Register with each of the temperature sources
        foreach (TempSource t in sources) {
            t.TemperatureChange +=
                new TemperatureEventHandler(this.TempChanged);
        }
    }

    public void TempChanged(string src, int temp) {
        Console.WriteLine(Name + " : Temp is " + temp
            + " F in the " + src);
    }

    public static void Main() {
        TempSource g = new TempSource("garden");
        TempSource r = new TempSource("refrigerator");

        new TempListener("Listener1", new TempSource[] {g, r});
        new TempListener("Listener2", new TempSource[] {g, r});

        g.Temp = 34;
        r.Temp = 16;
    }
}

This example demonstrates multiple listeners registering with a single source as well as a listener receiving events from multiple sources. When run, the example produces the following output:

Listener1 : Temp is 34 F in the garden.
Listener2 : Temp is 34 F in the garden.
Listener1 : Temp is 16 F in the refrigerator.
Listener2 : Temp is 16 F in the refrigerator.

.NET implementation guidelines

The .NET class libraries use events extensively, especially with GUI components. Events can utilize any delegate, but .NET standardizes the delegate signature used with events and provides a concrete implementation in the System.EventHandler delegate with the following signature:

public delegate void EventHandler(object sender, EventArgs e);

The sender is a reference to the object that contains and raises the event; e is an object that derives from System.EventArgs. System.EventArgs contains no specialized functionality. An event that needs to distribute event data should derive an event-specific class from EventArgs with the additional functionality it requires.

Events and inheritance

Because events can be called only from within the context of the containing type, derived classes cannot trigger events in superclasses; protected methods must be declared to indirectly trigger the event where required.

Modifiers

The modifiers applicable to event declarations are dependent upon the context in which the delegate is declared, as summarized in Table 5-16.

Table 5-16. Event Declaration Modifier Availability

 

Event Declaration Context

 
 

Member of Class

Member of Struct

Member of Interface

Accessibility

   

public

(implicit)

protected

N/A

N/A

private

(default)

(default)

N/A

internal

N/A

protected internal

N/A

N/A

Inheritance

   

new

N/A

abstract

N/A

(implicit)

sealed

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

extern

N/A

Accessors

Event accessors provide the functionality called when the += or -= operator is used to add and remove event listeners. In most cases, the default implementations provided by the compiler are sufficient. If custom behavior is required, syntax similar to that used in a property is employed. However, the two methods are named add and remove. The following code fragment shows the syntax used:

public event SomeEventHandler SomeEvent {
    add {
        // Functionality to add event listener
    }
    remove {
        // Functionality to remove an event listener
    }
}

Both add and remove blocks can take attributes but do not take any modifiers or parameters. The only parameter available in add and remove is an implicit parameter named value that contains a reference to the delegate instance being added or removed.

Use of custom accessors means the programmer is responsible for implementing the necessary functionality to track registered listeners and also for using the registered delegates to raise events. It’s no longer possible to clear the event listeners by nulling the event, nor can the event be compared with null to determine whether there are any registered listeners.

Properties

Properties are a feature of C# that allows seemingly direct access to the state of a class or struct while still giving the control associated with providing access through methods. Properties provide a formalization of the getter/setter pattern. This pattern is a convention used extensively throughout Java class libraries and packages, although the Java language lacks a formal syntax to support or enforce the pattern.

To demonstrate the benefit of properties, consider an instance of a class Person that contains a member field to hold a person’s age. To avoid exposing a public age field, a common approach is to implement getAge and setAge methods to allow controlled manipulation of the field. These would be used as follows:

aPerson.setAge(34);
int age = aPerson.getAge();

C# properties allow this manipulation to take the following form:

aPerson.Age = 34;
int age = aPerson.Age;

The benefits become more apparent when using the property in conjunction with operators, for example:

aPerson.Age += 5;
aPerson.Age++;

To the code consumer, it’s as though there is a directly accessible member field named Age. In reality, there is no field named Age: the code is calling special accessor methods to interact with the object state.

The use of properties provides clean and intuitive code, but for a Java developer this can be confusing. It’s often hard to determine the difference between working with a public field and working with a property.

Declaration

Properties provide a means to manipulate object state but are not stateful mechanisms themselves; something else, often a field member, is the target of the manipulation. The property declaration imposes no requirements on the containing type as to what the stateful item is, or is called.

Property declarations have the following syntax:

[attributes] [modifiers] type identifier {accessor-declaration}

The property type can be any value or reference type and specifies both the data type that can be assigned to the property and the type returned when the property itself is assigned to something else.

Here’s a Java implementation of the getter/setter pattern for the Person class discussed previously:

public class Person {

    private int thePersonsAge;

    public void setAge(int p_age) {
        // Do some validation
        thePersonsAge = p_age;
    }

    public int getAge() {
        return thePersonsAge;
    }
}

The equivalent C# implementation using properties is

public class Person {
    private int thePersonsAge;

    public int Age {
        get {
            return thePersonsAge;
        }
        set {
            thePersonsAge = value;
        }
    }
}

The get and set accessors in this example are simplistic and provide no additional benefit compared with making the thePersonsAge field public; however, as function members, properties can contain any executable code.

The undeclared variable value used in the set accessor is implicitly provided by the compiler, containing the value the caller has assigned to the property. The type is the same as that of the property declaration, in this example an int.

It’s possible to implement only the get or the set accessor, thus providing a read-only or write-only property. However, if the intention is to use the prefix or postfix increment (++) or decrement (--) operator, or any of the compound assignment operators (+=, – =, and so on), both the get and set accessors must be declared. Attempting any action on a property that requires an accessor that has not been implemented will result in a compiler error being raised.

Modifiers

The modifiers applicable to properties depend on the context of their declaration. Table 5-17 summarizes the available modifiers for each context.

Table 5-17. Property Declaration Modifier Availability

 

Property Declaration Context

 
 

Member of Class

Member of Struct

Member of Interface

Accessibility

   

public

(implicit)

protected

N/A

N/A

private

N/A

internal

N/A

protected internal

N/A

N/A

Inheritance

   

new

N/A

abstract

N/A

N/A

sealed

N/A

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

extern [a]

N/A

[a] When the extern modifier is used, the get and set accessor bodies should be empty statements.

Properties as Members of Interfaces

A property can be a member of an interface. The structure of the property declaration is the same as discussed earlier in this section; however, the body of the get and set accessors is an empty statement. If only one of the accessors is required, specify this in the interface. For example:

public interface IPerson {
    int Age {get; set;}
    string PayrollNumber {get;}    // No set accessor required.
}

Indexers

Sometimes it makes sense to access a class or struct by using index syntax, similar to that used with arrays. This is particularly appropriate if the class contains some collection of related information. C# provides the indexer member type for achieving this functionality.

Instead of making field members directly accessible or defining a series of methods for manipulating the underlying data, indexers provide indirect access to state using the familiar array-style index. Like properties, the indexer provides indirect access to the data via definable get or set methods. The index can be any value or reference type.

The .NET class libraries make extensive use of indexers to provide access to collections. See Chapter 9 for complete details.

Declaration

Indexers are similar to properties in that they provide a means to manipulate object state but are not stateful mechanisms themselves. As with properties, the containing class or struct must implement some other program element as the target of the manipulation. The indexer imposes no requirements as to how this is done.

Index declarations have the following syntax:

[attributes] [modifiers] type this [parameters] {accessor-declarations}

Use of an indexer is never performed explicitly through a member name, so indexers do not have identifiers. As a consequence of an unfortunate syntax choice, indexers are declared using the keyword this.

The indexer type can be any value or reference type and specifies both the data type that can be assigned to an indexer element and the type of the indexer element returned when assigned to something else. At least one parameter must be specified in the indexer declaration. Multiple parameters result in a multidimensional index, as in the case of a multidimensional array. For example:

public string this [int index1, byte index2, string index3] {...}

Parameters can be of any value or reference type, but it is not valid to use the ref or out modifier.

Multiple indexers can be defined for a single class. Given that all indexers are declared using the this keyword, additional indexers must be differentiated with different parameter types.

The following example demonstrates the declaration of an indexer:

public class TopTenArtists {

    private string[] artists = new string[10];

    public string this [int index] {
        get {
            if (index > 0 && index < 11) {
                return artists[index-1];
            } else {
                return null;
            }
        }
        set {
            if (index > 0 && index < 11) {
                artists[index-1] = value;
            }
        }
    }
}

The undeclared variable value used in the set accessor is implicitly provided by the compiler. It contains the value the caller is assigning to the property. Its type is the same as that of the property declaration—in this instance, an int.

The following code demonstrates how the indexer of the TopTenArtists class can be used:

public static void Main() {

    TopTenArtists artists = new TopTenArtists();

    artists[1] = "Rubens";
    artists[2] = "Gainsborough";
    artists[3] = "Yevseeva";


    for (int x = 1; x < 11; x++) {
        System.Console.WriteLine("Artist {0} is {1}", x, artists[x]);
    }
}

This example produces the following output:

Artist 1 is Rubens
Artist 2 is Gainsborough
Artist 3 is Yevseeva
Artist 4 is
Artist 5 is
Artist 6 is
Artist 7 is
Artist 8 is
Artist 9 is
Artist 10 is

As was the case with properties, indexers can implement only the get or set accessor; this provides a read-only or write-only member. As with properties, if the intention is to use the prefix or postfix increment or decrement operators, or any of the compound assignment operators, both the get and set accessors must be declared.

Modifiers

The modifiers applicable to indexers depend on the context of their declaration. Table 5-18 summarizes the available modifiers for each context.

Table 5-18. Indexer Declaration Modifier Availability

 

Indexer Declaration Context

 
 

Member of Class

Member of Struct

Member of Interface

Accessibility

   

public

(implicit)

protected

N/A

N/A

private

N/A

internal

N/A

protected internal

N/A

N/A

Inheritance

   

new

N/A

abstract

N/A

N/A

sealed

N/A

N/A

virtual

N/A

N/A

override

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static [a]

N/A

N/A

N/A

extern [b]

N/A

[a] Static indexers are not permitted.

[b] When the extern modifier is used, the get and set accessor bodies should be empty statements.

Indexers as Members of Interfaces

Indexers can be declared in interfaces. As with properties, the get and set accessors must be specified as an empty statement. To implement a read-only or write-only indexer, simply omit the accessor that is not required. When implementing the interface, explicit interface implementation must be used to identify the indexer implementation. For example:

public interface IMyInterface {
    string this [int index] {get; set;}
}

public class MyClass : IMyInterface {
    string IMyInterface.this [int index] {
        get {
            // implementation
        }
        set {
            // implementation
        }
    }
}

The problem with this approach is that the indexer cannot be accessed through an instance of the class, only through an instance of the interface. For example:

// This is a compile-time error
MyClass a = new MyClass();
string b = a[1];

// The following statements are valid
MyClass p = new MyClass();
string q = ((IMyInterface)p)[1];   // Cast to an interface instance

IMyInterface x = new MyClass();
string y = x[1];

Operators

In this section, we’ll consistently refer to this member type as an operator member to avoid confusion with regular operators such as + and !=. The operator member enables a programmer to specify the behavior when an instance of a class or struct is used in either of the following ways:

  • Is used in conjunction with an operator such as + or !=. This is called operator overloading.

  • Is implicitly or explicitly converted to another type, known as custom conversion.

Operator members provide syntactic convenience resulting in clean and logical code when used correctly; however, when used inappropriately or in idiosyncratic ways, operator members result in code that is difficult to understand and types that are difficult to use. For example, it makes sense for a class CalendarDay that represents a calendar day to support the ++ operator to increment the day being represented, but use of the + operator to add two CalendarDay instances together is not logical and constitutes confusing behavior.

Declaration

The declaration of an operator member takes the following general form:

[attributes] [modifiers] operator operator-token (parameters) {body}

The type of operator member determines the specific declaration syntax required, but there are a number of restrictions common to all operator members:

  • Operator members are always associated with types and must be public and static.

  • The only optional modifier is extern, in which case the body of the operator member must be an empty statement.

  • Arguments to operator members cannot be ref or out parameters.

  • Derived classes inherit operator members but cannot override or hide them.

  • Operator members cannot return void.

There are three categories of operator members: unary, binary, and conversion. We discuss each of these in the following sections.

Unary operators

Unary operator members allow the following operators to be overloaded: +, -, !, ~, ++, --, true, and false. The true and false operators must be implemented as a pair, or a compiler error will occur. Unary operator members take a single argument of the type the operator member is declared in. The return type depends on the overloaded operator. Table 5-19 summarizes the return type for each operator.

Table 5-19. Unary Operator Return Types

Operator

Return Type

+, -, !, ~

Any

++, --

The type containing the operator member

true, false

bool

The following example demonstrates the syntax used to declare a variety of unary operators as members of a class named SomeClass. We have highlighted the operator keywords and tokens for clarity.

public class SomeClass {

    // +, -, !, and ~ operators can return any type
    public static int operator +(SomeClass x) {/*...*/}
    public static SomeOtherClass operator ~(SomeClass x) {/*...*/}

    // ++ and –- operators must return an instance of SomeClass
    public static SomeClass operator ++(SomeClass x) {/*...*/}

    // true and false operators must be declared as a pair
    // and return bool
    public static bool operator true(SomeClass x) {/*...*/}
    public static bool operator false(SomeClass x) {/*...*/}
}

Given the declaration of SomeClass, the following statements are valid:

SomeClass x = new SomeClass();    // Instantiate a new SomeClass
SomeClass y = +x;                 // Unary + operator
SomeOtherClass z = ~x;            // Bitwise compliment operator

while (x) {                       // true operator
    x++;                          // postfix increment operator
}

Binary operators

Binary operator members allow the following operators to be overloaded: +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, and <=.

The following binary operators must be overloaded in pairs:

  • == and !=

  • > and <

  • >= and <=

Binary operator declarations must specify two arguments, one of which must be the type that the operator member is declared in. The order of the arguments must match the order of operands when the overloaded operator is used. The return value can be of any type. Paired operator declarations must have the same argument types and order.

When a binary operator is overloaded, the corresponding compound assignment operator (if any) is implicitly overloaded. For example, overloading the * operator implicitly overloads the *= operator. If a type overloads the == or != operator, a compiler warning is generated if the type does not override the inherited Object.Equals and Object.GetHashCode methods. Although the && and || operators cannot be directly overloaded, they are supported through the implementation of the & and | operators. Using the && operator as an example, the & operator must be overloaded, taking two arguments of the type in which the operator member is declared and returning the same type.

The following example demonstrates a number of binary operator member declarations as members of a struct named SomeStruct. We have highlighted the operator keywords and tokens for clarity.

public struct SomeStruct {

    // The == and != operators must be declared in pairs
    // The same arguments types and order must be used
    public static bool operator == (SomeStruct x, int y) {/*...*/}
    public static bool operator != (SomeStruct x, int y) {/*...*/}

    // Binary + operator causes += to be implicitly overloaded
    public static SomeStruct operator + (SomeStruct x, long y) {/*...*/}

    // Binary & operator declared to support usage with "&&" statements
    public static SomeStruct operator & (
        SomeStruct x, SomeStruct y) {/*...*/}

}

Given the declaration of SomeStruct, the following code fragments are valid:

SomeStruct x = new SomeStruct();    // Instantiate a new SomeStruct
SomeStruct y = new SomeStruct();    // Instantiate a new SomeStruct

if (x && y) {   // correctly implemented "&" operator supports this
    x += 45 ;   // implicitly provided by overloading "+" operator
}

Conversion operators

Conversion operator members allow the programmer to define the logic used to convert an instance of one type (the source) to an instance of another (the target). The compiler will use available conversion operators to perform implicit and explicit data type conversions. See the Types section earlier in this chapter for details of implicit and explicit conversions.

Conversion operator declarations include the keyword implicit or explicit. This determines where the compiler will use the conversion operator. Conversion operator members take a single parameter and return a value of any type. The parameter is the source type and the return value is the target type of the conversion. For example, the declaration of an implicit conversion operator to convert a string to an instance of SomeClass is as follows:

public static implicit operator SomeClass(String x) {/*...*/}

The declaration of an explicit conversion operator to convert an instance of SomeStruct to an int is as follows:

public static explicit operator int (SomeStruct x) {/*...*/}

The following restrictions apply to conversion operators:

  • Either the return type or the argument must be the type in which the conversion operator is declared.

  • It is not possible to declare a conversion operator to redefine an already existing conversion, meaning that it is not possible to define both an explicit and an implicit conversion operator for the same types.

  • A conversion operator must convert between different types.

  • Conversion to or from interface types is not possible.

  • No inheritance relationship can exist between the target and source types.

Cross-Language Considerations

Many languages do not support operator overloading, and it is not part of the Common Language Specification (CLS). To provide cross-language support, it’s necessary to provide method alternatives to any overloaded operators. The CLS provides recommendations on appropriate names for each operator type.

Nested Types

As with Java, C# supports nested type declarations. Full details can be found in the Types section earlier in this chapter.

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

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