DekGenius.com
[ Team LiB ] Previous Section Next Section

2.6 The CTS and CLS

Having seen the importance of metadata and IL, let's examine the CTS and the CLS. Both the CTS and the CLS ensure language compatibility, interoperability, and integration.

2.6.1 The Common Type System (CTS)

Because .NET treats all languages as equal, a class written in C# should be equivalent to a class written in VB.NET, and an interface defined in Managed C++ should be exactly the same as one that is specified in Managed COBOL. Languages must agree on the meanings of these concepts before they can integrate with one another. In order to make language integration a reality, Microsoft has specified a common type system by which every .NET language must abide. In this section, we outline the common types that have the same conceptual semantics in every .NET language. Microsoft .NET supports a rich set of types, but we limit our discussion to the important ones, including value types, reference types, classes, interfaces, and delegates.

2.6.1.1 Value types

In general, the CLR supports two different types: value types and reference types. Value types represent values allocated on the stack. They cannot be null and must always contain some data. When value types are passed into a function, they are passed by value, meaning that a copy of the value is made prior to function execution. This implies that the original value won't change, no matter what happens to the copy during the function call. Since intrinsic types are small in size and don't consume much memory, the resource cost of making a copy is negligible and outweighs the performance drawbacks of object management and garbage collection. Value types include primitives, structures, and enumerations; examples are shown in the following C# code listing:

int i;                     // Primitive
struct Point { int x, y; } // Structure
enum State { Off, On }     // Enumeration

You can also create a value type by deriving a class from System.ValueType. One thing to note is that a value type is sealed, meaning that once you have derived a class from System.ValueType, no one else can derive from your class.

2.6.1.2 Reference types

If a type consumes significant memory resources, then a reference type provides more benefits over a value type. Reference types are so called because they contain references to heap-based objects and can be null. These types are passed by reference, meaning that when you pass such an object into a function, an address of or pointer to the object is passed—not a copy of the object, as in the case of a value type. Since you are passing a reference, the caller will see whatever the called function does to your object. The first benefit here is that a reference type can be used as an output parameter, but the second benefit is that you don't waste extra resources because a copy is not made. If your object is large (consuming lots of memory), than reference types are a better choice. In .NET, one drawback of a reference type is that it must be allocated on the managed heap, which means it requires more CPU cycles because it must be managed and garbage-collected by the CLR. In .NET, the closest concept to destruction is finalization, but unlike destructors in C++, finalization is nondeterministic. In other words, you don't know when finalization will happen because it occurs when the garbage collector executes (by default, when the system runs out of memory). Since finalization is nondeterministic, another drawback of reference types is that if reference-type objects hold on to expensive resources that will be released during finalization, system performance will degrade because the resources won't be released until these objects are garbage-collected. Reference types include classes, interfaces, arrays, and delegates, examples of which are shown in the following C# code listing:

class Car {}                   // Class
interface ISteering {}         // Interface
int[] a = new int[5];          // Array
delegate void Process(  );     // Delegate

Classes, interfaces, and delegates will be discussed shortly.

2.6.1.3 Boxing and unboxing

Microsoft .NET supports value types for performance reasons, but everything in .NET is ultimately an object. In fact, all primitive types have corresponding classes in the .NET Framework. For example, int is, in fact, an alias of System.Int32, and System.Int32 happens to derive from System.ValueType, meaning that it is a value type. Value types are allocated on the stack by default, but they can always be converted into a heap-based, reference-type object; this is called boxing. The following code snippet shows that we can create a box and copy the value of i into it:

int i = 1;         // i - a value type
object box = i;    // box - a reference object

When you box a value, you get an object upon which you can invoke methods, properties, and events. For example, once you have converted the integer into an object, as shown in this code snippet, you can call methods that are defined in System.Object, including ToString( ), Equals( ), and so forth.

The reverse of boxing is of course unboxing, which means that you can convert a heap-based, reference-type object into its value-type equivalent, as the following shows:

int j = (int)box;

This example simply uses the cast operator to cast a heap-based object called box into a value-type integer.

2.6.1.4 Classes, properties, indexers

The CLR provides full support for object-oriented concepts (such as encapsulation, inheritance, and polymorphism) and class features (such as methods, fields, static members, visibility, accessibility, nested types, and so forth). In addition, the CLR supports new features that are nonexistent in many traditional object-oriented programming languages, including properties, indexers, and events.[10] Events are covered in Chapter 8. For now let's briefly talk about properties and indexers.

