Chapter 5. Data Types

This chapter discusses the data types provided by C#. Although C# maintains strong parallels with the data types offered by Java, the designers of C# have also drawn heavily on the features of C and C++. For the Java programmer, C# data types include many subtle and confusing differences as well as some new features to learn.

We begin with a broad discussion of the unified type system provided by the Microsoft .NET Framework and the fundamental division of data types into value and reference types. We then cover each of the data types in detail, describing how and when each type can be used and the functionality it provides. Next we detail each of the member types that can be used within interfaces, classes, and structs. Many of these member types will be familiar to the Java developer, but C# also defines new member types that provide clean and powerful language-based implementations of common programming models. Finally we discuss the types of variables in C# and in particular the features for passing variables by reference instead of value.

Types

Java has primitive and reference types; C# refers to them as value and reference types. Value types include byte, int, long, float, and double. Reference types include class, interface, and array. C# has a third type category, the infamous pointer. Java does not provide a pointer type, primarily because of the complexity of these types and the dangers they pose to application stability when used incorrectly. Pointers are discussed in the context of unsafe code in Chapter 6.

Despite the apparent similarities, a closer inspection reveals fundamental differences in the C# type system:

  • The common language runtime (CLR) unified type system extends object capabilities to value types.

  • A richer set of inbuilt value types provides additional flexibility.

  • The introduction of the struct data type provides stack-based objects.

  • The delegate reference type provides a safe, object-oriented approach to method pointers.

  • Enumerations provide a mechanism to group and name a related set of constant values.

Apart from the pointer, we’ll discuss all C# types in the following sections.

More Info

Attributes are applicable to the declaration of all types. However, we won’t discuss attributes in this chapter, saving them instead for a detailed discussion in Chapter 6.

Unified Type System

The Microsoft .NET Framework considers every data type to be ultimately derived from System.Object. This gives all value types, including primitives such as int and long, object capabilities. This has two consequences:

  • Methods can be invoked against value types. This is more important for structs, which can implement new function members.

  • Value types can be passed as object and interface references.

To understand the significance of the unified type system, it’s important to understand the traditional differences between value and reference types. These are summarized in Table 5-1.

Table 5-1. The Difference Between Value and Reference Types

Characteristic

Value Types

Reference Types

Memory allocation

Stack. However, value-type members of heap-based objects are stored inline, meaning that they are allocated memory on the heap within the containing object.

Heap

Contents

Data.

Reference to data

Disposal

Immediately as they leave scope.

Garbage collected

In Java, the benefit of value types stems from their simplicity relative to objects, resulting in performance benefits and memory savings compared with the alternative of implementing everything as objects.

The CLR maintains speed and memory savings by treating value types as objects only when required, minimizing the impact of providing value types with object capabilities. When a value type is used as an object or cast to an interface type, a process called boxing is used to automatically convert the value type to a reference type. Boxing and its counterpart, unboxing, provide the run-time bridge between value and reference types. The net effect is similar to that of using the wrapper classes in Java, such as the Integer and Double classes, but in C#, the CLR takes care of the details automatically.

Boxing

Boxing takes an instance of a value type and converts it to an object or interface type. For example:

// Box an int variable
int myInt = 100;
object myIntObject = myInt;
System.Console.WriteLine("myIntObject = " + myInt.ToString());

// Box a long literal
object myLongObject = 4500L;

This example uses simple data types, but the same syntax also works for boxing structs. Structs can also be boxed into instances of interfaces they implement. For example, boxing a struct named MyStruct that implements the interface ISomeInterface is achieved as follows:

MyStruct x = new MyStruct();
ISomeInterface y = x;

The runtime implements boxing by instantiating a container object of the appropriate type and copying the data from the value type into it. It’s important to understand that the boxed instance contains a copy of the source value. Any changes made to the original value are not reflected in the boxed instance.

Implicit boxing

