Types and Objects


The CTS uses abstractions derived from object-oriented (OO) programming environments, impacting both its units of abstraction and instruction set. As noted this type system was designed to be quite malleable and can be made to work underneath nearly any language interface. But this means that when we talk about the CTS, we necessarily do so in terms of classes and objects for representing data and encapsulated operations.

Type Unification

All types in the CTS have a common base type at the root of their type hierarchy: System.Object. As we'll see throughout this chapter, this unification provides a lot of flexibility in how we can pass instances of types around inside the type system. It also means that every type inherits a common group of members, such as methods to convert instances into their text representation, compare instances with each other for equality, and so on. The result is that any instance of any type can be treated as "just an object" to implement some general-purpose functionality. This turns out to be extremely convenient.

The type hierarchy in the CTS is split into two primary trees: reference types and value types. Reference types derive from System.Object directly, while value types derive instead from the special CTS type System.ValueType (which itself derives from System.Object). A diagram of the hierarchy of type abstractions and some specific built-in types is shown in Figure 2-1. It contains a number of special constructs that we will also take a look at throughout this chapter, such as interfaces and enumerations, each of which has a special status in the type system.

image from book
Figure 2-1: CTS type hierarchy.

Notice that a number of primitive data types are listed under the value type hierarchy. Most of the very fundamental types that you take for granted live here. You'll find the following:

  • System.Boolean, or bool in textual IL, is a type whose values can take on one of two values: true or false; in the IL these are represented as 1 and 0, respectively. The size of its storage is actually a full byte (8 bits), not 1 bit as you might imagine, to align on native memory boundaries and make operations on them more efficient.

  • System.Char, or just char in textual IL, representing a single unsigned double-byte (16-bit) Unicode character; this includes, for example, "a," "5," "," and "," among many, many others.

  • System.SByte, Int16, Int32, Int64, or int8, int16, int32, and int16 in textual IL, each representing a signed integer of 1, 2, 4, and 8 bytes (8, 16, 32, and 64 bits), respectively. Signed simply indicates values may be in the negative or positive range.

  • System.Byte, UInt16, UInt32, UInt64, or unsigned int8, unsigned int16, unsigned int32, and unsigned int64 in textual IL, each representing an unsigned integer of 1, 2, 4, and 8 bytes (8, 16, 32, and 64 bits), respectively. Unsigned, of course, means that they do not utilize a bit to represent sign and thus cannot represent negative values. It also means that they can use this extra bit to represent twice the number of positive values of their signed counterparts.

  • System.Single, Double, or float32 and float64 in textual IL, represent standard floating point numbers of 4 and 8 bytes (32 bits and 64 bits), respectively. These are used to represent numbers with a whole and fractional part.

  • System.IntPtr, UIntPtr, or native int and unsigned native int in textual IL, are used to represent machine-sized integers, signed and unsigned, respectively. Most often they are used to contain pointers to memory. On 32-bit systems they will contain 4 bytes (32 bits), while on 64-bit systems they will contain 8 bytes (64 bits), for example.

  • System.Void (or just void) is a special data type used to represent the absence of a type. It's used only in typing signatures for type members, not for storage locations.

From these types can be constructed other forms abstractions in the type hierarchy, for example:

  • Arrays are typed sequences of elements (e.g., System.Int32[]). Arrays are discussed in detail in Chapter 6.

  • Unmanaged and managed pointers to typed storage locations (e.g., System.Byte* and System.Byte&).

  • More sophisticated data structures, both in the reference and value type hierarchy (e.g., struct Pair { int x; int y }).

Chapter 5 describes in further detail more information about each of the primitive types, explains precisely what methods they define, and covers types such as Object, String, and DateTime, which were not mentioned in detail above. We cover enumerations, interfaces, and delegates next.

Reference and Value Types

As noted above, CTS types fall into one of two primary categories: reference types and value types. Reference types are often referred to as classes and value types as structures (or just structs), mostly a byproduct of C#'s keywords class and struct used to declare them. What hasn't been mentioned yet is why the distinction exists and what precisely that means. This section will explore those questions.

An instance of a reference type, called an object, is allocated and managed on the Garbage Collected (GC) heap, and all reads, writes, and sharing of it are performed through a reference (i.e., a pointer indirection). A value type instance, called a value, on the other hand, is allocated inline as a sequence of bytes, the location of which is based on the scope in which it is defined (e.g., on the execution stack if defined as a local, on the GC heap if contained within a heap-allocated data structure). Values are not managed independently by the GC, and are copied while sharing. They are used to represent the primitive and scalar data types.

To illustrate the difference between sharing an object and sharing a value, for example, consider the following. If you were to "load" a field containing an object reference, you would be loading a shared reference to that object. Conversely, when you "load" a field containing a value, you're loading the value itself, not a reference to it. Accessing the object will dereference that pointer to get at the shared memory, while accessing the value will work directly with the sequence of bytes making up that value.