[10] An event is a callback that is implemented using delegates, which is covered shortly.

A property is similar to a field (a member variable), with the exception that there is a getter and a setter method, as follows:

using System;

public class Car
{
  private string make;
  public string Make 
  {
    get { return make; }
    set { make = value; }
  }

  public static void Main(  ) 
  {
    Car c = new Car(  );
    c.Make = "Acura";  // Use setter.
    String s = c.Make; // Use getter.
    Console.WriteLine(s);
  }
}

Although this is probably the first time you've seen such syntax, this example is straightforward and really needs no explanation, with the exception of the keyword value. This is a special keyword that represents the one and only argument to the setter method.

Syntactically similar to a property, an indexer is analogous to operator[] in C++, as it allows array-like access to the contents of an object. In other words, it allows you to access an object like you're accessing an array, as shown in the following example:

using System;

public class Car
{
  Car(  )
  {
    wheels = new string[4];
  }

  private string[] wheels;
  public string this[int index] 
  {
    get { return wheels[index]; }
    set { wheels[index] = value; }
  }

  public static void Main(  ) 
  {
    Car c = new Car(  );
    c[0] = "LeftWheel";  // c[0] can be an l-value or an r-value.
    Console.WriteLine(c[0]);
 }
}

Unlike C++, but similar to Java, classes in .NET support only single-implementation inheritance.

2.6.1.5 Interfaces

Interfaces support exactly the same concept as a C++ abstract base class (ABC) with only pure virtual functions. An ABC is a class that declares one or more pure virtual functions and thus cannot be instantiated. If you know COM or Java, interfaces in .NET are conceptually equivalent to a COM or Java interface. You specify them, but you don't implement them. A class that derives from your interface must implement your interface. An interface may contain methods, properties, indexers, and events. In .NET, a class can derive from multiple interfaces.

2.6.1.6 Delegates

One of the most powerful features of C is its support for function pointers. Function pointers allow you to build software with hooks that can be implemented by someone else. In fact, function pointers allow many people to build expandable or customizable software. Microsoft .NET supports a type-safe version of function pointers, called delegates. Here's an example that may take a few minutes to sink in, but once you get it, you'll realize that it's really simple:

using System;
class TestDelegate 
{
  // 1. Define callback prototype.
  delegate void MsgHandler(string strMsg);

  // 2. Define callback method.
  void OnMsg(string strMsg)
  {
    Console.WriteLine(strMsg);
  }

  public static void Main(  ) 
  {
    TestDelegate t = new TestDelegate(  );

    // 3. Wire up our callback method.
    MsgHandler f = new MsgHandler(t.OnMsg);

    // 4. Invoke the callback method indirectly.
    f("Hello, Delegate.");
  }
}

The first thing to do is to define a callback function prototype, and the important keyword here is delegate, which tells the compiler that you want an object-oriented function pointer. Under the hood, the compiler generates a nested class, MsgHandler, which derives from System.MulticastDelegate.[11] A multicast delegate supports many receivers. Once you've defined your prototype, you must define and implement a method with a signature that matches your prototype. Then, simply wire up the callback method by passing the function to the delegate's constructor, as shown in this code listing. Finally, invoke your callback indirectly. Having gone over delegates, you should note that delegates form the foundation of events, which are discussed in Chapter 8.

[11] If you want to see this, use ildasm.exe and view the metadata of the delegate.exe sample that we've provided.

2.6.2 The Common Language Specification (CLS)

A goal of .NET is to support language integration in such a way that programs can be written in any language, yet can interoperate with one another, taking full advantage of inheritance, polymorphism, exceptions, and other features. However, languages are not made equal because one language may support a feature that is totally different from another language. For example, Managed C++ is case-sensitive, but VB.NET is not. In order to bring everyone to the same sheet of music, Microsoft has published the Common Language Specification (CLS). The CLS specifies a series of basic rules that are required for language integration. Since Microsoft provides the CLS that spells out the minimum requirements for being a .NET language, compiler vendors can build their compilers to the specification and provide languages that target .NET. Besides compiler writers, application developers should read the CLS and use its rules to guarantee language interoperation.

    [ Team LiB ] Previous Section Next Section