The C# compiler will implicitly box value types as required—for example, invoking a function member of a struct or passing a value type where an object is expected. Given the overheads associated with boxing, overuse can affect program performance. Where performance is an issue, you should write programs to avoid the unnecessary use of implicit boxing.

Unboxing

Unboxing is the reverse of boxing. It takes an object representing a previously boxed value and re-creates a value type from it. For example:

// Box an int variable
int myInt = 100;
object myIntObject = myInt;
System.Console.WriteLine("myIntObject = " + myInt.ToString());

// Unbox
int myOtherInt = (int)myIntObject;

As can be seen in this example, the previously boxed object must be explicitly cast to the appropriate type. The runtime checks that the boxed instance is being unboxed as the correct type; otherwise, it throws a System.InvalidCastException.

It isn’t possible to create a value type representation of any reference type using unboxing; unboxing works only on objects that contain previously boxed values.

Value Types

All value types in C# are of type struct or enum. Both struct and enum are types that were not implemented in Java, an omission that is frequently debated by language theorists. A set of inbuilt value types in C#, referred to as simple types, provides the same functionality as primitive types (int, long, float, and so forth) in Java, but their implementation is very different. All .NET inbuilt value types are implemented as structs; this is essential to enabling the boxing of inbuilt value types.

Structs

A struct is a data structure that is similar to a class. The most important differences between classes and structs are a consequence of structs being a value type:

  • When instantiated, a struct is allocated memory on the stack or inline if it’s a member of a heap resident object, such as a class.

  • Memory allocated to a struct instance contains the member data, not references to the data.

  • Struct instances are disposed of as soon as they lose scope; they are not garbage collected.

Declaration

A struct declaration takes the following form:

[attributes] [modifiers] struct identifier : interfaces {body}

Apart from the fact that structs do not support inheritance (discussed in the next section), structs are declared the same way as classes. For example, a public struct named MyStruct that implements the IMyInterface interface is declared as follows:

public struct MyStruct : IMyInterface {
    // function and data members
}

Inheritance

Structs do not support user-defined inheritance. However, all structs implicitly derive from System.ValueType, which is in turn derived from System.Object.

Modifiers

The applicability of modifiers to a struct declaration depends on the context in which the struct is declared. Structs can be declared as top-level types (that is, direct members of an enclosing namespace) or nested within the definition of another struct or class. Table 5-2 summarizes modifier availability.

Table 5-2. Struct Declaration Modifier Availability

 

Struct Declaration Context

 
 

Member of Namespace

Member of Class

Member of Struct

Accessibility

   

public

protected

N/A

N/A

private

N/A

(default)

(default)

internal

(default)

protected internal

N/A

N/A

Inheritance

   

new

N/A

N/A

abstract

N/A

N/A

N/A

sealed

(implicit)

(implicit)

(implicit)

virtual

N/A

N/A

N/A

override

N/A

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

N/A

N/A

extern

N/A

N/A

N/A

Empty constructors and structs

An empty constructor is one that takes no parameters. Although it’s valid to define an empty constructor for a class, it’s not valid to define one for a struct. The compiler implicitly defines an empty constructor for a struct, the body of which sets all of the struct members to their default values. This means that it’s impossible to stop the instantiation of a struct using a private empty constructor. If the accessibility of the struct makes it visible, it can always be instantiated.

Instantiation

Unlike classes, struct instances are allocated memory as soon they are declared; the new keyword is not required. However, if the new keyword is not used, all field members of the struct must be explicitly assigned values prior to use; otherwise, a compiler error occurs. If the new keyword is used, the field members of the struct will be initialized to their default values. Struct variables can never be assigned the value null.

Members

Structs can contain the following member types: constant, field, method, property, event, indexer, operator, instance constructor, static constructor, and nested type declarations. Structs cannot contain destructors, as stack-based object structs are not subject to the garbage collection process.

More Info

For comprehensive coverage of these member types, see the Members section later in this chapter.

Assigning and passing structs

If a struct instance is passed as a function member parameter, returned from a function member, or assigned to a variable, a copy of the complete struct will be created. The copy is independent of the original struct; any changes made to the content of the new struct will not be reflected in the original.

