Common Language Runtime

The ECMA standards define an infrastructure in which a single application can be developed using multiple high-level languages. The standards also specify that the application should be capable of running on different system (hardware and OS) platforms without being rewritten for each specific platform. This ECMA specification is referred to as the CLI. The current specifications for the CLI can be downloaded from the ECMA's Web site (www.ecma.ch). It is worth mentioning that C# specifications have also been submitted to the ECMA.

Microsoft's implementation of the CLI is called the common language runtime. The common language runtime forces a unified programming model and provides features such as cross-language integration and type safety. It also manages the execution of the code and provides certain runtime services to the managed code. In this section, we examine some important aspects of the common language runtime.

Strictly speaking, the common language runtime implements more than what is specified in the CLI. For example, the common language runtime provides support for managed code to interoperate with COM components. This is not part of the CLI specification. The focus of the CLI is more toward what makes sense across different platforms.

Common Type System

At the center of the CLI is a single type system, the CTS. The CTS is a model that defines the rules CLI follows when declaring, using, and managing types. The CTS establishes a framework that enables cross-language integration, type safety, and high-performance code execution.

The CTS also defines a base set of datatypes (e.g., int, string, etc.) and specifications for extending these types. The base datatypes are defined in the BCL. The complete list of base data types can be obtained from the ECMA specifications. We will look at a few important ones shortly.

Confusion over BCL

Note that the ECMA standards distinguish between the Runtime Infrastructure Library, Base Class Library, Network Library, Reflection Library, Floating-Point Library, and Extended Array Library. However, Microsoft lumps all these libraries into one and calls it the BCL.

The ECMA standards document is still a work in progress as of this writing. Hopefully, this discord will be addressed eventually.


The CTS is shared by the compilers, tools, and the runtime. Compiler vendors can write compilers and tools targeting the runtime. The runtime enforces the rules defined by CTS when managing types.

Under CTS, a type is defined as the unit that encapsulates a dataset and defines possible operations on the dataset. A type can have methods as well as other members such as fields, properties, and events. C# classes and structures are examples of types.

An important aspect of CTS is that all types are ultimately inherited from System.Object, a class defined in the BCL. This guarantees that every instance of every type supports the methods provided by CTS. Table 4.1 describes some of the important methods.

Table 4.1. Public Methods of System.Object
MethodDescription
GetHashCodeReturns a hash value
EqualsChecks if two objects are the same
FinalizeMethod called by the common language runtime when the object is about to be destroyed
GetTypeReturns the type of the object
MemberwiseCloneMakes a shallow copy of the object
ToStringReturns a string representation of the object

Method GetHashCode returns a 32-bit integer that is suitable for use in data structures such as hash tables. The default implementation returns a value that is guaranteed to be the same for the same instance. However, it is not guaranteed that two different instances have different hash codes or two objects storing the same value have the same hash code. This implementation is not particularly useful for hashing. Therefore, derived classes that can be used in hash tables should override GetHashCode and provide a more suitable implementation. For example, class System.String overrides Get-HashCode such that if two instances of the System.String class contain the same string, the returned hash code is the same.

Method Equals checks if two objects are the same. The default implementation of Equals checks objects by reference; that is, whether two references point to the same object. However, any derived class can override this method to specify its own equality condition. For example, the Equals method of System.String returns true for any two instances of a string that contain exactly the same characters in the same order.

If Equals deems two objects equal, then a good rule of thumb is that GetHashCode on the two objects be equal as well. Therefore, if you override Equals on a type, you should (although not required) also override Get-HashCode. The C# compiler, for example, will generate a warning otherwise.

Method Finalize is automatically invoked by the common language runtime when the object is about to be destroyed. The default implementation of Finalize does nothing. However, a derived class can override this method if need be. This is typically done to free any resources the object is holding or to perform some other cleanup operations on the object.

Note that C# does not let you override the Finalize method directly. However, you can implement a destructor on your class to express your finalization needs, as shown here:

class Foo {
    ...
    ~Foo() {
      // implement your cleanup code here
    }
}

During compilation, the compiler converts this destructor to Finalize method. In the process, the compiler also guarantees that the Finalize method for the base class, if any, is invoked.

Method GetType returns an instance of the System.Type class, which can be used to obtain metadata information (e.g., list of methods in a class) about the object. This method cannot be overridden.

Method MemberwiseClone creates and returns a copy of the object it is called on. We revisit this method when we discuss deep versus shallow copy.

Finally, method ToString returns the string representation of the object. The default implementation returns the fully qualified name of the type the object belongs to. For example, if a class Bar is defined under a namespace Foo, then ToString on an instance of Bar returns Foo.Bar by default. However, any derived class can override ToString and construct a more sensible string.

Operator Overloading in C#