There are clear pros and cons to using one or the other based on your usage. For example, System.String is a reference type, while System.Int32 (i.e., int32 in IL, int in C#) is a value type. This was done for a reason. The default choice for you should always be a class; however, whenever you have a small data structure with value semantics, using a struct is often more appropriate. This section seeks to educate you about the fundamental differences between the two, and on the pros and cons. It also covers the concepts of interfaces, pointer types, boxing and unboxing, and the idea of nullability.

Reference Types (Classes)

Classes should be the default for most user-defined classes. They derive directly from Object, or can be derived from other reference types, providing more flexibility and expressiveness in the type hierarchy. As noted above, all objects are allocated and managed by the GC on the GC heap. As we'll discuss in greater detail in Chapter 3, this means an object lives at least as long as there is a reachable reference to it, at which point the GC is permitted to reclaim and reuse its memory.

References to objects can take on the special value null, which essentially means empty. In other words, null can be used to represent the absence of a value. If you attempt to perform an operation against a null, you'll ordinarily receive a NullReferenceException in response. Values do not support the same notion, although a special type introduced in 2.0 (described below) implements these semantics.

You can create a new reference type using the class keyword in C#, for example:

 class Customer {     public string name;     public string address;     // Etc, etc, etc. } 

A class can contain any of the units of abstraction discussed later in this chapter, including fields, methods, constructors, properties, and so on.

Value Types (Structs)

Value types, also known as "structs," are used to represent simple values. Each value type implicitly derives from the class System.ValueType and is automatically sealed (meaning other types cannot derive from it, discussed later). Instances of value types are called values and are allocated inline on the execution stack (for locals) or the heap (for fields of classes or structs that themselves are fields of classes [or structs]). Value types used as static fields are usually allocated on the GC heap, although this is an implementation detail. Relative Virtual Address (RVA) statics can be allocated in special segments of CLR memory, for example when using scalar types for static fields.

Structs incur less overhead in space and time when working with local to a stack, but this overhead can be quickly dominated by the costs of copying the values around. This is especially true when the size of a value is too large. The rule of thumb is that structs should be used for immutable data structures of less than or equal to 64 bytes in size. We discuss shortly how to determine the size of a struct.

The lifetime of a value depends on where it is used. If it is allocated on the execution stack, it is deallocated once the stack frame goes away. This occurs when a method exits, due to either a return or an unhandled exception. Contrast this with the heap, which is a segment of memory managed by the Garbage Collector (GC). If a value is an instance field on a class, for example, it gets allocated inside the object instance on the managed heap and has the same lifetime as that object instance. If a value is an instance field on a struct, it is allocated inline wherever the enclosing struct has been allocated, and thus has the same lifetime as its enclosing struct.

You can create new value types using the struct keyword in C#. For example, as follows:

 struct Point2d {     public int x;     public int y; } 

A struct can generally contain the same units of abstraction a class can. However, a value type cannot define a parameterless constructor, a result of the way in which value instances are created by the runtime, described further below. Because field initializers are actually compiled into default constructors, you cannot create default values for struct fields either. And, of course, because value types implicitly derive from ValueType, C# won't permit you to define a base type, although you may still implement interfaces.

Values

A value is just a sequence of bytes without any self-description, and a reference to such a value is really just a pointer to the start of those bits. When creating a value, the CLR will "zero out" these bytes, resulting in every instance field being set to its default value. Creating a value occurs implicitly for method locals and fields on types.

Zeroing out a value is the semantic equivalent of setting the value to default(T), where T is the type of the target value. This simply sets each byte in the structure to 0, resulting in the value 0 for all integers, 0.0 for floating points, false for Booleans, and null for references. For example, it's as if the Point2d type defined in the preceding section was actually defined as follows:

 struct Point2d {     public int x;     public int y;     public Point2d()     {         x = default(double);         y = default(double);     } } 

Of course, this is a conceptual view of what actually occurs, but it might help you to get your head around it. default(T) is the same as invoking the default no-arguments constructor. For example, Point2d p = default(Point2d) and Point2d p = new Point2d() are compiled into the same IL.

Memory Layout

Let's briefly consider the memory layout for objects and values. It should help to illustrate some of the fundamental differences. Consider if we had a class and a struct, both containing two int fields:

 class SampleClass {     public int x;     public int y; } struct SampleStruct {     public int x;     public int y; } 

They both appear similar, but instances of them are quite different. This can be seen graphically in Figure 2-2, and is described further below.

image from book
Figure 2-2: Object and value memory layout.

You'll immediately notice that the size of a value is smaller than that of an object.

Object Layout

An object is completely self-describing. A reference to it is the size of a machine pointer — that is, 32 bits on a 32-bit machine, 64 bits on a 64-bit — that points into the GC heap. The target of the pointer is actually another pointer, which refers to an internal CLR data structure called a method table. The method table facilitates method calls and is also used to obtain an object's type dynamically. The double word before that (fancy name for 4 bytes, or 32 bits) makes up the so-called sync-block, which is used to store such miscellaneous things as locking, COM interoperability, and hash code caching (among others). After these come the actual values that make up instance state of the object.

The sum of this is that there is roughly a quad word (8-byte, 64-bit) overhead per object. This is, of course, on a 32-bit machine; for 64-bit machines, the size would be slightly larger. The exact number is an implementation detail and can actually grow once you start using certain parts of the runtime. For example, the sync-block points at other internal per-object runtime data structures that can collect dust over time as you use an object.

Value Layout

Values are not self-describing at all. Rather they are just a glob of bytes that compose its state. Notice above that the pointer just refers to the first byte of our value, with no sync-block or method table involved. You might wonder how type checking is performed in the absence of any type information tagged to an instance. A method table does of course exist for each value type. The solution is that the location in which a value is stored may only store values of a certain type. This is guaranteed by the verifier.

For example, a method body can have a number of local slots in which values may be stored, each of which stores only values of a precise type; similarly, fields of a type have a precise type. The size of the storage location for values is always known statically. For example, the SampleStruct above consumes 64 bits of space, because it consists of two 32-bit integers. Notice that there is no overhead — what you see is what you get. This is quite different from reference types, which need extra space to carry runtime type information around. In cases where structs aren't properly aligned, the CLR will pad them; this occurs for structs that don't align correctly on word boundaries.

Note

Note that the layout of values can be controlled with special hints to the CLR. This topic is discussed below when we talk about the subject of fields.

Lastly, because values are really just a collection of bytes representing the data stored inside an instance, a value cannot take on the special value of null. In other words, 0 is a meaningful value for all value types. The Nullable<T> type adds support for nullable value types. We discuss this shortly.

Discovering a Type's Size

The size of a value type can be discovered in C# by using the sizeof(T) operator, which returns the size of a target type T. It uses the sizeof instruction in the IL:

 Console.WriteLine(sizeof(SampleStruct)); 

For primitive types, this simply embeds the constant number in the source file instead of using the sizeof instruction, since these do not vary across implementations. For all other types, this requires unsafe code permissions to execute.

Object and Value Unification

As we've seen, objects and values are treated differently by the runtime. They are represented in different manners, with objects having some overhead for virtual method dispatch and runtime type identity, and values being simple raw sequences of bytes. There are some cases where this difference can cause a mismatch between physical representation and what you would like to do. For example:

  • Storing a value in a reference typed as Object, either a local, field, or argument, for example, will not work correctly. A reference expects that the first double word it points to will be a pointer to the method table for an object.

  • Calling methods on a value that have been defined on a type other than the value requires that a this pointer compatible with the original method definition be defined. The value of a derived value type will not suffice.

  • Invoking virtual methods on a value requires a virtual method table, as described in the section on virtual methods. A value doesn't point to a method table, due to the lack of a method table, and thus we could not dispatch correctly.

  • Similar to virtual methods, calling interface methods requires that an interface map be present. This is only available through the object's method table. Values don't have one.

To solve all four of these problems, we need a way to bridge the gap between objects and values.

Boxing and Unboxing

This is where boxing and unboxing come into the picture. Boxing a value transforms it into an object by copying it to the managed GC heap into an object-like structure. This structure has a method table and generally looks just like an object such that Object compatibility and virtual and interface method dispatch work correctly. Unboxing a boxed value type provides access to the raw value, which most of the time is copied to the caller's stack, and which is necessary to store it back into a slot typed as holding the underlying value type.

Some languages perform boxing and unboxing automatically. Both C# and VB do. As an example, the C# compiler will notice assignment from int to object in the following program:

 int x = 10; object y = x; int z = (int)y; 

It responds by inserting a box instruction in the IL automatically when y is assigned the value of x, and an unbox instruction when z is assigned the value of y:

 ldc.i4.s 10 stloc.0 ldloc.0 box [mscorlib]System.Int32 stloc.1 ldloc.1 unbox.any [mscorlib]System.Int32 stloc.2 

The code loads the constant 10 and stores it in the local slot 0; it then loads the value 10 onto the stack and boxes it, storing it in local slot 1; lastly, it loads the boxed 10 back onto the stack, unboxes it into an int, and stores the value in local slot 2. You might have noticed the IL uses the unbox.any instruction. The difference between unbox and unbox.any is clearly distinguished in Chapter 3, although the details are entirely an implementation detail.

Null Unification

A new type, System.Nullable<T>, has been added to the BCL in 2.0 for the purpose of providing null semantics for value types. It has deep support right in the runtime itself. (Nullable<T> is a generic type. If you are unfamiliar with the syntax and capabilities of generics, I recommend that you first read about this at the end of this chapter. The syntax should be more approachable after that. Be sure to return, though; Nullable<T> is a subtly powerful new feature.)

The T parameter for Nullable<T> is constrained to struct arguments. The type itself offers two properties:

 namespace System {     struct Nullable<T> where T : struct     {         public Nullable(T value);         public bool HasValue { get; }         public T Value { get; }     } } 

The semantics of this type are such that, if HasValue is false, the instance represents the semantic value null. Otherwise, the value represents its underlying Value. C# provides syntax for this. For example, the first two and second two lines are equivalent in this program:

 Nullable<int> x1 = null; Nullable<int> x2 = new Nullable<int>(); Nullable<int> y1 = 55; Nullable<int> y2 = new Nullable<int>(55); 

Furthermore, C# aliases the type name T? to Nullable<T>; so, for example, the above example could have been written as:

 int? x1 = null; int? x2 = new int?(); int? y1 = 55; int? y2 = new int?(55); 

This is pure syntactic sugar. C# compiles it into the proper Nullable<T> construction and property accesses in the IL.

C# also overloads nullability checks for Nullable<T> types to implement the intuitive semantics. That is, x == null, when x is a Nullable<T> where HasValue == false, evaluates to true. To maintain this same behavior when a Nullable<T> is boxed — transforming it into its GC heap representation — the runtime will transform Nullable<T> values where HasValue == false into real null references. Notice that the former is purely a language feature, while the latter is an intrinsic property of the type's treatment in the runtime.

To illustrate this, consider the following code:

 int? x = null; Console.WriteLine(x == null); object y = x; // boxes 'x', turning it into a null Console.WriteLine(y == null); 

As you might have expected, both WriteLines print out "True." But this only occurs because the language and runtime know intimately about the Nullable<T> type.

Note also that when a Nullable<T> is boxed, yet HasValue == true, the box operation extracts the Value property, boxes that, and leaves that on the stack instead. Consider this in the following example:

 int? x = 10; Console.WriteLine(x.GetType()); 

This snippet prints out the string "System.Int32", not "System.Nullable`1<System.Int32>", as you might have expected. The reason is that, to call GetType on the instance, the value must be boxed. This has to do with how method calls are performed, namely that to call an inherited instance method on a value type, the instance must first be boxed. The reason is that the code inherited does not know how to work precisely with the derived type (it was written before it even existed!), and thus it must be converted into an object. Boxing the Nullable<T> with HasValue == true results in a boxed int on the stack, not a boxed Nullable<T>. We then make the method invocation against that.

Accessibility and Visibility

Before delving into each of the member types available, let's briefly discuss the visibility and accessibility rules for both types and members. Visibility defines whether a type is exported for use outside of an assembly, the unit of packaging and reuse for managed binaries. Accessibility defines what code inside an assembly can access a type or specific member. In both cases, we can limit what parts of the system can "see" a type or member.

The visibility of types is determined by your compiler and is heavily dependent on accessibility rules. In general, whenever a type uses the public or family accessibility declaration, it becomes visible to other assemblies. All visible types are marked as such in the assembly's manifest, as described further in Chapter 4. Although these rules are not precise, they are sufficient for most purposes. We'll limit further discussion here to accessibility modifiers.

By "see" in the opening paragraph, we just mean that the runtime will enforce that all references to types and members are done in a manner that is consistent with the policy outlined here. This provides safety to ensure that encapsulation of data is maintained and that certain invariants can be controlled by the type itself. Further, the Visual Studio IDE hides such members and the C# compiler checks these access policies at compile time so that developers don't accidentally make invalid references that fail at runtime.

Whether code has access to a member is partially defined by lexical scoping in your favorite language and partially by the accessibility modifiers on the member itself. Below are the valid accessibility modifiers. Note that many languages, C# included, only support a subset of these:

  • Public: The type or member may be accessed by any code, internal or external to the assembly, regardless of type. This is indicated by the public keyword in C#.

  • Private: This applies to members (and nested types, a form of member) only. It means that the member may only be accessed by code inside the type on which the member is defined. This is indicated with the private keyword in C#. Most languages use private as the default for members if not explicitly declared.

  • Family (Protected): Applies only to members, and means a member may be accessed only by the type on which the member is defined and any subclasses (and their subclasses, and so on). This is indicated by the protected keyword in C#.

  • Assembly: Accessible only inside the assembly in which the type or member is implemented. This is often the default for types. This is indicated by the internal keyword in C#.

  • Family (Protected) or Assembly: Accessible by the type on which the member lives, its subclass hierarchy, and any code inside the same assembly. That is, those who satisfy the conditions for Family or Assembly access as defined above. This is marked by the protected internal keywords in C#.

  • Family (Protected) and Assembly: Accessible only by the type on which the member lives or types in its subclass hierarchy and that are found in the same assembly. That is, those that satisfy the conditions for both Family and Assembly access as defined above. C# does not support this accessibility level.

Nesting

The lexical scoping part of accessibility noted above really only becomes a concern when working with nested type definitions. This is of course language dependent, as the CLR doesn't carry around a notion of lexical scoping (aside from rules associated with accessing fields from methods, and loading locals, arguments, and other things related to method activation frame information). The CLR does, however, permit languages to create first class nested types.

For example, C# permits you to embed a type inside another type (and so on):

 internal class Outer {     private static int state;     internal class Inner     {         void Foo() { state++; }     } } 

The Inner class is accessible from outside of Outer using the qualified name Outer.Inner. Inner types have the same visibility as their enclosing type, and the accessibility rules are the same as with any other member. Your language can of course supply overriding policy, but most do not. You can also specify the accessibility manually, as is the case with the above Inner class marked as internal.

The inner type Inner has access to all of the members of the outer type, even private members, as would be the case of any ordinary member on Outer. Further, it can access any family/protected members of its outer class's base type hierarchy.

Type Members

A type can have any number of members. These members make up the interface and implementation for the type itself, composing the data and operations available for that type. This includes a type's constructors, methods, fields, properties, and events. This section will go over each in detail, including some general cross-cutting topics.

There are two types of members: instance and static. Instance members are accessed through an instance of that type, while static members are accessed through the type itself rather than an instance. Static members are essentially type members, because they conceptually belong to the type itself. Instance members have access to any static members or instance members lexically reachable (based on your language's scoping rules); conversely, static members can only access other static members on the same type without an instance in hand.

Fields

A field is a named variable which points to a typed data slot stored on an instance of a type. Fields define the data associated with an instance. Static fields are stored per type, per application domain (roughly equivalent to a process, see Chapter 10 for more details). Field names must be unique to a single type, although further derived types can redefine field names to point at their own locations. The size of a value type value is roughly equal to the sum of the size of all of its field type sizes. (Padding can change this, that is, to ensure that instances are aligned on machine word boundaries.) Objects are similar, except that they add some amount of overhead as described above.

For example, the following type introduces a set of fields, one static and five instance:

 class FieldExample {     private static int idCounter;     protected int id;     public string name;     public int x;     public int y;     private System.DateTime createDate; } 

The equivalent in textual IL is:

 .class private auto ansi beforefieldinit FieldExample     extends [mscorlib]System.Object {   .field private static int32 idCounter   .field family int32 id   .field public string name   .field public int32 x   .field public int32 y   .field private valuetype [mscorlib]System.DateTime createDate } 

We can now store and access information in the static member from either static or instance methods, and can store and access information in the instance members in FieldExample's instance methods.

The size of a FieldExample instance is the sum of the sizes of its fields: id (4 bytes), name (4 bytes on a 32-bit machine), x (4 bytes), y (4 bytes), and createDate (size of DateTime, which is 8 bytes). The total is 24 bytes. Notice the size of name, because it is a managed reference, will grow on a 64-bit machine; thus, the size on a 64-bit machine would be 28 bytes. And furthermore, object overhead would make the size of a FieldExample on the GC heap at least 32 bytes (on 32-bit), and likely even more.

Read-Only Fields

A field can be marked as initonly in textual IL (readonly in C#), which indicates that once the instance has been fully constructed, the value cannot be changed for the lifetime of an instance. Read-only static members can only be initialized at type initialization time, which can be accomplished conveniently in, for example, C# with a variable initializer, and cannot be rewritten after that.

 class ReadOnlyFieldExample {     private readonly static int staticData; // We can set this here     static ReadOnlyFieldExample()     {         // Alternatively, we can set 'staticData' here.     }     private readonly int instanceData; // We can set this here     public ReadOnlyFieldExample()     {         // Alternatively, we can set 'instanceData' in one of         // 'ReadOnlyFieldExample's constructors.     } } 

Don't confuse the read-only feature with immutability, however. For example, if a read-only field refers to a mutable data structure, the contents of the structure can still be changed. Read-only simply means that the reference cannot be updated to point to a new data structure instance. For example, given:

 class ReadOnlyFieldBadExample {     public static readonly int[] ConstantNumbers = new int[] {         0, 1, 2, 3, 4, 5, 6, 7, 8, 9     }; } // Some rogue code can still rearrange numbers inside the array; they simply // cannot set 'constantNumbers' to a new array! ReadOnlyFieldBadExample.ConstantNumbers[0] = 9; ReadOnlyFieldBadExample.ConstantNumbers[1] = 8; // ... 

Those with access to ConstantNumbers (everybody who can access ReadOnlyFieldBadExample, since the field has been marked public) may change around the elements of the array. This can be quite surprising when seemingly "invariant constants" in your code are mucked with by users, leading to strange failures (which silently slipped past your test cases).

Constant Fields

You can also create constant or literal fields. These are marked as static literal in the IL and using the const modifier in C#. Constants must be initialized inline, for example by using inline field assignment in C#, and can only contain primitive values, such as integers, floating points, string literals, and so on. You can of course use simple mathematical expressions, too, so long as the compiler is able to reduce them to a constant value at compile time.

 class UsefulConstants {     public const double PI = 3.1415926535897931;     // The following is simple enough for the compiler to reduce to '4':     public const int TwoPlusTwo = 2 + 2; } 

Notice that C# emits constants as implicitly static or associated with a type. This makes intuitive sense since the value cannot reasonably differ based on instance. It is, after all, constant.

Using a constant field is an efficient way to represent constant values but can lead to some subtle versioning problems. When a compiler sees you using a const field, for example, most often the literal value is embedded in your code. C# does exactly this. If the constant originated from another assembly on which you depend, problems might arise if the value ever changes in that assembly. Namely, your previously compiled IL will continue to use the old value. You must recompile it for the IL to contain the new constant values. This issue is obviously not present for constants internal to your own assembly.

Controlling Field Layout for Structs

We've assumed a nave view of structure layout all throughout the discussion in this chapter, including the section entitled memory layout! While it's correct for 99% of the cases you'll encounter, there is a feature which enables you to override the default layout of structs. In particular, you can rearrange the order and offsets of fields on a value type. This is mostly used for unmanaged interoperability (see Chapter 11) but can also be used for advanced scenarios such as union types and efficient bit-packing.

Layout is controlled in C# by using the System.Runtime.InteropServices.StructLayout Attribute custom attribute. It has three basic modes of operation, specified by the LayoutKind enumeration value passed to the custom attribute's constructor. These compile down to IL keywords (indicated in the text below):

  • LayoutKind.Automatic: This option permits the CLR to layout value type fields in whatever manner it chooses, indicated by the autolayout IL keyword, enabling it to optimize for alignment. For example, if you had three fields, a 1-byte, a 2-byte, and another 1-byte field, it would likely align the data structure such that the 2-byte field was laid out on a word boundary. This is the default. But it also prohibits marshaling values across an unmanaged interoperation boundary because the layout is unpredictable.

  • LayoutKind.Sequential: With this mode, all fields are laid out in the exact order they are specified in the IL. This is indicated with the layoutsequential IL keyword. While this does not enable the runtime to optimize layout, it does guarantee a predictable layout for marshaling across to unmanaged code.

  • LayoutKind.Explicit: This last option pushes the burden of layout on the author of the struct itself, and is indicated with the explicitlayout keyword in IL. The FieldOffsetAttribute must be attached to each field in the struct, specifying precisely where the field occurs in the structs overall layout. It's tricky to get right, as you can easily overlap fields by accident but enables some powerful scenarios. We take a look at an example of this below.

There are three other pieces of relevant information that may be supplied when controlling layout. First is the absolute Size (in bytes) of the data structure. This must be equal to or greater than the sum of all fields based on the layout specified above. It can be used, for example, to extend the size of the data structure for alignment or if there is unmanaged code that accesses the bytes beyond its managed data to store information. Next, you can specify the Pack (again, in bytes), which states how the data structures field's should be padded. The default is 8 bytes, meaning that a data structure that is less than 8 bytes will be padded to consume 8 bytes. Lastly, the CharSet is used entirely for unmanaged interoperability; thus, we won't discuss it further here.

Explicit Layout Example

This example below shows the use of explicit layout so that fields are not laid out sequentially, and it also intentionally overlaps some data to create a C-style discriminated union:

 using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Explicit)] struct MyUnion {     [FieldOffset(0)] string someText;     [FieldOffset(7)] byte additionalData;     [FieldOffset(6)] byte unionTag; // 0 = 'a', 1 = 'b'     [FieldOffset(4)] short unionA;     [FieldOffset(4)] byte unionB1;     [FieldOffset(5)] byte unionB2; } 

The first 4 bytes are taken up by the someText managed reference. Then we intentionally start the unionA and unionB1 fields after the 4th byte. unionA is a short and thus takes up 2 bytes, whereas unionB1 is only a single byte. We then pack another byte after unionB2, which overlaps with the second byte of unionA. After this, we have a single byte to indicate which type the union is. The intention is that if unionTag is 0, unionA contains valid data; otherwise, both unionB1 and unionB2 contain valid data. We then store some additionalData (a single byte) in the last byte.

Methods

Functions have existed in languages for quite some time, becoming the first way in which to abstract and reuse blocks of code whose execution differs based on input. A method is simply the object-oriented lingo for a function. Every piece of executable user code in the CTS is a method, be it a traditional-style method, a constructor, or a property getter/setter. In this section, we'll look at what methods consist of, and also how they are called from other methods.

A method accepts any number of formal parameters (referred to as just parameters from here on), which are essentially named variables that your method's body has access to and that are supplied by callers through actual arguments (referred to as just arguments). Methods also have a single output return parameter, which is optional and can be typed as void to indicate one isn't supplied. It is used to supply the caller with information once the method completes. When we talk about parameters, the default is an input parameter. The CTS also supports parameters passed by reference, which permits the caller and callee to share the same references to the same data. We will see how to take advantage of these features in just a few paragraphs.

The following code illustrates a method SomeMethod, which takes two parameters and returns a value:

 class Calculator {     public int Add(int x, int y)     {         return x + y;     } } 

This method might be called from user code, for example as follows:

 Calculator calc = new Calculator(); int sum = calc.Add(3, 5); 

The IL generated contains a call to the object referred to by sum's Add method. The values 3 and 5 are passed as the arguments for parameters x and y, respectively, for a single activation frame of the method Add. The method then uses the dynamic values 3 and 5 from the frame, adding them together and returning the result. The frame is then popped off, resulting in the number 8 being passed back to the receiver; in this case, it stores it in its own local variable sum.

Methods, like any other member, can be instance or static. Instance methods have access to a special this pointer when they execute (named this in C#, Me in VB) — referring to the instance on which the method was called — using which they may access the public, protected, and private instance members of the enclosing type (and public or protected members in its type's hierarchy). For example, consider a class with instance state:

 class Example {     int state;     public void Foo()     {         state++;         this.state++;     } } 

Note that C# infers the this qualification for the reference to the state field because it notices that an instance variable is in scope. C# also supports explicit qualification with the this keyword. In either case, the underlying IL encodes this access by loading the 0th argument of the activation frame. This is the convention for passing the this pointer from a caller to a receiving instance method. All other real arguments begin at 1.

Static methods, conversely, have no this pointer. They have access to private static members of the enclosing type, but there is no active instance passed on which to access instance members. Arguments for static methods begin at 0. This is entirely transparent if you stay inside the world of C#. But at the IL level you must recognize and deal with the difference.

Locals

Two types of data are local to a method's activation frame: arguments and locals. Both occupy space on the physical stack; they are allocated when a method call is made and deallocated when a method exits, either because of an ordinary return or an unhandled error. We've already seen the use of arguments above. Arguments are locations into which callers copy data they wish to pass to the callee. We'll examine the different argument passing styles later, which can involve copying values or passing references to a caller's local data structures, for example, depending on the scenario.

A method can also use locals. Locals are also allocated on the stack, but are entirely transparent to callers. They are an implementation detail of the method. When the stack space is allocated, locals are initialized by zeroing them out (resulting in 0 for scalars, 0.0 for floating points, false for Booleans, and null for references), and each is assigned a conceptual slot. Slots are typed so that the CLR can allocate the correct amount of space and so that the verifier can ensure they are used in a type-safe manner.

For example:

 class Example {     public void Foo()     {         int x = 0;         double y = 5.5;         string s = "Hello, World";         // ...     } } 

This code defines three locals, x, y, and s, which get numbered when they are emitted to the IL. Most compilers would turn these into 0, 1, and 2, respectively, but a compiler is free to perform optimizations that would alter this numbering. Additional locals may of course be defined further in the method in the C# language (unlike C, where all variables must be introduced at the beginning of a function), although this is language specific and each will still ordinarily get its own slot (although compilers are free to reuse slots as well).

Overloading

Multiple methods with the same name can exist on a type. When they do, they must differ in some significant way, leading to something called overloading. Methods may be overloaded through distinguished parameters. Differences by return type only are not sufficient to overload a method.

This capability permits you to create methods with similar functionality for differing parameter types, often a convenient feature for your users. It also allows you to implement default values for arguments, for example by providing versions that simply call other overloads with the default values for certain parameters.

For example, consider the overloaded method Bar on the following type:

 class Foo {     void Bar() { Bar(10); /* "default" value */ }     void Bar(int x) { /*...*/ }     void Bar(int x, int y) { /*...*/ }     void Bar(object o) { /*...*/ } } 

Compilers (and/or late-binders for dynamic languages) have the fun job of overload resolution, a process that consists of matching the arguments of a caller with an appropriate overload for the receiving method. This is essentially a search to find the best match given a set of actual arguments. In general, compilers will choose the most specific match. When an ambiguous match, or no match, is found, a compiler error or warning will often result.

For example, given two methods:

 void Foo(object o); void Foo(string s); 

The following code would bind to Foo(string) because it is more specific:

 Foo("Hello, Binding!"); 

Of course, there are more complex examples that demonstrate the nitty gritty for these rules. But a little experimentation should help to alleviate any doubt or questions about the way in which binding works in your favorite language. The CTS certainly doesn't mandate any specific set of overload resolution rules, so it is subject to vary from language to language.

Argument Passing Style

The way that values are passed as arguments to a method is often a point of confusion. The CTS uses two primary styles: pass-by-value and pass-by-reference, also known by their shorthand forms byval and byref. The distinction can be tricky to understand, especially with the differences between value and reference types.

Pass-by-Value and Pass-by-Reference

Pass-by-value is also known as pass by copy. What happens here is that a copy of the argument is made and then passed to the receiving method. If the callee method modifies the value of the argument inside of its method body, the caller will never see the results of these modifications. In the case of object references, the value is actually the memory location. This leads many people to believe that passing an object reference is pass-by-reference, which is very incorrect. We're making a copy of the reference, and passing that copy. The copy ends up pointing to the same address, true, but the method receiving the argument cannot change our original reference to point at a different object.

Pass-by-reference, as you might imagine, supplies the callee with a reference to the same location that the caller's argument was pointing to, enabling the method's body to access the shared reference and even change it to point elsewhere. If the caller were to pass a reference to one of its data, it would notice updates made by the method body. Note that verifiability guarantees that a reference to data cannot outlive the data itself. Pass-by-reference is not used as the default in C# and must be explicitly declared by the method and the caller in C# if you intend to use it. Note that VB uses the ByVal and ByRef keywords to indicate the behaviors.

For example, this type declares a method with a byref argument:

 public void Swap<T>(ref T x, ref T y) {     T t = x;     x = y;     y = t; } 

Accessing a value passed by reference — even if it's a value type — requires an extra dereference to get to the data the byref points at. In the above example, the values for x and y are the addresses of the caller's arguments, not the values themselves. The result of executing the Swap method is to swap the values referenced by two argument references to occupy the location of the other. This isn't possible with pass by value because in the body where we modify x and y, we'd be changing the value of the method's local copies, not shared data. The modifications wouldn't be visible to the caller and would be entirely lost.

To call this method in C#, the user has to explicitly state that they wish to supply a byref argument:

 void Baz() {     string x = "Foo";     string y = "Bar";     // Print the original values of x and y:     Console.WriteLine("x:{0}, y:{1}", x, y);     // Perform the swap...     Swap<string>(ref x, ref y);     // Now, print the modified values of x and y:     Console.WriteLine("x:{0}, y:{1}", x, y); } 

Executing this code will demonstrate that, prior to the call to Swap, x refers to the "Foo" string and y refers to the "Bar" string. After calling Swap, x refers to "Bar" and y refers to the location of "Foo". Strings were used to illustrate the effect, since clearly strings are immutable; their contents could not have been modified by the Swap function, rather the values of the x and y pointers in Baz's local address slots must have been.

This has the effect of using load address instructions in the IL, rather than the typical load value. The distinction is made clearer in Chapter 3, where we examine these instructions in detail. Suffice it to say for now that passing, for example, x in the above example by reference involves loading the address of the x local variable, using the ldloca instruction.

Output Parameters (Language Feature)

All parameters are input by default. That is, their values are supplied to the method by callers, but changes cannot be communicated back. We saw already that passing something by reference changes this. You can view the return value of a method as a special type of parameter, namely one that supplies a value to the caller from the callee but does not accept input at method invocation time. There is one significant limitation with a return parameter: You can only have one.

The C# language enables you to create additional output parameter on your method, which is simply a special form of the pass-by-reference. The difference between an ordinary byref and an output parameter is that C# permits you to pass a reference to a memory location not specifically initialized by the user program, doesn't permit the receiving method to read from that reference until it's been assigned by the receiver, and guarantees that the receiving method writes something to it before returning normally.

For example, consider this case:

 public void Next3(int a, out int x, out int y, out int z) {     x = a + 1;     y = a + 2;     z = a + 3; } 

This example just assigns the next three integers after the input parameter a to the output parameters x, y, and z, respectively. Similar to the case with pass-by-reference parameters, the caller needs to explicitly state that he or she wishes to pass an argument as an out:

 int a, b, c; Next3(0, out a, out b, out c); 

The C# compiler generates code that, again, simply uses load address instructions.

Managed Pointers

Pass-by-reference and output parameters in C# all work implicitly by using managed pointers. A managed pointer is used to point to the interior of an instance, either object or value, unlike a normal reference, which points to the beginning of an object record. We say the type of a managed pointer is T&, where T is the type of the data it points to. Managed pointers enable you to refer to an instance's field, an argument, or data on the execution stack, for example, without worrying about whether it will move. The GC updates managed pointers with new locations whenever it moves instances. And verification ensures that a managed pointer can never outlive the data to which it points.

When you pass something in C# using the ref or out keyword, you're implicitly creating a new managed pointer and passing that to the method, using one of the load address instructions, that is ldflda, ldarga, ldloca, or ldelema. Accessing the data behind a managed pointer leads to an extra level of indirection, as the execution engine must dereference the managed pointer before it can get at the underlying value or reference.

Variable Argument Methods

Ever since the old C-style printf function has there been support for variable-length arguments in programming languages. This permits methods to accept a number of arguments whose count is not known at compile time. Callers of the method are free to pass an unbounded set of arguments at invocation. The method body itself uses special constructs to extract and work with such arguments.

The CTS actually supports variable arguments directly in the IL, through the use of the vararg method modifier. Such methods then use the arglist instruction to obtain an instance of System.ArgIterator for purposes of walking the incoming list of arguments. The method signature doesn't even mention the type of arguments.

However, many languages (e.g., C#) use an entirely different convention for variable argument methods. These languages use arrays to represent the variable portion of their arguments and mark the method with the System.ParamArrayAttribute custom attribute. The calling convention is such that callers passing variable-length arguments pack them into an array, and the method itself simply works with the array. For example, consider this example in C#, using the params keyword:

 void PrintOut(params object[] data) {     foreach (object o in data)         Console.WriteLine(o); } PrintOut("hello", 5, new DateTime(10, 10, 1999)); 

C# will turn the invocation to PrintOut into IL, which constructs an array, packs the string "Hello", the int 5, and the DateTime representing 10/10/1999 into it, and passes that as the data argument. The PrintOut function then just deals with data as any ordinary array.

Methods and Subclassing

We discuss subtyping and polymorphism in much greater detail later on in this chapter. But assuming that you are at least somewhat familiar with standard OO idioms, we will discuss virtual methods, overriding, and newslots here. All you need to understand for this discussion is that a type may subclass another type, forming a special relationship between them. The subclass inherits its base type's nonprivate members, including methods. The topic here is controlling how those methods get inherited.

Virtuals and Overriding

By default, subclasses inherit both nonprivate method interfaces and implementations of their base classes (all the way up the chain). A method can be marked as virtual, however, which declares that further derived types can override the behavior of the base type. Overriding a virtual method simply replaces the implementation inherited from the base type.

Some languages like Java treat all methods as virtual by default. C# does not, meaning that you must opt in to virtualism explicitly using the virtual keyword.

 class Base {     public virtual void Foo()     {         Console.WriteLine("Base::Foo");     } } class Derived : Base {     public override void Foo()     {         Console.WriteLine("Derived::Foo");     } } 

Derived has overridden the method Foo, supplying its own implementation. When a call is made to the virtual method Base::Foo, the method implementation is selected at runtime based on the identity the instance against which the call is made. For example, consider the following sample:

 // Construct a bunch of instances: Base base = new Base(); Derived derived = new Derived(); Base derivedTypedAsBase = new Derived(); // Now invoke Foo on each of them: base.Foo(); derived.Foo(); derivedTypedAsBase.Foo(); 

When run, the above snippet of code will output the following to the console:

 Base::Foo Derived::Foo Derived::Foo 

To demonstrate that no analysis trickery accomplishes this, consider this method:

 public void DoSomething(Base b) { b.Foo(); } // In another assembly altogether... DoSomething(new Base()); DoSomething(new Derived()); 

This prints out the following:

 Base::Foo Derived::Foo 

Virtual method dispatch traverses the object reference to get at the method table of the instance. (We described object layout earlier in this chapter; if you've forgotten, you might want to refer back.) The method table contains a set of method slots, each of which is a pointer to a piece of code. When performing a virtual method dispatch, the pointer to the object is dereferenced, and then the pointer to the appropriate virtual method slot in the method table; then the value held in the slot is used as the target address of the method call. Please refer to Chapter 3 for more information about virtual method dispatching, in particular how the JIT Compiler plays a role.

New Slots

Methods can also be marked as newslot rather than override, meaning that they introduce an entirely new version of a method and explicitly do not provide a new version of a base type's implementation. newslot is used even when a base type implementation doesn't exist, preventing versioning problems from arising.

This is indicated with the new keyword in C#. For example, using the above example as a basis:

 class Base {     public virtual void Foo()     {         Console.WriteLine("Base::Foo");     } } class Derived : Base {     public new void Foo()     {         Console.WriteLine("Derived::Foo");     } } 

The behavior of the sample code changes ever so slightly:

 // Construct a bunch of instances: Base base = new Base(); Derived derived = new Derived(); Base derivedTypedAsBase = new Derived(); // Now invoke Foo on each of them: base.Foo(); derived.Foo(); derivedTypedAsBase.Foo(); 

Executing the above code prints out:

 Base::Foo Derived::Foo Base::Foo 

Notice that the first two method calls work as expected. There is a subtle difference, however. In the original virtual method and overriding example, the IL contained callvirt instructions to the Base::Foo method. In this case, however, there is a single callvirt to Base::Foo, followed by an ordinary call to Derived::Foo, an entirely different method.

Then a possible surprise arises: the third invocation results in "Base::Foo". This is because the method call against the Base-typed reference pointing to a Derived instance will emit a virtual call to the method Base::Foo. The virtual method at runtime will notice that Base's version of Foo has not been overridden; instead, the Derived::Foo method is represented as an entirely distinct method with its own slot in the method table. Thus, the method dispatch calls Base's version.

Exception Handlers

Each method may define a set of exception handler blocks of code. This is used to transform try/catch/finally blocks into IL, since the binary representation of IL has no inherent understanding of blocks. This is entirely a language abstraction. An exception handler block specifies where its catch clauses live in the IL using instruction offsets, which is then read by the runtime's exception subsystem. Please refer to Chapter 3 for an in-depth discussion of exception handling, including how handlers are encoded in the IL.

Constructors and Instantiation

Instances of types must be created before they are used, a process known as instantiation. An instance is ordinarily constructed by invoking its constructor with the newobj instruction, whose job it is to initialize the state of an instance before making it available to other code. Value types support implicit initialization by simple zeroing out of their bits, for example using the IL instruction initobj, by assigning 0, or by relying on the default local and field initialization behavior. Types can have any number of constructors, which are special types of methods with a void return type and each of which is overloaded as with any other method (i.e., by its parameters).

When the CLR allocates a new object, it does so by first creating space for it on the GC heap (a process described further in Chapter 3). It then invokes your constructor, passing any arguments supplied to the newobj instruction using an activation frame. It is then the responsibility of your constructor code to ensure that, when the constructor exits, your type has been fully initialized into a usable state. There might be additional operations necessary in order to progress to other states, but if a constructor completes successfully, the instance should be usable and explicitly not corrupt.

Constructors are named .ctor in the IL representation, although C#, for instance, uses a simpler syntax. The format is to use the type name as though it were a method name, omitting any type of return type specification (a syntax borrowed from C++). For example, this snippet of code creates a constructor which initializes some instance state:

 class Customer {     static int idCounter;     private int myId;     private string name;     public Customer(string name)     {         if (name == null)             throw new ArgumentNullException("name");         this.myId = ++idCounter;         this.name = name;     }     // ... } 

This is compiled into an instance void . ctor(string) method in the IL.

The Customer class here offers only a single constructor, which takes a string argument representing an instance's name. It sets its id field based on the static idCounter and then stores the name argument value in its instance name field. The class implements an implicit invariant that fully initialized customers have a nonzero id and a non-null name.

Default Constructors (Language Feature)

The C# and VB languages emit a default constructor, defined as a constructor that takes no parameters, on your reference types if you don't specify any explicitly. This enables users of your type to create new instances using the new YourType() syntax. That is, the following C# type declaration actually gets a default constructor in the compiled IL:

 class CtorExample {} 

The following is structurally equivalent in the IL:

 class CtorExample {     public CtorExample() {} } 

As soon as you introduce an explicit constructor, the compiler no longer adds the default one automatically. For example, the IL resulting from the following C# program does not contain a default constructor. The only way to construct an instance is to use the overload which accepts an integer:

 class CtorExample {     private int value;     public CtorExample(int value)     {         this.value = value;     } } 

The role of a constructor is simply to initialize the type and make it ready for further use. You should try to avoid expensive operations like blocking I/O, for example, or generally anything that will surprise your users. In such situations, it's usually more appropriate to employ a multistate object that offers an explicit I/O operation to acquire and retrieve data from the outside.

Value Construction

Because value types are allocated inline, their default initialization is to zero-out all of their bits without even calling a constructor. This is extremely efficient, avoiding the need to make essentially a method call for each value instantiation. For this reason, C# prohibits creating parameterless constructors for structs. This avoids confusion with the statement new YourValueType(), which is actually compiled into IL that creates a new zeroed out chunk of bits. In C#, you cannot guarantee that value type creation will go through one of your constructors, although this is not entirely obvious immediately.

Constructor Chaining (Language Feature)

When deriving from existing types, the base type's constructor may be called. In fact, it is required in C# to ensure that base classes are able to construct state correctly. Without doing so, executing any code on the derived type could result in unpredictable behavior. This chaining happens by convention in C# before the body of a derived type's constructor executes.

If you don't explicitly state the base type constructor overload to call, the C# compiler will automatically utilize the base type's default constructor. If one does not exist, C# will give you a compilation error. For example, consider CtorExampleDerived, which uses the above-defined class CtorExample as its base type:

 class CtorExampleDerived : CtorExample {     private DateTime initializedDate;     public CtorExampleDerived(int value) : base(value * 2)     {         initializedDate = DateTime.Now;     } } 

Notice that this constructor makes a call to the base constructor, using C#'s special syntax. It looks much like a function call — where base is the constructor function — and indeed the constructor matching happens depending on how many parameters are passed to base, precisely as method overload resolution happens. The call to the base constructor executes to completion before initializedDate is set by CtorExampleDerived's constructor. If we hadn't specified the target constructor, the C# compiler would fail because CtorExample (the version above that takes an integer) doesn't have a parameterless constructor.

You can also chain constructors across overloads on a single type. This can be used in a fashion similar to method overloading, for example to define default values and avoid redundant code. The syntax is very similar to base above, except that you use this instead:

 class CtorExampleDerived : CtorExample {     private bool wasDefaultCalled;     private DateTime initializedDate;     public CtorExampleDerived() : this(0)     {         wasDefaultCalled = true;     }     public CtorExampleDerived2(int value) : base(value * 2)     {         initializedDate = DateTime.Now;     } } 

This code uses the default constructor to supply a default value. Of course, both constructors can do more than just forwarding. The default constructor might perform additional operations, for example setting wasDefaultCalled to true in the above example.

Field Initialization (Language Feature)

If you use inline initialization for instance fields, the code for that initialization ends up being copied by the C# compiler to all constructors, which don't delegate to another constructor on the type using the this keyword. For example, consider this type:

 class FieldInitExample {     int x = 5;     int y;     public FieldInitExample() : this(5)     {     }     public FieldInitExample(int y)     {         this.y = y;     } } 

The int x = 5 line is actually copied to the first few statements of the constructor, which takes an integer argument. It doesn't get copied to the default constructor because that constructor just delegates to the other overload, which would lead to redundant stores to the x field.

The resulting IL is as if you wrote:

 class FieldInitExample {     int x;     int y;     public FieldInitExample() : this(5)     {     }     public FieldInitExample(int y)     {         this.x = 5;         this.y = y;     } } 

Static field initialization is handled nearly identically, with the caveat that static class constructors are used instead of instance constructors. The CctorExample type from above could have been written as follows, while keeping the same semantics as before:

 class CctorExample {     static DateTime classLoadTimestamp = DateTime.Now; } 

Type Initializers

There is another style of constructor, called a type constructor. This is commonly referred to as a static or class constructor, or sometimes a type initializer, and permits you to initialize static type information associated with a class. These are named .cctor in the IL, and C# has special syntax for writing them:

 class CctorExample {     static DateTime classLoadTimestamp;     static CctorExample()     {         classLoadTimestamp = DateTime.Now;     } } 

Note that this snippet of code is equivalent to the following:

 Class CctorExample {     static DateTime classLoadTimestamp = DateTime.Now; } 

The C# compiler transforms the latter into the former. An explicit constructor as well as a combination of static field initialzers can appear on the same type. Very much like the previous section on instance field initialization, the compiler will preprend the contents of a type constructor with all field initializations.

Type initializers written in C# have beforefieldinit semantics. This means that the initializers are not guaranteed to run at any specific point, but that they will always be run at least by the time the first access is made for the given type. Access in this context means reading or writing to a static or instance field or a method call to a static or instance method on the type. This permits the CLR to optimize when precisely the initializer is run, delaying it or running it early based on heuristics. Conversely, the default policy is to run initializers precisely when the access is made.

Type constructors run in the context of the thread that caused them to execute. There is logic in runtime to guarantee that circular dependencies among static constructors do not lead to infinite loops. You can explicitly force the execution of type initializations for a target type with the method System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor.

Unhandled Constructor Exceptions

An unhandled exception from a constructor can occur if the body of a constructor causes an exception to be raised. This is sometimes unavoidable, although it is inconvenient. For example, if you were unable to fully initialize the object, for example because of inconsistent or corrupt state, you would have no other choice but to throw an exception. The alternative would be to pretend everything was OK by returning successfully from the constructor, which could lead to failures later on in the program's execution. It's better just to have the program fail as soon as you notice a problem.

Note

The behavior of exceptions that are completely unhandled in the CLR is discussed in Chapter 3. There is also an API that permits code to fail immediately, for example when severe state corruption is noticed. This, too, is discussed in the next chapter.

The reason that this topic is problematic enough to call out is that, in the face of an exception thrown by a constructor, the caller of your constructor will have no reference to your object. Consider what the IL sequence looks like for the C# code Foo f = new Foo():

 newobj instance void Foo::.ctor() stloc.0 

The newobj instruction makes a call to the constructor, leaving the result on the execution stack. stloc.0 stores it in a local slot in the execution stack. But if an exception is raised as a result of the newobj instruction (because of insufficient memory to allocate the object, or because of an unhandled exception thrown from the constructor), the result will never be placed on the stack, and the stloc.0 will never execute because the CLR exception handling behavior will take over. The result is that the variable f will be null.

This can cause some unpredictable behavior. For example, consider a type that allocates resources in its constructor but then throws an exception. Normally, types like this will support the IDisposable interface — explained fully in Chapter 5 — so that these resources can be reclaimed deterministically. Such code might look like this:

 using (Foo f = new Foo()) {     // Do something interesting... } 

The semantics of the using statement are such that the variable inside the parenthesis will have its Dispose method called at the end of the block, regardless of whether exit is normal or due to an exception from the block's body. But if an exception occurs during the call to Foo's constructor, f will never get assigned a value! The result is that Dispose will never get called, and hopefully f's finalizer will clean up the resources sometime later on. Exceptions, finalization, and resource management are discussed in detail in Chapter 3.

Properties

A property is metadata syntactic sugar. There is no special support for properties in the CTS, but they do have both library and language support, and represent a very convenient way of defining methods for accessing and manipulating state on an instance. They are marked with specialname in the IL to indicate their special status. These are often called getters and setters. Instead of exposing fields directly to callers, you can encapsulate access to them in small methods; these methods can perform lazy initialization, for example, or any other logic that might be appropriate.

The C# language has explicit support for properties as a first class language feature. For example, consider an example that exposes two public fields:

 class PropertyExample {     public int X;     public int Y; } 

This example will function just fine, but it breaks one of the fundamental principles of OO design: encapsulation. A better way to implement this is by controlling access to these fields through methods. In languages such as Java, this would be done through the convention of providing int getX(), void setX(int x), and similar methods. But with C#, you need not rely on conventions, for example:

 class PropertyExample {     private int x;     private int y;     public int X     {         get { return x; }         set { x = value; }     }     public int Y     {         get { return y; }         set { y = value; }     } } 

This gives callers the means to access and set the values of X and Y but also gives the PropertyExample type a way to control such access. All property accesses go through the get and set bodies. If you look at the IL that gets generated, you'll see get_X, set_X, get_Y, and set_Y methods; these are abstracted away by the C# compiler.

Note that you needn't provide both a getter and a setter but instead can just offer one of the two. Providing a getter but not a setter, for example, has the effect of creating a read-only property. Lastly, it might be obvious, but the accessibility of the fields to which the properties refer should usually be more restricted than that of the properties themselves. If you supply a property yet clients can still directly access the fields, you haven't taken advantage of the primary value of properties: encapsulation.

Indexer Properties

A special variant on properties called an indexer property may be supplied. Consider this example:

 class Customer {     private Dictionary<int, Order> orders;     public Order this[int id]     {         get { return orders[id]; }         set { orders[id] = value; }     } } 

This is essentially a default property for a type, indicated by the System.Reflection.DefaultMember Attribute in the IL. This style of property enables callers to access your object using special syntax. For example, C# permits callers to use an array-like syntax for this:

 Customer c = /*...*/; Order o = c[10010]; 

Indexer properties are also special in that they can be overloaded through the parameters. You may also use multiple parameters for input.

Mixed Mode Accessibility (Language Feature)

Properties are composed of two bits of code: the getter and setter. As of C# 2.0, you can define these to have separate levels of accessibility. Of course, this isn't a change to the CTS — you could have previously written this all in IL — but is an improvement for the C# language itself. For example, it's quite common to provide a public getter and a protected setter. The following syntax enables just that:

 class PropertyExample {     private int x;     private int y;     public int X     {         get { return x; }         protected set { x = value; }     }     public int Y     {         get { return y; }         protected set { y = value; }     } } 

Notice that the property declaration still has a default accessibility, while the setter has a more restrictive accessibility declared right at the point where you say set.

Events

CLR types may contain events. Much like properties, events are more of a convention and pattern than a feature of the CTS itself. As such, they too are marked with the specialname token. Events enable clients to subscribe to get notifications about interesting occurrences having to do with a type. For example, Windows Forms uses events quite extensively to enable programmers to respond to various UI events, such as a button click, the window closing, the user typing a key in a textbox, and so on. They can, of course, also be used for communication associated with your object models.

C# makes it simple to express events. The event keyword is used to declare a member on a class, for example:

 using System; class Foo : IDisposable {     public event EventHandler OnInitialized;     public event EventHandler OnDisposed;     public void Init()     {         // Initialize our state...         EventHandler onInit = OnInitialized;         if (onInit != null)             onInit(this, new EventArgs());     }     public void Dispose()     {         // Release our state...         EventHandler onDisp = OnDisposed;         if (onDisp != null)             onDisp(this, new EventArgs());     } } 

The type Foo exposes two events, OnInitialized and OnDisposed, to which consumers may subscribe. Each is of type EventHandler, which is a delegate type in the System namespace. Delegates are a crucial part of the CTS, discussed later in this chapter. In this example, we accepted the default implementation for subscription and removal of events, although C# also offers a more powerful syntax to write your own implementation. We signal that the events have occurred by calling the delegates represented by our events. If nobody has subscribed, the delegate will be null; thus, we check before trying to make the call (to avoid a NullReferenceException).

Clients may then subscribe to these events:

 using (Foo f = new Foo()) {     f.OnInitialized += delegate { Console.WriteLine("init"); };     f.OnDisposed += delegate { Console.WriteLine("disposed"); };     // ...     f.Init();     // ... } 

The client here subscribes to the events using C#'s += syntax. Notice that the code used to respond to an event is represented by passing an anonymous delegate. This exposes a form of user extensibility in your APIs.

The representation of events in the IL is not quite as straightforward as C# makes it look. It gets compiled into an add_OnXxx and remove_OnXxx method, each of which accepts an EventHandler instance. The list of event subscriptions is stored inside a Delegate, which offers methods like Combine and Remove to participate with events.

Operator Overloading

If there has been one feature that has been the source of the greatest contention and confusion in programming language design in recent years, it certainly must be operator overloading. We're very familiar with the operators our language provides. From unary to binary to conversion operators, these are things which we ordinarily take for granted in our language. For example, we always assume that 1 + 1 is 2, among other things. But what if a user could redefine these operators for his or her special types?

Operator overloading enables you to do just that. The System.Decimal class is a great example of good use of operator overloading. Most users simply do not know that a decimal doesn't have as high a status in the CTS as, say, an Int32 does. This is all masked by proper use of operator overloads.

There is a large number of operators data type authors may overload, essentially a set of controlled language extensibility hooks. Overloading an operator for a type is performed by defining a static method on that type with a special name and signature. For example, consider this C# type, which overloads the addition operator:

 class StatefulInt {     private int value;     private State state;     public StatefulInt(int value, State state)     {         this.value = value;         this.state = state;     }     public static StatefulInt operator+(StatefulInt i1, StatefulInt i2)     {         return new StatefulInt(i1.value + i2.value,             i1.state.Combine(i2.state));     }     public static StatefulInt operator+(StatefulInt i1, int i2)     {         return new StatefulInt(i1.value + i2, i1.state);     } } 

The result is that users can add two StatefulInts together, or alternatively add an int to a StatefulInt:

 StatefulInt i1 = new StatefulInt(10, ...); StatefulInt i2 = new StatefulInt(30, ...); StatefulInt i3 = i1 + i2; // result has value == 40 StatefulInt i4 = i2 + 50; // result has value == 80 

The following table describes the internal special name used in the IL, and the corresponding operator in C#, VB, and C++/CLI:

Special Name

Type

C#

VB

C++/CLI

op_Decrement

Unary

--

n/a

--

op_Increment

Unary

++

n/a

++

op_UnaryNegation

Unary

-

-

-

op_UnaryPlus

Unary

+

+

+

op_LogicalNot

Unary

!

Not

!

op_AddressOf

Unary

&

AddressOf

&

op_OnesComplement

Unary

~

Not

~

op_PointerDereference

Unary

* (unsafe)

n/a

*

op_Addition

Binary

+

+

+

op_Subtraction

Binary

-

-

-

op_Multiply

Binary

*

*

*

op_Division

Binary

/

/

/

op_Modulus

Binary

%

Mod

%

op_ExclusiveOr

Binary

^

Xor

^

op_BitwiseAnd

Binary

&

And

&

op_BitwiseOr

Binary

|

Or

|

op_LogicalAnd

Binary

&&

And

&&

op_LogicalOr

Binary

||

Or

||

op_Assign

Binary

=

=

=

op_LeftShift

Binary

<<

<<

<<

op_RightShift

Binary

>>

>>

>>

op_Equality

Binary

==

=

==

op_Inequality

Binary

!=

<>

!=

op_GreaterThan

Binary

>

>

>

op_GreaterThanOrEqual

Binary

>=

>=

>=

op_LessThan

Binary

<

<

<

op_LessThanOrEqual

Binary

<=

<=

<=

op_MemberSelection

Binary

.

.

. or ->

op_PointerToMemberSelection

Binary

n/a

n/a

.* or ->

op_MultiplicationAssignment

Binary

*=

*=

*=

op_SubtractionAssignment

Binary

-=

-=

-=

op_ExclusiveOrAssignment

Binary

^=

n/a

^=

op_LeftShiftAssignment

Binary

<<=

<<=

<<=

op_RightShiftAssignment

Binary

>>=

>>=

>>=

op_ModulusAssignment

Binary

%=

n/a

%=

op_AdditionAssignment

Binary

+=

+=

+=

op_BitwiseAndAssignment

Binary

&=

n/a

&=

op_BitwiseOrAssignment

Binary

|=

n/a

|=

op_Comma

Binary

,

n/as

,

op_DivisionAssignment

Binary

/=

/=

/=

op_Implicit

Conversion

n/a (implicit)

n/a (implicit)

n/a (implicit)

op_Explicit

Conversion

CInt, CDbl, ..., CType

(type)

(type)

Depending on the type of operator — unary, binary, or conversion — the signature must accept the appropriate number of arguments. Return value depends on the operator itself. Please refer to SDK documentation for more details on overloading a specific operator.

Coercion

Coercion is the process of copying a scalar value from one type into an instance of another. This must occur when the target of an assignment is not of the same type of the right-hand, or assigned, value. For example, consider assigning a 32-bit float to a 64-bit double. We say coercion is widening if the target is of a larger storage capacity, as in the 32-bit to 64-bit conversion specified above, while it is narrowing if the target is of a lesser capacity. Widening coercions are implicitly supported by the runtime, while narrowing coercions can result in loss of information and are thus represented as explicit conversions.

Subclassing and Polymorphism

Types can derive from other types, forming a special relationship between two types. We say type B is a subclass of type A if B derives from A. In IL terminology, B extends A. In this example, A is B's base type (a.k.a. superor parent type). Types can only have one immediate base type in the CTS; in other words, multiple inheritance is not supported. Interfaces, a topic we discuss in this section, enable multiple interface inheritance, but this is subtly different from what is normally meant as multiple inheritance. Because userdefined structs cannot derive from an arbitrary type (they implicitly derive from System.ValueType and are sealed by default), we just go ahead and say subclass instead of using more precise terminology.

You can express a subtype relation in C# as follows:

 class A {} class B : A {} class C : A {} 

In this example, both B and C are subclasses of A. Of course, we can then go ahead and create additional subclasses of A, or even subclasses of B and C. In IL this looks as follows:

 .class private auto ansi beforefieldinit A extends [mscorlib]System.Object {} .class private auto ansi beforefieldinit B extends A {} .class private auto ansi beforefieldinit C extends A {} 

When B is a subclass of A, we say that B is polymorphic with respect to A. All this means is that, because A subclass inherits all of the publicly visible traits of its base type, an instance of B can be treated just like an instance of A in a type-safe manner. The reverse is of course not true. We discuss inheritance in more detail shortly.

Styles of Inheritance

There are two primary styles of inheritance available in the CTS: implementation and interface inheritance. Which you use does not impact the ability to treat subtypes in a polymorphically safe manner. The primary difference is the way in which subclasses receive copies of base type members. We'll see what this means below. Private interface inheritance is also supported, although private implementation inheritance is not.

Implementation Inheritance

This is the default inheritance scheme in the CTS whenever you create a subclass of another type. Implementation inheritance means that a subclass gets a copy of all of its base type's nonprivate members, including method implementations. So the methods defined on the base type, for example, are immediately callable on an instance of the subclass as you would expect. Depending on whether methods are virtual, a subclass might choose to override these methods to supply their own implementation instead of keeping the base type's existing version. Virtual methods and overriding are topics we discuss in more detail shortly.

As an example of subclassing, consider two types A and B:

 class A {     public void Foo()     {         Console.WriteLine("A::Foo");     } } class B : A {     public void Bar()     {         Console.WriteLine("B::Bar");     } } 

Here, B derives from A — using the <Subclass> : <BaseType> syntax — inheriting the public method Foo and extending A by adding a new method Bar. The result is that the type A has a single method Foo, and the type B has two methods, Foo and Bar. Calling Foo on an instance of B just works, and does the same exact thing as A's version of Foo (prints out "A::Foo") — the creator of B didn't have to do anything special to enable this. This is a result of the subclass inheriting both the interface and implementation of the base type's definition of Foo.

Interface Inheritance

A subclass inheriting from a base type using interface inheritance only gets API placeholders in its public surface area for the base type's nonprivate members. That is, no implementation comes along with inherited methods, only the signature. The subclass is usually required to manually implement these members. There are two cases where the CTS supports interface inheritance: abstract classes and interfaces. These are discussed below.

Abstract Classes

An abstract class is a class that cannot be instantiated. It represents a base class for other types to derive from. If a class isn't abstract, it's said to be concrete (although this is the default, so we ordinarily don't mention the concreteness). Individual methods on an abstract class can also be marked as abstract, meaning that they don't supply an implementation. Properties cannot be marked abstract. Abstract methods are what most C++ programmers know as pure virtual methods. An abstract class is not required to have abstract methods, but a type with abstract methods must be abstract.

For example, all four of these classes can be marked as abstract. Only examples 2 and 4 must be marked as abstract, because they contain abstract members:

 // Abstract, no members abstract class AbstractType1 {} // Abstract, with only abstract members abstract class AbstractType2 {     public abstract void Foo();     public abstract void Bar(); } // Abstract, with only non-abstract members abstract class AbstractType3 {     public void Foo()     {         Console.WriteLine("AbstractType3::Foo");     }     public void Bar()     {         Console.WriteLine("AbstractType3::Bar");     } } // Abstract, with a mix of abstract and non-abstract members abstract class AbstractType4 {     public void Foo()     {         Console.WriteLine("AbstractType4::Foo");     }     public abstract void Bar(); } 

When a type subclasses an abstract class, it inherits all of the base type's members, as is the case with ordinary classes. For methods that supply an implementation, that implementation is also inherited. But for methods that are marked abstract, the subclass must either provide an implementation for every single one or it may declare that it, too, is an abstract class.

For example, consider a class deriving from AbstractType4 from our example above:

 class ConcreteType : AbstractType4 {     public override void Bar()     {         // We must provide an implementation here, or else mark         // 'ConcreteType' as abstract, too.         Console.WriteLine("ConcreteType::Bar");     } } 

Abstract types are marked with the abstract metadata token in the IL generated, and an abstract method is implicitly marked as both abstract and virtual. Thus, a derived class acts as though it were overriding any ordinary virtual method to supply an implementation. As with any other virtual method, an override cannot redefine a method's visibility.

Interfaces

An interface is a special type containing no method implementations but that can be used by other types to declare support for some set of public APIs. For example, the following interface defines a method and three properties:

 interface ICollection : IEnumerable {     void CopyTo(Array array, int index);     int Count { get; }     bool IsSynchronized { get; }     object SyncRoot { get; } } 

Note

It is customary to name interfaces to start with a capital I, a throwback to the days of COM. All interfaces in COM — also by convention — started with I.

Much like abstract methods on an abstract class, we don't specify implementations for members; unlike abstract classes, an interface cannot contain any implementations whatsoever. Notice also that the interface itself derives from another interface. An interface which derives from another interface inherits the interface of all members on the base interface, meaning that an implementer must supply concrete versions of both the base interface's and the new interface's members.

When a type implements an interface, it must provide support for the entire interface. Although, you can use an abstract class and avoid implementing specific members by marking them abstract. This type can then be referenced by and accessed polymorphically through variables typed as that interface. This is pure interface inheritance, as there are no implementations inherited. For example, consider this example of a simpler interface and an example implementation:

 interface IComparable {     int CompareTo(object obj); } struct Int32 : IComparable {     private int value;     public int CompareTo(object obj)     {         if (!(obj is int))             throw new ArgumentException();         int num = (int)obj;         if (this.value < num)             return -1;         else if (this.value > num)             return 1;         return 0;     }     // ... } 

With this definition, an instance of Int32 can be used wherever an IComparable was expected, for example as an argument to a method, to store in a local variable or a field, and so on. The same is true of any base types the interface derived from. For example, ICollection implements IEnumerable; thus, anything that implements ICollection can be treated as either an ICollection or an IEnumerable. Because Int32 is a struct, it must be boxed before it can be passed around as an IComparable.

There are a few additional interesting things to note:

The fact that a type implements an interface is part of its public contract. This means any methods implemented for that interface must be public. The one instance where this isn't the case is with private interface implementation, detailed further below, in which case the method is still accessible through interface method calls but is actually still private.

You can choose whether to mark the implementing methods as virtual or leave them with the default of final. Note that because of interface reimplementation, discussed below, you can never prevent further derived subclasses from creating new method slots for the same interface.

Interface Method Dispatch

An interface call is roughly equivalent to making a virtual method call. While the IL looks the same — that is, it gets emitted as a callvirt instruction — there is actually an extra layer of indirection in the implementation necessary to perform the method dispatch. If you look at the machine code that the JIT produces, it includes a lookup into the interface map (which hangs off the method table) in order to correlate the implementation of an interface to the correct slot in the method table. In most circumstances, this performance overhead shouldn't be a concern.

As noted earlier — and it applies here too — to make a virtual method invocation on a value type requires the value to be boxed first. Constrained calls, a new feature in 2.0, enable the runtime to optimize this away in some circumstances. This feature is described further in Chapter 3.

Multiple Inheritance

The CTS does not permit a single type to inherit from more than one base class. This is a design decision that was made early on in the creation of the CLR and the .NET Framework, and deviates from some languages that permit it, most notably C++. Multiple inheritance is wisely recognized as a poor object-oriented programming practice, although treating instances of a single type as a number of polymorphically compatible types is a powerful capability.

Multiple interface implementation is a compromise. It enables you to have multiple interface inheritance, without all of the problems associated with multiple implementation inheritance. For example, this code declares Implementer to implement both IFoo and IBar:

 interface IFoo {     void Foo(); } interface IBar {     void Bar(); } class Implementer : IFoo, IBar {     public void Foo()     {         Console.WriteLine("Implementer::Foo");     }     public void Bar()     {         Console.WriteLine("Implementer::Bar");     } } 

This will now permit an instance of Implementer to be treated as an IFoo or IBar.

Private Interface Inheritance

Historically, languages have permitted private inheritance. In C++, you can inherit from a type without being polymorphically compatible with that type. It's just a convenient way to reuse an implementation. In the CTS, you cannot do private implementation inheritance. But you can use private interface inheritance.

Private interface inheritance is really just a way to hide methods from a type's public API. They are compiled into private methods but are actually accessible through a type's interface map. In other words, they can only be called through a reference typed as the interface on which the method is defined. An example will make this easier to understand:

 class PrivateImplementer : IFoo {     void IFoo.Foo()     {         Console.WriteLine("PrivateImplementer::IFoo.Foo");     } } 

In this case, PrivateImplementer is publicly known to implement IFoo. Thus, an instance can be treated polymorphically as an instance of IFoo. But you cannot actually call Foo on it unless you do treat it as an IFoo. This code demonstrates this:

 PrivateImplementer p = new PrivateImplementer(); p.Foo(); // This line will fail to compile IFoo f = p; f.Foo(); 

You can select individual methods of an interface to implement privately. For instance, if PrivateImplementer implemented IFooBar, it might choose to implement Foo privately, but Bar publicly using the ordinary syntax.

In practice, there aren't many common cases where you would use private implementation. The System.Collections.Generic library uses this approach to secretly implement all of the legacy System.Collections weakly typed interfaces. This makes backwards compatibility "just work," for example passing an instance of List<T> to a method that expects an IList will work just fine. In this specific example, cluttering the new type APIs would have been a pity (there are quite a few methods necessary for the weakly typed interoperability).

Private Inheritance Accessibility

One hidden problem when using private interface inheritance is the fact that the implemented methods are generated as private. They can be accessed through the interface map, but it does mean that subclasses will have no way to access the methods. For example, reimplementing an interface and "chaining" to the base implementation is a common practice. But with private implementations, you cannot do this.

Consider if we had a PrivateExtender type that wanted to redefine Foo, but still make use of the base type's version:

 class PrivateExtender : PrivateImplementer, IFoo {     void IFoo.Foo()     {         base.Foo(); // This line fails to compile         Console.WriteLine("PrivateExtender::IFoo.Foo");     } } 

We'd normally accomplish this by making a call through the base keyword in C#. But this won't compile, because Foo is private on PrivateImplementer. The syntax you might imagine for this situation could look like ((IFoo)base).Foo(), but alas that doesn't compile (and indeed there isn't any representation for it in IL). You could write ((IFoo)this).Foo(), but that's just going to get you into an infinite loop (since it calls virtually to PrivateExtender's copy — the same method doing the call). You just can't write the code to do it!

Interface Reimplementation

You can generally prevent subclasses from redefining a method by marking methods as final (or not making them virtual in the first place). A subclass can always mark their method as newslot and provide a new method that matches an existing method's signature. But when a virtual call is made to the base type's version, this new definition won't be dispatched to. This is because the vtable differentiates between the two — in other words, it creates a new slot for both.

However, with interfaces, there is only a single interface map per interface for any given type. This means that calling through the interface map always goes to the furthest derived version, regardless of whether subclasses have defined their own implementations as final or virtual. This can be surprising.

For example, consider the base and subclass:

 class FooBase : IFoo {     public void Foo()     {         Console.WriteLine("FooBase::Foo");     }     public void Bar()     {         Console.WriteLine("FooBase::Bar");     } } class FooDerived : FooBase, IFoo {     public new void Foo()     {         Console.WriteLine("FooDerived:Foo");     }     public new void Bar()     {         Console.WriteLine("FooDerived::Bar");     } } 

If you wrote some code that called Foo and Bar in different ways, you might or might not be surprised at the results. When looked at in totality, they make intuitive sense. But a lot of people are still surprised that a type can completely redefine a base type's implementation of an interface although the initial implementation was protected.

 FooDerived d = new FooDerived(); FooBase b = d; IFoo i = d; b.Foo();   // Prints "FooBase::Foo" b.Bar();   // Prints "FooBase::Bar" d.Foo();   // Prints "FooDerived::Foo" d.Bar();   // Prints "FooDerived::Bar" i.Foo();   // Prints "FooDerived::Foo" 

Choosing between Abstract and Interface

Abstract classes and interfaces offer similar functionality, but both have unique pros and cons. Because abstract classes can offer implementations in addition to just an interface, they can make versioning much simpler. Thus, they are the default recommendation, although there are some scenarios in which interfaces make sense too.

As an example of the versioning difficulties they can introduce, imagine that you have released an abstract class and interface with two methods, void A() and void B(). You are basically stuck with them. That is, you cannot remove them without breaking classes that had derived from your class or implemented your interface. With abstract classes, however, you can extend your class over time. If you wanted to add a new void C() method, for example, you could add this on the abstract class with some default implementation. Similarly, if you want to add convenience overloads, you are free to do so with abstract classes. With interfaces, you simply cannot.

Conversely, abstract classes take over derived classes' type hierarchy. A class can implement an interface yet still maintain some type hierarchy that makes sense. With abstract classes, this is not so. Furthermore, with interfaces you achieve multiple interface inheritance, whereas with abstract classes you cannot.

Sealing Types and Methods

A type can be marked as sealed meaning that no further derived types can be created. The C# surfaces this feature with the sealed keyword:

 sealed class Foo {} 

No subclasses of Foo can be created. All custom value types are implicitly sealed due to the restriction that user defined value types cannot be derived from.

Also note that a type which is both sealed and abstract can by its very definition never have a concrete instance. Therefore, these are considered static types, in other words types that should only have static members on them. The C# language keyword for static classes uses this very pattern in the IL, that is, these two lines of code are semantically equivalent:

 static class Foo { /*...*/ } abstract sealed class Foo { /*...*/ } 

The C# compiler conveniently ensures that you do not accidentally add instance members to a static class, avoiding creating completely inaccessible members.

Individual virtual methods can also be sealed, indicating that further overriding is not legal. Further, derived types may still hide the method through newslotting, however, for example:

 class Base {     protected virtual void Bar() { /*...*/ } } class Derived : Base {     protected override sealed void Bar() { /* ... */ } } 

In C#, the sealed keyword is used to seal methods. In the above example, it means that subclasses of Baz cannot override the method Bar.

Runtime Type Checking

The CLR offers a variety of ways to perform dynamic runtime type checks that look for polymorphic compatibility between an instance and a type. Given an object o and a type T, you can use the cast-class and isint IL instructions to check whether o is of type T or whether its type implements T if it's an interface or if its type is a subtype of T, in which case it is safe to treat it as a T. The C# language surfaces these instructions with casting and its is and as keywords:

 object o = /*...*/; string s1 = (string)o; // Casting uses 'castclass' string s2 = o as string; // 'as' uses 'isinst' if (o is string) { // 'is' also uses 'isinst'     // ... } 

In this example, the cast uses the castclass instruction to dynamically check if the object instance o is of type System.String. If it's not, a CastClassException will be thrown by the runtime. Both the as and is keywords use isinst. This instruction is much like castclass, but it won't generate an exception; it leaves behind a 0 if the type doesn't pass the type check. In the case of as, this means a null will result if the instance isn't the correct type; in the case of is, this same condition results in a false.

Namespaces: Organizing Types

All useful programming environments have a module and packaging system. Aside from assemblies and modules, which provide physical packaging for distribution and reuse — we discuss assemblies further in Chapter 4 — namespaces provide a logical packaging facility. As part of a type's naming, it can also be assigned a namespace. The type's namespace plus its name is called its fully qualified name. All types included in the .NET Framework have namespaces, most of them starting with System, although some product-specific ones start with Microsoft. The CTS has no concept of namespaces. All types and references to them are emitted using their fully qualified name.

Namespaces are hierarchical in nature. We refer to them by their order, so, for example, the root is the first level, the set below that are second level, and so on. For example, consider that the fully qualified name System.Collections.Generic.List<T>. System is the first level, Collections is the second level, Generic is the third level, and List<T> is the type name. Namespaces really have nothing to do technology-wise with the assemblies in which they live. Types in the same namespace can span multiple assemblies. Most developers tend to have a near 1:1 relationship between namespaces and assemblies, however, to make locating types easier. For example, rather than a user having to consider a set of assemblies when looking for a certain type in a certain namespace, having a 1:1 correspondence limits the choice to one.

Defining a Namespace

To place a type inside a namespace in C#, you simply wrap its declaration inside a namespace block:

 namespace MyCompany.FooProject {     public class Foo { /*...*/ }     public struct Bar { /*...*/ }     namespace SubFeature     {         public class Baz { /*...*/ }     } } 

Notice that we have a top-level namespace MyCompany.FooProject in which classes Foo and Bar reside. Their fully qualified names are MyCompany.FooProject.Foo and MyCompany.FooProject .Bar, respectively. We then have a nested namespace, named SubFeature. It's not required to place it lexically within the enclosing namespace; we could have instead typed it out completely in the same or a separate file, that is, as MyCompany.FooProject.SubFeature. It contains a single type, Baz, whose fully qualified name is MyCompany.FooProject.SubFeature.Baz.

Namespace Resolution

Different languages resolve references to types inside namespaces in different manners. For example, in C# you can declare that a certain scope uses a namespace with the using keyword (Imports in VB), and you immediately get to access the types within it without having to type the fully qualified names. Without this feature, you'd need to type all of your source files as follows:

 class Foo {     void Bar()     {         System.Collections.Generic.List<int> list =             new System.Collections.Generic.List<int>();         // ...     } } 

Thankfully with the using keyword you can eliminate having to type so much:

 using System.Collections.Generic; class Foo {     void Bar()     {         List<int> list = new List<int>();         // ...     } } 

Sometimes imported namespaces can have types whose names conflict defined within them. In such cases, you must resort to fully qualified type names. To alleviate this problem, C# enables you to alias namespaces so that you can save on typing. For example, imagine we imported the Microsoft .Internal.CrazyCollections namespace, which just happened to have a List<T> type in it. This would conflict with System.Collections.Generic and must be disambiguated:

 using SysColGen = System.Collections.Generic; using Microsoft.Internal.CrazyCollections; class Foo {     void Bar()     {         SysColGen.List<int> list = new SysColGen.List<int>();         // ...     } } 

Special Types

As depicted in the early part of this chapter, in Figure 2-1, there are a set of special types in the CTS's type hierarchy. They are delegates, custom attributes, and enumerations. Each provides extended type system facilities for writing managed code.

Delegates

Delegates are special types in the CTS that represent a strongly typed method signature. Delegate types are derivatives of the special System.Delegate type, which itself derives from System.ValueType. A delegate can be instantiated and formed over any target method and instance combination where the method matches the delegate's signature. Delegates can be formed over static methods, too, in which case no instance is required. A delegate instance is the CLR's version of a strongly typed function pointer.

Most languages offer syntax to make delegate creation and instantiation simple. For example, in C# a new delegate type is created using the delegate keyword:

 public delegate void MyDelegate(int x, int y); 

This says that we create a new delegate type, named MyDelegate, which can be constructed over methods with void return types and that accept two arguments each typed as int. VB has similar syntax. Such syntax hides a lot of the complexity that compiler authors must go through to generate real delegates, making working with delegates straightforward for users of the language.

Our delegate can then be formed over a target, passed around, and then invoked at some point in the future. Invocation in C# looks like any ordinary function call:

 class Foo {     void PrintPair(int a, int b)     {         Console.WriteLine("a = {0}", a);         Console.WriteLine("b = {0}", b);     }     void CreateAndInvoke()     {         // Implied 'new MyDelegate(this.PrintPair)':         MyDelegate del = PrintPair;         del(10, 20);     } } 

CreateAndInvoke constructs a new MyDelegate, formed over the PrintPair method with the current this pointer as the target, and then invokes it.

CTS Support

The actual IL emitted shows some of the complexities of delegates in the underlying type system:

 struct MyDelegate : System.MulticastDelegate {     public MyDelegate(object target, IntPtr methodPtr);     private object target;     private IntPtr methodPtr;     public internal void Invoke(int x, int y);     public internal System.IAsyncResult BeginInvoke(int x, int y,         System.IAsyncCallback callback, object state);     public internal void EndInvoke(System.IAsyncResult result); } 

The constructor is used to form a delegate over a target object and a function pointer. The Invoke, BeginInvoke, and EndInvoke methods implement the delegate invocation routine and are marked as internal (i.e., runtime in IL) to indicate that the CLR provides the implementation; their IL bodies are left blank. Invoke performs a synchronous invocation, while the BeginInvoke and EndInvoke functions follow the Asynchronous Programming Model pattern, described further in Chapter 10, for asynchronous method invocation.

Notice first that the MyDelegate type breaks one of the rules discussed above, namely that structs cannot derive from types other than ValueType. Delegates have special support in the CTS, so this is allowed. Also notice that MyDelegate derives from MulticastDelegate; this type is the common base for all delegates created in C# and supports delegates that have multiple targets. The section on events describes why this is useful. MulticastDelegate also defines a large set of additional methods that we will ignore for now and that have been omitted from the definition above. Please refer to Chapter 14 for more details on using these methods.

To form a delegate over a target, the IL uses MyDelegate's constructor. The constructor accepts an object for input, which is the this pointer to be passed during method invocation (or null for static methods), and an IntPtr representing the managed function pointer to the CLR method. This is a C-style function pointer that points at the just-in-time compiled code in the runtime. The Invoke signature matches that of the delegate we defined, and the CLR ensures that both statically and dynamically the function pointer contained within the delegate matches this signature.

The CreateAndInvoke code above emits the following sequence of IL:

 ldarg.0    // loads the 'this' for CreateAndInvoke's method ldftn      void Foo::PrintPair(int32, int32) newobj     instance void MyDelegate::.ctor(object, native int) ldc.i4.s   10 ldc.i4.s   20 callvirt   instance void MyDelegate::Invoke(int32, int32) ret 

Notice that it uses the ldftn instruction to load a function pointer to the PrintPair method and then uses the Invoke method defined on MyDelegate to invoke the underlying method. This has the result of indirectly calling the code for PrintPair, passing the values 10 and 20 for its arguments.

Delegates are discussed in much more depth in Chapter 14.

Co- and Contravariance

I've made some simplifications on the rules for binding up until this point. I stated that the return value and parameter types of the target method must match the delegate exactly in order to form a delegate instance over a target. This is technically incorrect. The CLR 2.0 permits so-called covariant and contravariant delegates (although 1.x did not). These terms are well defined in the field of computer science and are forms of type system polymorphism. Covariance means that a more derived type can be substituted where a lesser derived type is expected. Contravariance is the opposite — it means that a lesser derived type can be substituted where a further derived type was expected.

Covariant input is already permitted in terms of what a user can supply to a method. If your method expect BaseClass and somebody gives you an instance of DerivedClass (which subclasses BaseClass), the runtime permits it. This is bread and butter object-oriented polymorphism. Similarly, output can be contravariant in the sense that if the caller expects a lesser derived type, there is no harm in supplying an instance of a further derived type.

Note

The topics of co- and contravariance get relatively complex quickly. Much of the literature says that contravariant input and covariant output is legal, which happens to be the exact opposite of what I just stated! This literature is usually in reference to the ability to override a method with a co-/contravariant signature, in which case it's true. Derived classes can safely relax the typing requirements around input and tighten them around output if it deems it appropriate. Calling through the base version will still be type-safe. The CLR doesn't natively support co- and contravariance in this manner.

For delegates, however, we are looking at the problem from a different angle: We're simply saying that anybody who makes a call through the delegate might be subject to more specific input requirements and can expect less specific output. If you consider that calling a function through a delegate is similar to calling through a base class signature, this is the same policy.

As an example, consider the following definitions:

 class A { /* ... */ } class B : A { /* ... */ } class C : B { /* ... */ } B Foo(B b) { return b; } 

If we wanted to form a delegate over the method Foo, to match precisely we'd need a delegate that returned a B and expected a B as input. This would look MyDelegate1 below:

 delegate B MyDelegate1(B b); delegate B MyDelegate2(C c); delegate A MyDelegate3(B b); delegate A MyDelegate4(C c); 

But we can use covariance on the input to require that people calling through the delegate supply a more specific type than B. MyDelegate2 above demonstrates this. Alternatively, we could use contravariance to hide the fact that Foo returned a B, and instead make it look like it only returns an A, as is the case with MyDelegate3. Lastly, we could use both co- and contravariance simultaneously as shown with MyDelegate4. All four of these delegate signatures will bind to the Foo method above.

Anonymous Delegates (Language Feature)

This is a feature of the C# 2.0 language, not of the CLR itself. But anonymous delegates are so useful and pervasive that it's worth a brief mention in this chapter. Due to the ease with which delegates permit you to pass method pointers as arguments to other methods, it's sometimes preferable to simply write your block of code inline rather than having to set up another method by hand. Anonymous delegates permit you to do this. It's purely syntactic sugar.

Consider a method that takes a delegate and applies it a number of times:

 delegate int IntIntDelegate(int x); void TransformUpTo(IntIntDelegate d, int max) {     for (int i = 0; i <= max; i++)         Console.WriteLine(d(i)); } 

If we wanted to pass a function to TransformUpTo that squared the input, we'd have to first write an entirely separate method over which we'd form a delegate. However, in 2.0, we can use anonymous delegates to accomplish the same thing:

 TransformUpTo(delegate(int x) { return x * x; }, 10); 

The C# compiler generates an anonymous method in your assembly that implements the functionality indicated inside the curly braces. The compiler is smart enough to deduce that the function returns an integer (because it's used in a context that expected a function returning an integer), and the parameter types are specified explicitly in the parenthesis following the delegate keyword.

We won't spend too much time on this feature. But suffice it to say, it's very complex and very powerful. You can capture variables inside the delegate that are lexically visible. The compiler does a lot of work to ensure that this works correctly. Take a look at the IL that gets generated if you'd like to appreciate the work it's doing for you. Consider this example:

 delegate void FooBar(); void Foo() {     int i = 0;     Bar(delegate { i++; });     Console.WriteLine(i); } void Bar(FooBar d) {     d(); d(); d(); } 

It shouldn't come as a surprise that the output of calling Foo is 3. A local variable i is declared in Foo and set initially to 0. Then we create an anonymous delegate that, when invoked, increments i by one. We pass that delegate to the Bar function, which applies it three times.

If you stop to think about what the compiler is doing here, it's clever and impressive (and perhaps a tad frightening at the same time). The compiler notices that you've accessed a local variable from inside your delegate and responds by hoisting the storage into a heap allocated object. The type of this object is auto-generated by the C# compiler and never seen by your code. It then does the magic to ensure that the references to i in the local scope work with the same object as the delegate.

This is quite nice of the compiler to do, but for the performance-conscious readers, you might worry that this feature can be abused. What appears to be local variable access turns out to actually involve an object allocation and at least two levels of indirection. Your concern would not be without justification, but seldom does it pay off to worry about such micro-performance tuning.

Custom Attributes

We've seen throughout this chapter various IL keywords that compilers use to modify runtime behavior for the type or member they have been applied to. These are pseudo-custom attributes, which are serialized using a fixed size and location in the metadata and usually have their own efficient storage slots in CLR data structures. The CLR intimately knows about these attributes, will notice them, and will respond to them accordingly, based on the documented contract for the specific attribute.

However, the CLR also permits users to attach custom attributes to CLR data types. This is done by creating a new type that derives from System.Attribute, and providing a set of fields and properties the attribute will carry around when instantiated at runtime. A user of your attribute may then attach an instance to an assembly, module, type, or member. The attribute and its instantiation information are serialized into the assembly's metadata and can be rehydrated at runtime. User components may then inspect instances at runtime for the presence of these attributes and react accordingly, much like the runtime does with pseudo-custom attributes.

Here is an example attribute type in C#:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] class MyAttribute : System.Attribute {     private string myString;     private int mySize;     public MyAttribute(string myName)     {         this.myName = myName;         this.mySize = 8; // default     }     public string MyName { get { /*...*/ } }     public int MySize { get { /*...*/ } set { /*...*/ } } } 

A user may then attach this to a class or method (the two legal targets based on AttributeUsage, which itself is an attribute). This is done in C# with the [ Attribute ] syntax and VB with the < Attribute > syntax, for example:

 [MyAttribute("MyFoo")] class Foo {     [MyAttribute("MyFoo::MyBar", MySize = 16)]     public void Bar()     {     } } 

Notice that C#'s attribute syntax permits you to call the constructor and supply a set of property values. The System.Reflection.MemberInfo type and all of its derived types (Type, MethodInfo, PropertyInfo, FieldInfo, and so forth) then enable components to read a type system component's attributes using the GetCustomAttributes method. This small snippet of code reads Foo and Foo.Bar's attributes:

 Type fooType = typeof(Foo); object[] myAttrOnType = fooType.GetCustomAttributes(     typeof(MyAttribute), false); if (myAttrOnType.Length > 0) {     // Do something special...it has a MyAttribute. } MethodInfo fooBarMethod = fooType.GetMethod("Bar"); foreach (object attr in fooBarMethod.GetCustomAttributes(false)) {     if (attr.GetType() == typeof(MyAttribute))     {         // Has a MyAttribute, do something about it.     } } 

This was a very quick overview. Please refer to Chapter 14, which discusses more about these APIs, the internals of custom attributes, such as their storage format, and more about how they relate to dynamic programming.

Enumerations

An enumeration (a.k.a. enum) is a special type that maps a set of names to numeric values. Using them is an alternative to embedding constants in your code and provides a higher level of nominal type safety. Enum types look much like ordinary types in metadata, although they abide by a strict set of rules as defined in the CTS. For example, defining methods or constructors on enum types is prohibited, as is implementing interfaces, and they can only have a single field to represent the value. The rules exist so that enums are performant and so languages can treat them in a certain manner. Thankfully, most languages have syntactic support to abstract these rules away (C# included).

An enum type itself derives from System.Enum, which itself derives from System.ValueType. Each is backed by a specific primitive data type, one of Boolean, Char, Byte, Int16, Int32, Int64, SByte, UInt16, UInt32, UInt64, IntPtr, UIntPtr, Single, and Double. Int32 is used as the default in most languages; it provides a good compromise between storage and capability to extend the enum in the future to support more and more values.

An instance of a given enum contains a single field representing its value. Because enums are value types, having an instance of one is essentially the same as having a value of its backing store type, except that you can refer to it by type name, and they can be coerced back and forth rather simply.

In C# you simply write the following to create a new enum:

 enum Color : byte {     Red,     Green,     Blue } 

The part specifying the enum's type as byte is optional. The C# compiler handles the necessary translation into metadata, which follows the above rules:

 .class private auto ansi sealed Color     extends [mscorlib]System.Enum  {   .field public specialname rtspecialname uint8 value__   .field public static literal valuetype Color Red = uint8(0x00)   .field public static literal valuetype Color Green = uint8(0x01)   .field public static literal valuetype Color Blue = uint8(0x02) } 

The Color enum could then be used in a program. For example, imagine that we prompted a user to enter in his or her favorite color. We could use the enum to present the list, parse the input, and then pass around the selection to various routines:

 class ColorPick {     static void Main()     {         Color favorite = SelectFavoriteColor();         RespondToFavoriteColor(favorite);     }     static Color SelectFavoriteColor()     {         Color favorite = (Color)(0xff);         // Loop until a valid color has been selected.         do         {             // Display the prompt and a list of valid colors.             Console.Write("Please enter your favorite color (");             foreach (string name in Enum.GetNames(typeof(Color)))                 Console.Write("{0} ", name);             Console.Write("): ");             string input = Console.In.ReadLine();             try             {                 favorite = (Color)Enum.Parse(typeof(Color), input, true);             }             catch (ArgumentException)             {                 // User's input didn't match an enum name.                 Console.WriteLine("Bad input, please choose again!");                 Console.WriteLine();             }         }         while (favorite == (Color)(0xff));         return favorite;     }     static void RespondToFavoriteColor(Color c)     {         // Notice that C# enables you to switch over an enum.         switch (c)         {             case Color.Red:                 // Do something for people that like 'Red'.                 // ...                 break;             case Color.Green:                 // Do something for people that like 'Green'.                 // ...                 break;             case Color.Blue:                 // Do something for people that like 'Blue'.                 // ...                 break;             default:                 // An unrecognized color got passed in.                 // This is probably an error! But we need to handle it.                 break;         }     } } 

Notice the use of convenience static methods like GetNames and Parse on the Enum type itself. We'll explore the Enum class in a bit more detail farther below.

Flags-Style Enumerations

More often than not, values for an enum will be exclusive. That is, only one selection from the list of possible values is valid for any single instance of the enum. Sometimes, however, programs must represent a single instance as a combination of values. An enum type may be annotated with the System .FlagsAttribute custom attribute, causing languages that recognize this to permit combination and extraction of individual values for a single instance. For example, consider a set of permissions for file-based operations:

 [Flags] enum FileAccess {     Read = 1,     Write = 2,     ReadWrite = 3 } 

It's quite common to open a file for both Read and Write simultaneously. Using a flags-style enum enables you to represent this idea. Notice the numeric value for Read and Write are power of two, starting with 1. This is because combining or extracting values from a single instance is done using a bitwise AND or OR. Additional values would occupy 4, 8, 16, 32, and so on. The C# compiler in particular doesn't auto-number these for you; you must do it manually; otherwise, you will end up with the default sequential numbering scheme, which will not compose correctly with bitwise operations.

Notice that to represent the ability to Read and Write, the two independent values are ORed together. 1 | 2 is 3, hence the convenient value ReadWrite; it enables programmers to write the latter instead of the former in this example, which is more convenient and readable:

 FileAccess rw1 = FileAccess.Read | FileAccess.Write; // value '3' FileAccess rw2 = FileAccess.ReadWrite; // value '3' 

With flags-style enum values, you obviously cannot test for equality to determine whether an instance (with a combined set of values) contains a specific value. If you are testing a FileAccess instance for Read access, the natural thing is to say this:

 FileAccess fa = /*...*/; if (fa == FileAccess.Read)     // permit read else     // deny access 

But unfortunately, if fa were FileAccess.ReadWrite, the test would fail and access would be denied. In most cases, this is the wrong behavior. Instead, you must use a bitwise AND to extract individual values from the instance:

 FileAccess fa = /*...*/; if ((fa & FileAccess.Read) != 0)     // permit read else     // deny access 

This check has the correct result, that is, that it the read check succeeds when a FileAccess.ReadWrite is passed for the value.

Type Safety Limitations

I've mentioned a couple times now that enums add type safety where traditionally out-of-range numbers could be passed in instead. But this is subtly misleading. Because an enum is nothing but a simple primitive data type underneath, instances can be manufactured that hold values with no name mapping. In the example at the beginning of this section with the Color enum with Red (0), Green (1), and Blue (2), for example, we can create an instance that has a value of 3!

 Color MakeFakeColor() {     return (Color)3; } 

There's not much you can do to prevent this. All you can do is to take precautions when accepting an enum instance from an untrusted source. For example, if you've authored a public API that takes an enum as input, it must always check that it is in the valid range before operating on it. To aid you in this task, the System.Enum type defines a static helper method, IsDefined. It returns a Boolean value to indicate whether the supplied value is inside the defined range for the specific enum:

 void AcceptColorInput(Color c) {     // Ensure we check the input enum value for validity.     if (!Enum.IsDefined(typeof(Color), c))         throw new ArgumentOutOfRangeException("c");     // OK to work with the input here, we've validated it.     // ... } 

If somebody were to call AcceptColorInput with an intentionally invalid instance of Color, the method would throw an exception instead of failing in an unpredictable way:

 AcceptColorInput(MakeFakeColor()); 

This approach doesn't quite work with flags enums. An instance of a flags enum that represents a combination of values will not itself be a valid selection from the range of the enum's values. But usually this is acceptable. Presumably, the method body won't be doing a switch or a check for precise equality. Instead, all of its bitwise checks will fail, probably leading it to a branch of program logic where a failure is initiated in a controlled manner.

Other Helper Methods

The System.Enum type has a set of useful methods for dealing with enums, particularly in the areas of parsing, validating, and generating lists of valid names or values. You will notice that most of the methods still don't have generics-based versions in 2.0, although it would make perfect sense; instead, most accept the type of the precise enum as a Type parameter. We've seen most of these methods in use already in the above code snippets, and all others are easy to figure out.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net