Should the need arise to pass a struct reference, use the ref or out modifier on the parameter. The need to use ref may indicate that a class would be a more appropriate alternative. See the Variables section later in this chapter for full details of the ref and out parameter modifiers.

Issues with structs

Thought should be given to how a data structure will be used before deciding whether to implement a class or a struct. The decision can affect the performance of an application. There are no definitive rules; only guidelines can be offered:

  • As a general rule, small and simple favors structs. The larger and more complex a data structure is, the more likely it should be implemented as a class.

  • When working in a resource-constrained environment where memory needs to be freed quickly, structs will provide a benefit over the nondeterministic garbage collection of classes. Because structs are allocated on the stack, their memory is freed as soon as they go out of scope.

  • If speed is of paramount importance, efficiency will be gained from the stack-based nature of structs.

  • Frequently passing a struct as a parameter or assigning structs to variables can be expensive; bear in mind that a complete copy of the contents of that struct is created each time and is more costly than copying an object reference.

  • Frequent method calls are better served by classes; the overhead of implicit boxing can forfeit benefits gained from the fact that structs are stack-based.

  • When a data structure will be used inside a containing structure, the approach used by the container to manage its contents can dramatically affect overall performance. In a collection, a struct will be repeatedly boxed as the Equals or GetHashCode method is called. This will cause a significant performance overhead. In an array, a struct can provide higher efficiency than a class, as there is no need to look up references.

Simple Types

C# provides a rich selection of predefined value types that are collectively known as simple types, including types equivalent to the primitive types available in Java. C# also provides unsigned versions of all the integer types, as well as a new type called decimal. The decimal is a high-precision fixed-point value type designed for use in calculations in which the rounding issues associated with floating-point arithmetic are problematic.

Each of the predefined value types is a struct implemented in the System namespace of the .NET class library. For convenience, C# provides keywords to reference these simple types. Either the keyword or the struct name can be used in a program.

Table 5-3 details the predefined simple type keywords, the Java equivalents, the structs that the keywords are aliases for, the range of values supported by each type, and the default values.

Table 5-3. C# Simple Data Types

Java Type

C# Keyword

.NET Struct

Values

Default Value

boolean

bool

System.Boolean

true or false

False

byte

sbyte

System.SByte

8-bit signed integer (-128 to 127)

0

N/A

byte

System.Byte

8-bit unsigned integer (0 to 255)

0

short

short

System.Int16

16-bit signed integer (-32768 to 32767)

0

N/A

ushort

System.UInt16

16-bit unsigned integer (0 to 65535)

0

int

int

System.Int32

32-bit signed integer (-2147483648 to 2147483647)

0

N/A

uint

System.UInt32

32-bit unsigned integer (0 to 4294967295)

0

long

long

System.Int64

64-bit signed integer (-9223372036854775808 to 9223372036854775807)

0

N/A

ulong

System.UInt64

64-bit unsigned integer (0 to 18446744073709551615)

0

float

float

System.Single

32-bit double-precision floating-point

0

double

double

System.Double

64-bit double-precision floating-point

0

N/A

decimal

System.Decimal

128-bit high-precision decimal number with 28 significant digits

u0000

char

char

System.Char

2-byte Unicode

0

Enumerations

An enum (short for enumeration) is a data type that declares a set of named integer constants. Enums are data structures that have no direct equivalent in Java; the closest Java alternative is a set of individually defined constant values. However, enums offer advantages over the use of constants:

  • Using enum types as parameters in function members restricts the range of valid values that can be passed during calls.

  • There is a logical connection between the member values of an enum as opposed to discrete constants.

Declaration

An enum declaration has the form

[attributes] [modifiers] enum identifier [:base] {body}

The base component of the enum declaration specifies the underlying type of the enum and can be any of the simple integer types; the default is int. The base type limits the range of values that can be assigned to the enum members.