When you compare two objects using C#'s == operator, do not expect that Object.Equals will be invoked. There is no relationship between the two. However, C# specifies a way to overload operators similar to that of C++. The following code excerpt shows how to overload operator ==:

class Foo {
    ...
    private int m_Value;

    static public bool operator==(Foo left, Foo right) {
      return (left.m_Value == right.m_Value);
    }

    ...

}

With this change, when you compare two instances of class Foo as shown here, then the overloaded operator gets invoked:

Foo x = new Foo();
Foo y = new Foo();
if (x == y) {
    ...
}

Generally, when you overload the equals operator, it is a good practice to overload not equals (!=) as well.

Behind the scenes, the C# compiler expands the overloaded equals operator as op_Equality, a BCL-defined standard method name for comparing two values. The framework SDK defines all possible operator overloads, their equivalent BCL method names, and overloading usage guidelines. Table 4.2 shows some common overload operators.

Table 4.2. Some C# Overload Operators
OperationC# Operator SymbolBCL Method Name
Equals==op_Equality
Not equals!=op_Inequality
Bitwise (binary) And&op_BitwiseAnd
Logical And&&op_LogicalAnd
Bitwise (binary) Or|op_BitwiseOr
Logical Or||op_LogicalOr
Logical Truetrueop_True
Logical Falsefalseop_False

Sample project OperatorLoading demonstrates the behavior for many overload operators.

Beware of Overloading & AND | Operators

Consider the following C# code excerpt:

If (x && y) { // logical and
  ...
}

Under .NET, the behavior of this logical and can be represented as follows:

T.op_False(x) ? T.op_True(x) : T.op_True(T.op_BitwiseAnd(x,y))

The operation first invokes op_False on x. If x is false, then y is simply ignored. If x is true, however, then the code calls op_BitwiseAnd on x and y and then calls op_True on the result of op_BitwiseAnd. What this means is that even if x and y are true when tested individually, it is possible that (x && y) may still return false. Likewise, if y is false, (x && y) may still return true. It all depends on how op_BitwiseAnd is defined.

Similarly, the behavior of the logical or operation can be represented as follows:

T.op_True(x) ? T.op_True(x) : T.op_True(T.op_BitwiseOr(x,y))

The bottom line is that you have to be especially careful when defining op_BitwiseAnd and op_BitwiseOr when creating a new type.


Common Language Specification

Although CTS provides the base types that all languages can use, not all base types are intended to be supported by all programming languages. For example, Visual Basic .NET does not support unsigned datatypes. Depending on the specifications of a programming language, the compiler vendor may support only the subset of CTS types needed by the language.

The CLS is an agreement between compiler vendors and .NET Framework designers. It defines the minimum set of features a .NET-compliant compiler should support. Among other things, the CLS dictates the subset of CTS datatypes that are intended to work across all .NET languages and a set of usage conventions. Whereas CTS is wide enough to allow many features so that languages can implement any feature they want, CLS is narrow enough to allow just those language features needed to support cross-language integration. Table 4.3 lists CTS-defined base datatypes along with their C# datatype and CLS compliance.

Table 4.3. C# Primitive Datatypes
BCL TypeDescriptionC# TypeCLS Compliant?
System.BooleanA true/false valueboolYes
System.SByte8-bit signed integersbyteNo
System.Byte8-bit unsigned integerbyteYes
System.Char16-bit Unicode charactercharYes
System.Int1616-bit signed integershortYes
System.UInt1616-bit unsigned integerushortNo
System.Int3232-bit signed integerintYes
System.UInt3232-bit unsigned integeruintNo
System.Int6464-bit signed integerlongYes
System.UInt6464-bit unsigned integerulongNo
System.SingleIEEE 32-bit floatfloatYes
System.DoubleIEEE 64-bit floatdoubleYes
System.Decimal96-bit monetary type (used in financial calculations)decimalYes
System.StringString of Unicode charactersstringYes
System.ObjectRoot system classobjectYes

Use CLS-Compliant Datatypes

Whenever possible, use CLS-compliant datatypes (unless cross-language interoperability is not important for your needs).

To ensure that your code is in compliance with CLS, add the following line to any of the source file used in building your assembly:

[assembly:System.CLSCompliant(true)]

With this change, the compiler flags an error if it finds code that is not compliant with CLS.

Note that CLS compliance is meant only for public and family members. As private members are never exposed to any other type, they need not be compliant.


Here is the code excerpt that shows how an assembly based on Visual Basic .NET can make a cross-language method call into a C#-based assembly.

// Project CrossLanguage

// File ConsoleGreeting.cs
// Compile as: csc.exe –t:library ConsoleGreeting.cs
namespace MyGreeting {
    public class ConsoleGreeting {
      private String m_userName;