The body of the enum declaration contains a list of member names with optional values. Member names must be unique, but multiple members can have the same value. By default, the first member will have the integer value 0 and subsequent members will be assigned values sequentially.

Using explicit numbering, the programmer can assign an integer value to each enum member. The assigned value must be valid for the specified base type. Any members without explicit values are assigned a value sequentially based on the value of the previous member.

The following example declares an enum named PokerChip with a byte base type:

public enum PokerChip : byte {

    Blue = 1,
    Green,            // Automatically assigned the value 2 (Blue + 1)
    Red = 5,          // Explicitly assigned the value 5
    Orange = 10,
    Brown = 25,
    Silver = 50,
    Gold = Silver * 2, // Any determinable constant is acceptable
    Min = Blue,        // Multiple members with the same value are ok
    Max = Gold
}

The following example demonstrates the PokerChip enum used as a method argument and a switch statement expression:

public void PlaceBet(PokerChip chip) {
    switch (chip) {
        case PokerChip.Blue:
            //Do something
            break;
        case PokerChip.Green:
            //Do something else
            break;
        default:
            //Do something else
            break;
    }
}

As shown in the PlaceBet method, function members can specify enum types as arguments. Using an enum type argument restricts the range of values that are valid for the argument. However, the integer constant 0 (zero) is implicitly convertible to any enum type and can always be passed in place of a valid enum member.

Modifiers

Enums can be declared as top-level structures or contained within a class or struct. The modifiers available depend on the context of the enum declaration. Table 5-4 summarizes the available modifiers for each context.

Table 5-4. Enum Declaration Modifier Availability

 

Enum Declaration Context

  
 

Member of Namespace

Member of Class

Member of Struct

Accessibility

   

public

protected

N/A

N/A

private

N/A

(default)

(default)

internal

(default)

protected internal

N/A

N/A

Inheritance

   

new

N/A

N/A

abstract

N/A

N/A

N/A

sealed

(implicit)

(implicit)

(implicit)

virtual

N/A

N/A

N/A

override

N/A

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

N/A

N/A

extern

N/A

N/A

N/A

Members and inheritance

The user-defined members of an enum are restricted to the set of public name/value pairs contained within the enum. Enums do not support user-defined inheritance, but all enums implicitly derive from System.Enum, which derives from System.ValueType and ultimately from System.Object. The System.Enum class provides a number of static methods for working with enum member names and values, summarized in Table 5-5.

Table 5-5. System.Enum Methods

Method

Description

GetName()

Gets the name of the member with the specified value in an enum.

GetNames()

Gets an array of the member names contained within an enum.

GetUnderlyingType()

Gets the underlying base type of the enum.

GetValues()

Gets an array of the member values contained within an enum.

IsDefined()

Determines whether the enum has a member with the specified value.

Reference Types

In C#, reference types include class, interface, array, and delegate. With the exception of delegate, Java developers will be familiar with all of these. We discuss each reference type in detail in the following sections.

Classes

The implementation of classes in C# mirrors that of Java. Both are reference types, supporting single implementation inheritance and multiple interface implementations. The few differences that do exist are predominantly syntax changes or are required to support the modifiers and member types introduced by C#. Only Java anonymous and local classes have no C# equivalent.

Declaration

A C# class declaration takes the following form:

[attributes] [modifiers] class identifier [:superclass] [interfaces] {body}

The Java keywords extends and implements are not used. C# uses the colon to separate the class name from its superclasses and interfaces. Commas are used to separate the superclass and each subsequent interface name; the superclass must come first, or a compiler error will occur.

For example, the following is the C# code to declare a public class named MyClass that inherits from the superclass MyBaseClass and implements two interfaces named IAnInterface and IAnotherInterface:

public MyClass : MyBaseClass, IAnInterface, IAnotherInterface {
    // Implementation code
}

Modifiers

The applicability of modifiers to class declarations depends on the context in which the class is declared. Classes can be declared as a top-level type, being direct members of an enclosing namespace, or they can be nested within the definition of a class or struct. Table 5-6 summarizes modifier availability for classes.

Table 5-6. Class Declaration Modifier Availability

 

Class Declaration Context

  
 

Member of Namespace

Member of Class

Member of Struct

Accessibility

   

public

protected

N/A

N/A

private

N/A

(default)

(default)

internal

(default)

protected internal

N/A

N/A

Inheritance

   

new

N/A

N/A

abstract

sealed

virtual

N/A

N/A

N/A

override

N/A

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

N/A

N/A

extern

N/A

N/A

N/A

Members

Classes can contain the following member types: constant, field, method, property, event, indexer, operator, instance constructor, static constructor, and nested type declarations. For comprehensive coverage of member types, see the Members section later in this chapter.

Abstract classes

The implementation of interface members represents an important difference in the default behavior of how abstract classes are handled in Java and C#. In Java, any interface members that are not implemented simply become abstract members that concrete classes must implement. C# abstract classes must provide implementations for all interface members, but the abstract class may declare the member as abstract. For example:

public interface MyInterface {
    int MyMethod(int x);
}

public abstract class MyClass : MyInterface {
    // Compile time error if the following declaration is missing
    public abstract int MyMethod(int x);
}

Default constructor accessibility

The C# compiler, like the Java compiler, will provide a default constructor if one is not explicitly defined for a class. The accessibility Java assigns to this default constructor is the same as the accessibility of the containing class; C# defines the default constructor as protected if the class is abstract, otherwise as public.

Preventing class instantiation

Both Java and C# offer the same capabilities for creating concrete classes that cannot be instantiated. Declaring at least one constructor ensures that the compiler does not generate a default constructor. Declaring this constructor private renders it inaccessible to other code, so the class cannot be instantiated.

Anonymous and local classes

It’s not possible to define anonymous or local classes in C#; all classes must be explicitly defined before use. Java anonymous classes are regularly used for event handling, especially with GUI components. See the Events section later in this chapter for details of event handling in C#.

Interfaces

Interfaces serve the same purpose in C# as Java. There are some differences in the declaration syntax, but the primary difference is that constants cannot be declared within the context of a C# interface.

Declaration

A C# interface declaration takes the following form:

[attributes] [modifiers] interface identifier [:superinterfaces] {body}

For example, the C# code to declare a public interface named MyInterface that extends the superinterfaces IAnInterface and IAnotherInterface follows:

public MyInterface : IAnInterface, IAnotherInterface {
    // member declarations
}

C# uses a colon followed by a comma-separated list of superinterfaces to specify inheritance.

Modifiers

The applicability of modifiers to an interface declaration depends on the context in which the interface is declared. Interfaces can be declared as a top-level type, being direct members of an enclosing namespace, or they can be nested within the definition of a class or struct. Table 5-7 summarizes modifier availability.

Table 5-7. Interface Declaration Modifier Availability

 

Interface Declaration Context

 
 

Member of Namespace

Member of Class

Member of Struct

Accessibility

   

public

protected

N/A

N/A

private

N/A

(default)

(default)

internal

(default)

protected internal

N/A

N/A

Inheritance

   

new

N/A

N/A

abstract

N/A

N/A

N/A

sealed

virtual

N/A

N/A

N/A

override

N/A

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

N/A

N/A

extern

N/A

N/A

N/A

Members

Interfaces can contain the following member types: method, property, event, and indexer. Aside from support for the member types introduced by C#, the major difference from Java is that constants cannot be declared in interfaces. The alternative provided by C# is to use a peer-level enum; however, this does not keep associated constants together with the interface.

More Info

For comprehensive coverage of the members applicable in the context of an interface declaration, see the Members section later in this chapter.

Implementing interfaces

C# classes and structs implement interfaces using the same syntax. The following example shows a struct MyStruct and a class MyClass both declaring the implementation of two interfaces IAnInterface and IAnotherInterface:

public struct MyStruct :IAnInterface, IAnotherInterface {
    // Implementation code
}