    public void Greet() {
      Console.WriteLine("Hello " + m_userName);
    }

    public String UserName {
      set {
        m_userName = value;
      }
      get {
        return m_userName;
      }
    }
  }
}

' File MyVBApp.vb
' Compile as: vbc.exe -t:exe -r:ConsoleGreeting.dll MyVBApp.vb

Imports MyGreeting
Module MyVBApp
     Sub Main()
      Dim obj As New ConsoleGreeting()
      obj.UserName = "Jay"
      obj.Greet()
    End Sub
End Module

Value Types and Reference Types

Another important aspect of the CTS is its classification of types. The CTS classifies types into two broad categories—value types and reference types. There are two major differences between these types:

  1. Storage: A value type instance is created on the stack and a reference type instance is created on the heap (managed heap, more specifically).

  2. Assignment: When a value type instance is assigned to another instance (of the same type), the second instance gets a duplicate copy of the first instance's data. In case of a reference type, both the instances share the same memory location.

Consider the following C# code excerpt:

System.Int32 a,b;
a = 5;
b = a;

Datatype System.Int32 (int in C#) is of value type. Therefore, when variable a is assigned to variable b, b gets a copy of a. Thereafter, changing the value of a does not cause a change in the value of b.

Under C#, classes and interfaces are always that of a reference type. Consider the following C# class:

public class Foo
{
    public int x;
};

The following code excerpt demonstrates how the assignment operation works for a reference type:

public static void Test(Foo a, Foo b) {
  a.x = 5;
  b.x = 10;
  b = a;
  Console.WriteLine(b.x); // will display "5"
  a.x = 20;
  Console.WriteLine(b.x); // will display "20"
}

As can be observed from the test, once a is assigned to b, changing the contents of a automatically reflect in b. This is because the assignment operation just assigned the reference (the memory location) of a to b.

Under C#, a struct definition is always that of a value type. Here is an example:

// Project ValueType

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

public static void Main()
{
    Point a, b;

    // All the fields of a value type variable have to be
    // assigned a value before the variable can be used
    a.x = 10;
    a.y = 20;

    b = a;

    Console.WriteLine("Point b is ({0},{1})", b.x, b.y);
}

Note that, unlike reference type instances, it is not necessary to instantiate a value type using the new operator. However, if not instantiated using the new operator, the value type variable has to be assigned a value before it can be used. Otherwise, the compiler generates an error.

To identify a datatype as a value type, the CTS requires that the type be derived from System.ValueType. This class overrides the virtual methods Equals, GetHashCode, and ToString from its base class, System.Object, to provide more appropriate implementation for value types.

Note that System.ValueType internally uses Reflection when overriding the virtual methods. Using Reflection is performance intensive. Moreover, the default implementation of GetHashCode just returns the hash code of the first non-null field of the value type. Therefore, it is highly recommended that each value type definition provide its own implementation of these methods.

The following code excerpt illustrates this idea:

// Project ValueType

struct Point {
    public int x;
    public int y;

    public override bool Equals(Object obj) {
      if(!(obj is Point)) {
        return false;
      }

      Point p = (Point) obj;
      return ((this.x == p.x) && (this.y == p.y));
    }

    public override int GetHashCode() {
      return (this.x ^ this.y); // a simple scheme
    }

    public override string ToString() {
      string s = "(" + this.x + "," + this.y + ")";
      return s;
    }
};

Implementing Value Types

Whenever possible, define your own implementation of Equals, GetHashCode, and ToString for value type definitions.


Boxing and Unboxing

It is possible, and sometimes necessary, to convert between value types and reference types. The conversion of a value type to a reference type is called boxing and the conversion of a reference type to a value type is called unboxing. The following code excerpt illustrates this idea:

// Project BoxingUnboxing

public static void Main()
{
    int i = 10; // "int" (System.Int32) is value type
    Object o;   //  "object" (System.Object) is reference type

    o = (object) i; // Boxing "i"

    int j = (int) o; // Unboxing "o" to integer

    Console.WriteLine(j);
}

Boxing results in creating an object on the heap and copies the value from the value-type object onto the object. This makes it fairly performance intensive. It is a good idea to inspect your code and remove unnecessary boxing operations if possible. Read Gunnerson's article on MSDN [Gun-01a] to check how box-savvy you are. Also, read [Gun-01b] to learn the performance implications of boxing.

Unboxing first checks if the source object is a boxed value of the requested value type. If the source object is null or is a reference to an incompatible object, an InvalidCastException is thrown. Otherwise, the value from the source object is copied into the destination value type object.

Sometimes it is a good idea to perform the compatibility check yourself before unboxing an object. This is exactly what we did in our implementation of the Point.Equals method in the previous section.

Let's now see what happens when a managed application is executed.

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

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