public class MyClass :IAnInterface, IAnotherInterface {
    // Implementation code
}

Explicit interface implementation

C# includes a feature called explicit interface implementation, which gives greater control over interface member implementations. Explicit interface implementation is most commonly used when implementing multiple interfaces that contain members with conflicting names or signatures. The only mandatory use of explicit interface implementation is when implementing an indexer declared in an interface.

The following is an example of a class implementing two interfaces with conflicting member declarations. Explicit interface implementation is used to differentiate between the implementation of the interface methods.

 public interface IMyInterface {
    void SomeMethod();
}

public interface IMyOtherInterface {
    void SomeMethod();
}

public class MyClass : IMyInterface, IMyOtherInterface {
    void IMyInterface.SomeMethod() {
        // Implementation Code
    }
    void IMyOtherInterface.SomeMethod() {
        // Implementation Code
    }
}

Each implementation of SomeMethod is qualified using the name of the interface from which it’s derived.

Explicit interface implementation provides the following benefits:

  • An implementation can differentiate between interface members that have the same signature and return type. In Java, and nonexplicit member implementations in C#, a single implementation is used to satisfy all matching interface members.

  • An implementation can differentiate between interface members that have the same signature and different return types. This is not possible in Java.

  • If an interface that derives from a superinterface has hidden an inherited member, explicit interface implementation is used to differentiate between the implementation of the parent and child members.

The use of explicit interface implementation has some consequences worth mentioning:

  • The members can no longer be accessed through a class instance; they must be accessed through an instance of the interface in which the member is declared.

  • A compile-time error will occur if any access modifiers are applied to the member. The accessibility of an explicitly implemented member is a special case: it is never accessible through the class instance but always accessible through the interface instance.

  • A compile-time error will occur if the abstract, virtual, override, or static modifier is applied to the explicit implementation.

Arrays

Arrays are reference types in both C# and Java. Because of their importance, both languages implement native language syntax to declare and manipulate arrays.

Declaration and creation

C# provides three syntax variants for declaring arrays, including two variants to deal with arrays of multiple dimensions: single-dimensional, multidimensional (rectangular), and jagged (arrays of arrays). Java makes no distinction between multidimensional and jagged arrays.

The syntax for array declaration and creation in C# is less flexible than in Java; the square brackets must follow the type specification. If anything, however, this avoids confusion, improving code clarity without reducing functionality.

Table 5-8 demonstrates the Java and C# syntax for array declaration and creation. We use arrays of type int in these examples, but C#, like Java, will support arrays of any valid type, including delegates, interfaces, and abstract classes.

Table 5-8. A Cross-Language Array Comparison

 

Java

C#

Single-dimensional

int[] x = new int[5]

int[] x = new int[5]

Multidimensional

int[][] x = new int[5][5]

int[,] y = new int[5,5]

or rectangular

int[][][] y = new int[5][5][5]

int[,,] z = new int[5,5,5]

Jagged, or

int[][] x = new int[5][5]

int[][] x = new int[5][]

array of arrays

int[][][] x = new int [5][5][5]

int[][,] y = new int[5][,]

Initialization

As with Java, it’s possible to initialize a declared array without explicitly creating it. An array of the appropriate size will be implicitly created based on the initialization values. For example, the following two statements are equivalent:

int[,,] x = {{{2,3,4}, {5,8,2}}, {{4,6,8}, {7,9,0}}};
int[,,] x = new int [2,2,3] {{{2,3,4}, {5,8,2}}, {{4,6,8}, {7,9,0}}};

The foreach statement

C# provides the foreach statement that simplifies the syntax required to iterate over the elements of an array. Full details of the foreach statement are included in the Statements section in Chapter 4.

Arrays as objects and collections

Arrays are considered objects in both Java and C#; however, the objectlike features of arrays in C# are more extensive than those in Java. In Java, arrays are not directly exposed as objects, but the runtime enables an instance of an array to be assigned to a variable of type java.lang.Object and for any of the methods of the Object class to be executed against it.

In C#, all arrays inherit from the abstract base class System.Array, which derives from System.Object. The System.Array class provides functionality for working with the array, some of which is available in java.util.Arrays. Arrays are also considered to be one of the fundamental collection types.

More Info

Both the object and collection characteristics of arrays are covered in detail in Chapter 9.

Delegates

Delegates are a type introduced by C# that has no direct analogue in Java. Delegates provide an object-oriented type-safe mechanism for passing method references as parameters without using function pointers. Delegates are primarily used for event handling and asynchronous callbacks.

Instances of delegates contain references to one or more methods; this is known as an invocation list. Methods are added to the invocation list through the delegate constructor and subsequent use of simple operators. Through the delegate, the methods on the invocation list can be executed.

Delegates do not provide functionality that is impossible to achieve in Java. The use of appropriate design patterns and interfaces in Java can provide equivalent capabilities; however, delegates are an elegant and powerful feature. Since delegates are used extensively throughout the .NET class libraries, a detailed understanding of how they work is important.

Declaration

A delegate declaration takes the following form:

[attributes] [modifiers] delegate type identifier (parameters);

The type and parameters of the delegate declaration define the return type and signature template a method must have so that it can be added to the delegate invocation list. For example:

//Declare a new delegate type
public delegate int MyDelegate(int x, string y);

//The following methods are valid delegate references
public int Method1(int a, string b) { /* ... */}
public int Method2(int e, string f) {/* ... */}
public int Method3(int p, string q) {/* ... */}

//The following methods are not valid
public void Method4(int p, string q) {/* ... */}
public int Method5(ref int p, string q) {/* ... */}

In this example, Method1, Method2, and Method3 are all valid to be used with MyDelegate. Method4 and Method5 are not because their return types and signatures do not match those defined in the MyDelegate declaration.

Instantiation

Delegates are instantiated using a single parameter: the name of a method that matches the template specified in the delegates declaration. Existing delegate instances can also be assigned to another instance of the same delegate type, in which case a copy of the delegate invocation list will be assigned to the target delegate.

Delegates can be added together using the + and += operators. This results in the target delegate having the combined invocation list of the two operand delegates. Method references are maintained in the order in which they are added. Adding the same method twice results in two references to the same method being maintained on the invocation list.

Removing method references from a delegate instance is achieved using the - or -= operator. Where there are multiple references to the method, only the last instance will be removed. Attempting to remove a method that is not on the invocation list is not an error. Continuing from the foregoing example:

//Instantiating and modifying delegates
MyDelegate d1 = new MyDelegate(Method1);
MyDelegate d2 = new MyDelegate(Method2);
MyDelegate d3 = d1 + d2;
d3 += new MyDelegate(Method3);
d3 -= d1;
d3 -= new MyDelegate(Method3);

A delegate is concerned only with the signature and return type of a method, not the type that implements the method; any instance or static method that matches the template can be used with a delegate.

Invoking a delegate

Invoking a delegate is achieved using the delegate instance as if it were a method with the return type and parameters specified in the delegate declaration. For example:

//Invoking a delegate
int i = d3(6, "Test");

This will cause each method on the invocation list to be called in sequence using the parameters provided. If passing an object reference or using a ref parameter, subsequent methods will see any changes made to the arguments by previous methods. The return value will be the value returned from the last method in the invocation list.

Modifiers

The applicability of modifiers to a delegate declaration depends on the context in which the delegate is declared. Delegates can be declared as a top-level type, being direct members of an enclosing namespace, or they can be nested within the definition of a class or struct. Table 5-9 summarizes modifier availability.

Table 5-9. Delegate Declaration Modifier Availability

 

Delegate Declaration Context

 
 

Member of Namespace

Member of Class

Member of Struct

Accessibility

   

public

protected

N/A

N/A

private

N/A

(default)

(default)

internal

(default)

protected internal

N/A

N/A

Inheritance

   

new

N/A

N/A

abstract

N/A

N/A

N/A

sealed

(implicit)

(implicit)

(implicit)

virtual

N/A

N/A

N/A

override

N/A

N/A

N/A

Other

   

readonly

N/A

N/A

N/A

volatile

N/A

N/A

N/A

static

N/A

N/A

N/A

extern

N/A

N/A

N/A

Events

In the .NET Framework, delegates are most frequently used in conjunction with event members. For a complete description of events, see the Events section later in this chapter.

Conversion

Converting instances of one type or value to another can be implicit or explicit. The compiler handles implicit conversions automatically; the programmer need take no action. Implicit conversions can occur when conversion of one type to another will not cause loss of information.

When no implicit conversion exists between two types, explicit conversion is used. The programmer forces explicit conversion using casts. If a cast is not specified when an explicit cast is required, a compiler error will occur.

The .NET class library includes the System.Convert utility class to convert between different types. This includes the conversion between string, Boolean, date, and value types.

Implicit Conversion

Different data types support different implicit conversions. We discuss these in the following sections.

Implicit numeric conversion

For the simple numeric types, the implicit conversions described in Table 5-10 are possible.

Table 5-10. Supported Implicit Numeric Conversions

From Type

To Types

sbyte

short, int, long, float, double, or decimal

byte

short, ushort, int, uint, long, ulong, float, double, or decimal

short

int, long, float, double, or decimal

ushort

int, uint, long, ulong, float, double, or decimal

int

long, float, double, or decimal

uint

long, ulong, float, double, or decimal

long or ulong

float, double, or decimal

char

ushort, int, uint, long, ulong, float, double, or decimal

float

double

Implicit enumeration conversion

The integer literal 0 (zero) implicitly converts to any enumeration type. Function members should accommodate the fact that 0 is valid where an enum type is expected.

Implicit reference conversion

Implicit reference conversion allows use of a reference type where an instance of a different reference type is expected.

  • Any reference type is implicitly convertible to System.Object.

  • Any reference type is implicitly convertible to any class it derives from.

  • Any reference type is implicitly convertible to any interface it implements.

  • Any array type is implicitly convertible to System.Array.

  • Arrays of the same dimension with underlying types that support implicit conversion are implicitly convertible.

  • Any delegate type is implicitly convertible to System.Delegate.

  • The null value is implicitly convertible to any reference type.

Implicit boxing conversion

Implicit boxing conversion allows the conversion of any value type to System.Object or any interface that the value type implements. The process of boxing is discussed in the Boxing and Unboxing sections earlier in this chapter.

Explicit Conversion

Explicit conversion requires the use of the cast expression; this is the same as Java, where the type to be cast is preceded by a set of brackets containing the target type. For example:

float f = 23897.5473F;
byte b = (byte)f;        // cast float to byte (losing data)

Using a cast where an implicit conversion exists incurs no penalty and can improve the readability of code, clarifying the programmer’s intentions. We discuss the different types of explicit conversion in the following sections.

Explicit numeric conversion

Explicit numeric conversion allows conversion from any numeric type to another. Depending on the types and values converted, information loss or exceptions can occur. See the section The checked and unchecked Keywords in Chapter 4 for details of how to handle numeric overflow.

Explicit enumeration conversion

Explicit enumeration conversion supports the following conversions:

  • From all simple numeric types to any enum type

  • From any enum type to any simple numeric type

  • From any enum type to any other enum type

Explicit reference conversion

Explicit reference conversion permits the conversion of one reference type to another. If an explicit reference conversion fails at runtime, the CLR will throw a System.InvalidCastException.

Explicit unboxing conversion

Unboxing conversions allow a previously boxed value type to be unboxed. The process of unboxing is discussed in the Boxing and Unboxing sections earlier in this chapter.

User-Defined Conversion

C# allows the programmer to define custom mechanisms for the implicit and explicit conversion of user-defined types. We’ll discuss the syntax for defining custom conversions in the Operators section later 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