CLR Types

Use Reflection to Discover and Assess the Most Common Types in the .NET Framework

Panos Kougiouris

Code download available at:CLRTypes.exe(187 KB)

This article assumes you're familiar with C#

Level of Difficulty123

SUMMARY

The .NET Framework Class Library and the common language runtime (CLR) serve as the foundation for all .NET-based applications. But how much do you know about any of the thousands of CLR classes in the library and where do you begin to learn about them?

In this article, the author uses the number of relationships with other types to determine which types are going to affect your programming most frequently and how often you'll encounter them. He programmatically surveys the CLR library using reflection to make that determination. Following that, an in-depth examination of the nine most important CLR types is provided.

Contents

CLR Types and Their Relationships
Unified Modeling Language
Reflection and the CLR
The Program
System.Object
System.EventHandler
System.IComparable
System.IFormattable
System.IDisposable
System.ICloneable
System.Collections.IEnumerable
System.Globalization.CultureInfo
System.IAsyncResult
Conclusion

The Microsoft® .NET Framework can be used for developing applications ranging from standalone rich clients to XML Web Services. The framework has two main components: the Common Language Runtime (CLR) and the Framework Class Library (FCL). Mastering the framework requires familiarity not only with the runtime and your favorite language, but also with the types in the class library. Otherwise, you might find yourself reinventing the wheel by implementing functionality that already exists. That said, the .NET FCL has about 9,000 CLR types. If you were to study the most important ones, where would you start? In this article, I will let .NET and the reflection facilities of the CLR answer this question. I will first define a framework that establishes the popular types or more precisely, the types that belong to the most relationships, a concept I'll discuss next. Then I will present a C# program that uses the framework to rank the types according to their "popularity." Finally, I will explore the top nine types, where you'll use them, and what they do.

CLR Types and Their Relationships

With a few exceptions, nearly every CLR type relates to other types. There are many kinds of relationships, but in this article I will focus on "public" relationships. I will define a public relationship as a relationship that is revealed to the user of a type from the signature of its public members.

Let's consider several examples of relationships between different CLR types. For instance, the type System.Collections.IEnumerable is defined in C# as follows:

public interface IEnumerable { IEnumerator GetEnumerator(); }

This type relates to another type, System.Collections.IEnumerator, because the method GetEnumerator returns an object of the type IEnumerator. In a similar fashion, the DataSet class in the following example is related to the XmlWriter class because its WriteXml method takes an argument of type XmlWriter:

public class DataSet : ...{ ••• public void WriteXml(XmlWriter writer); ••• }

The inheritance relationship depicted in the following example is much more common:

public class Page : TemplateControl, IHttpHandler { // methods omitted for clarity }

The class System.Web.UI.Page is the superclass of every ASP.NET page. This class implements the IHttpHandler interface and inherits from the TemplateControl abstract class. As you can see, there are two types of inheritance associations, implementation inheritance and interface inheritance.

Finally, the following example shows another kind of relationship between different CLR types:

public sealed class Console { ••• public static TextWriter Out {get;} ••• }

The Console class is related to the TextWriter class because the out property is of the type TextWriter. The property could be an instance property or a field.

Unified Modeling Language

The Unified Modeling Language (UML) was created to help software professionals model, explain, and understand software. UML depicts a system in terms of a model and its views. Each view is illustrated through a graphical diagram. UML defines eight different diagrams, as well as a consistent notation for symbols in these diagrams. For instance, a class is defined as a box that contains the name of the class. One of the most widely used UML diagrams is the UML static structure, which is a graph that depicts types and their relationships. The nodes of the graph depict types; the edges of the graph depict relationships between types. UML uses different kinds of arrows to connect the types.

To get a little bit more concrete, here is an example of how to create a UML class diagram to model your system. For every type, draw a box. Then draw an arrow from one type to another if the types are related by a generalization, aggregation, composition, or some other relationship. Figure 1 shows a simplified representation of a small part of the UML static diagram within the .NET Framework. This is not just an inheritance graph, since inheritance is only one type of relationship. For instance, the type IComparable has a line into the type Object because the method CompareTo takes an argument of type Object. The type HttpCookie is associated with String because it has properties of type String.

Figure 1 Simplified .NET UML Chart Excerpt

Figure 1** Simplified .NET UML Chart Excerpt **

Charting the .NET Framework using UML or other means results in a huge graph. In Figure 1 some types have more arrows pointing towards them than others. Types with many arrows pointing towards them indicate a higher degree of relationships. Since you'll come across these types again and again as you explore the types of the .NET Framework, it's probably a good idea to become familiar with them up front. Of course, drawing the complete UML graph and then trying to count the arrows would be impossible. Fortunately, the CLR has a feature that makes this task much easier.

Reflection and the CLR

Reflection makes it possible to explore the CLR type system programmatically. This is not only faster than using UML, but it's also a much more interesting approach. Reflection allows you to find what types are in an assembly, the methods of a type, how many arguments a method has, and so on.

At the center of the CLR reflection subsystem is a type called System.Type that implements the System.IReflect interface. This interface allows querying of fields, properties, and methods. For instance, the GetFields method returns an array of FieldInfo objects that describe the fields of the Type. Most of the methods in the IReflect interface take a parameter representing a value from the BindingFlags enumeration. This parameter provides you with finer control over the set of types returned. For instance, the following code shows how you can print the names of all the public instance methods of the type String:

Type tp = typeof(String); MethodInfo[] aMi = tp.GetMethods( BindingFlags.DeclaredOnly |BindingFlags.Instance |BindingFlags.Public); foreach (MethodInfo mi in aMi) Console.Out.WriteLine(mi.ToString());

Admittedly, the preceding section serves as just a very quick introduction to the Reflection APIs. The Reflection classes are described in much more detail in Matt Pietrek's October 2000 article "Introducing Application Metadata in the Microsoft .NET Framework".

The Program

The program I will present in this section is a .NET-based console application that traverses a set of CLR types and determines the relationships for each type. It then sorts the types based on the number of relationships they have. You can download the code from the MSDN® Magazine Web site at the link at the top of this article.

The first issue to address is finding the types. In the CLR, types are packaged as special portable executable (PE) files with .dll or .exe extensions. So to find types you need to look inside the file system files with these extensions. Using the -dir option, the program allows the user to specify one or more directories to search for .NET DLLs. In addition, if you use the -sys option the program automatically includes all the .NET Framework types. These types are found in a directory which is pointed to by the registry entry HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\InstallRoot. For instance, on my machine (running Windows 2000) the registry entry points to the directory C:\WINNT\Microsoft.NET\Framework\. You append the current version, which is also stored in the registry, and you've got the path.

As mentioned previously, my program traverses all the types and finds their relationships with other types. For every type, the app keeps a data structure with usage counters. A typical breadth-first search (BFS) graph-traversal algorithm is used. It picks a type and updates the usage counts of all the types that are associated with this particular type. The algorithm then marks this type as visited and moves to the next type, until all the types have been exhausted. Finally, all the data structures are sorted according to their usage and printed in the standard output.

Figure 2 shows the output when you run the program. For every type, eight numbers are reported. The first column indicates the number of types that inherit from this type using interface inheritance. Obviously, only interfaces have nonzero numbers in this column. The second column indicates the number of types that inherit from this type using class inheritance. The third column indicates the number of times this type is used as a property, while the fourth column indicates the number of times the type is used as a public field. The fifth column indicates how many times the type is used as an argument in a constructor, while the sixth column indicates the number of times it's used as an argument in an operator. The seventh column indicates the number of times the type is used as an argument or return value in a method. Finally, the eighth column indicates the sum of all the other numbers. The types are reported in ascending order based on this last number.

Figure 2 Program Output

C:\> clrts -dir d:\projects\my-dotnet-app ••• System.Boolean 0, 0, 797, 146, 309, 3, 784, 2039 System.IntPtr 0, 0, 18, 379, 99, 1, 1561, 2058 System.Object 0, 2779, 204, 252, 470, 0, 2477, 6182 System.String 0, 0, 1246, 1160, 1537, 2, 4945, 8890 System.Int32 0, 0, 698, 7740, 860, 7, 5889, 15194

I ran the program with the -sys option to get a report on all the types in the .NET Framework, and the program traversed 9202 types! Figure 3 lists the most widely used types. These types are referenced very frequently in the framework, so you will be running across them regularly. I will explore some of these types in the following sections.

Figure 3 Widely Used Types in the .NET Framework

Type Inheritance Properties and Fields Methods Total
System.Int32 0 8438 6756 15194
System.String 0 2406 6484 8890
System.Object 2779 456 2947 6182
System.IntPtr 0 397 1661 2058
System.Boolean 0 943 1096 2039
System.EventHandler 0 4 1766 1770
System.IComparable 1158 0 0 1158
System.IConvertible 1139 0 0 1139
System.IFormattable 1135 0 0 1135
System.Enum 1120 0 0 1120
System.Type 5 48 659 712
System.Runtime.Serialization.ISerializable 617 0 0 617
System.IDisposable 602 0 0 602
System.Single 0 86 505 591
System.ICloneable 574 0 0 574
System.ValueType 535 0 3 538
System.Int16 0 345 139 484
System.Collections.IEnumerable 472 1 9 482
System.Byte[] 0 46 375 421
System.ComponentModel.IComponent 353 0 42 395
System.MulticastDelegate 346 0 0 346
System.UInt32 0 105 227 332
System.IAsyncResult 13 3 315 331
System.Byte 0 213 112 325
System.UIntPtr 0 0 314 314
System.AsyncCallback 0 0 307 307
System.Int64 0 66 234 300
System.Collections.ICollection 261 2 34 297
System.Object[] 0 21 256 277
System.Int32& 0 0 267 267
System.Array 233 0 32 265
System.Attribute 222 0 37 259
System.Double 0 25 218 243
System.Reflection.Emit.OpCode 0 222 20 242
System.Globalization.CultureInfo 0 6 227 233
System.Windows.Forms.IWin32Window 171 0 16 187
System.String[] 0 29 152 181
System.Int32[] 0 10 171 181
System.Drawing.Rectangle 0 16 164 180
System.Char 0 36 142 178
System.DateTime 0 40 134 174
System.Exception 22 6 145 173
System.IO.Stream 23 3 146 172

Since this article assumes familiarity with other languages and runtimes, I will skip an elaborate description of the basic data types. Figure 4 contains a summary of the mappings of the basic types to a few of the .NET languages. In the C tradition, notice that both signed and unsigned types are supported. In contrast with most other environments, the decimal data type—a very useful type for all applications that deal with money—is a first-class type for both C# and Visual Basic® .NET.

Figure 4 Mapping the Basic CLR Types

System Types Visual Basic .NET Managed C++ C#
System.Boolean Boolean bool bool
System.SByte N/A byte sbyte
System.Int16 Short short short
System.Int32 Integer long int
System.Int64 Long __int64 long
System.Byte Byte byte byte
System.UInt16 N/A unsigned short ushort
System.UInt32 N/A unsigned long uint
System.UInt64 N/A unsigned __int64 ulong
System.Single Single float float
System.Double Double double double
System.Char Char char char
System.String String System::String string
System.DateTime Date N/A N/A
System.Decimal Decimal N/A decimal

System.Object

System.Object is undoubtedly the most important type in .NET; every class in .NET inherits from it. This type has four public methods and two protected methods, as shown here:

Public class Object { public virtual bool Equals(object); public virtual int GetHashCode(); public virtual string ToString(); public Type GetType(); // static public static bool Equals(object, object); //protected ~Object(); // Finalize protected object MemberwiseClone(); }

The public methods are Equals, GetHashCode, ToString, and GetType. Equals allows an object to compare itself with another object for equality. GetHashCode returns an integer that can be used with data structures and algorithms that store and retrieve objects. ToString returns a representation of the object as a string. Note that these three protected methods have default implementations that you would want to override in most cases. Finally, GetType returns an object that represents the CLR type of the object. It is one of the entry points to the CLR's reflection subsystem, which I discussed earlier.

The protected methods are Finalize and MemberwiseClone. Expressed in C# and managed C++ with destructor syntax, the Finalize method is called by the garbage collector just after the object has become inaccessible. You can ask the system not to call this method on an object using the System.GC.SuppressFinalize method. Since Finalize is called by the system in a nondeterministic way, it is important not to rely on finalization to free any other resources that the object might hold. Later I'll discuss how implementing the IDisposable interface provides the recommended way to do this.

The MemberwiseClone method performs a copy of an object. Two points need to be stressed here. First, the copy is a shallow copy; if the object contains references to other objects, these objects are not copied. Second, since this method is protected, it cannot be called by other objects. If you want to provide this facility to other objects, you should use the ICloneable interface (discussed later).

System.EventHandler

System.EventHandler is a delegate type. Jeffrey Richter has covered delegates extensively in MSDN Magazine (see NET: Implementation of Events with Delegates, among others), so I will only remind you that in C/C++ a CLR delegate is similar to a function pointer. Objects that generate events (like timers, buttons, DataSets, and so on) allow interested parties to provide a delegate that has the System.EventHandler prototype. When the object fires the event, the delegates are called.

The example in Figure 5 is from the class in the code behind an ASP.NET page. When the Page_Init method is called, it wires the ServerClick event of the HtmlButton object m_button with the onClick method. When the button is clicked, the OnClick method is called. This method takes two arguments. The first argument is the caller. The second argument is an instance of the EventArgs class that contains data. The data in the EventArgs object is interpreted differently depending on the context.

Figure 5 Calling Delegates

public class myHTMLPage : System.Web.UI.Page { ••• System.Web.UI.HtmlControls.HtmlButton m_button; /// <summary> /// The EventHandler delegate /// </summary> public void OnClick(Object o, EventArgs ea) { Response.Write ("Clicked"); } /// <summary> /// Wire the delegate with the button /// </summary> public void Page_Init(Object Sender, EventArgs e) { m_button.ServerClick += new EventHandler(this.OnClick); } ••• }

System.IComparable

Recall that every CLR object has an Equals method that compares the object with another object for equality. For certain algorithms, this is not adequate. For instance, consider a Sorter object that implements a sorting algorithm. This object could work on any kind of object, as long as it can compare two objects and not only determine if they are equal, but also which object is greater.

Here is the declaration of the IComparable interface that provides this capability to object implementations:

public interface IComparable { int CompareTo(Object obj); }

The single method of this interface takes an object argument and returns an integer: 0 if the objects are equal, -1 if the argument is greater than the object that implements the interface, and 1 if the object that implements the interface is greater than the argument object. This pattern is similar to the strcmp function in C/C++.

For an example of this pattern, take a look in the Array.Sort method. This method takes one argument, an array of objects that implement IComparable. It then sorts the objects in ascending order based on the relationship defined by the CompareTo method. Several types implement this interface (including String and Int32) and that's why it appears so high on the list in Figure 3.

System.IFormattable

All objects in the CLR implement the ToString method. This is a very easy way to convert an object to a string. In many cases, however, an object can have several string mappings. Consider the System.DateTime class. You might want to convert a date to a string using a long or short format. The Object.ToString method does not provide enough control to specify such details. When this kind of precision is important for a class, the class needs to implement the IFormattable interface, as shown here:

public interface IFormattable { string ToString(string format, IFormatProvider formatProvider); }; public interface IFormatProvider { object GetFormat(Type formatType); };

IFormattable has only one method, which takes two arguments. The first argument is a string that specifies the format. The second argument is an object of type IFormatProvider, which is used to format the value. NumberFormatInfo, DateTimeFormatInfo, and CultureInfo are examples of classes in the framework that implement the IFormatProvider interface.

Here is an example of how you would print the current date in the Greek culture using the long format:

using System.Globalization; ••• DateTime dt = DateTime.Now; string str = dt.ToString("D", CultureInfo.CreateSpecificCulture("el")); Console.Out.WriteLine(str); •••

IFormattable enables seamless integration of a type with formatting in the framework. For instance, you have already seen the CLR equivalent of printf. It uses curly brackets to specify the position of the arguments. Inside the curly brackets, the syntax is

{ N [, M ][: formatString ]}

where N is the position of the parameter and M is the number of spaces to use. If a formatString is passed after the colon and the argument supports the IFormattable interface, the ToString method of this object is called using a null formatProvider. For instance, to print the Date in the current locale in long format you would use:

Console.WriteLine("The date is {0:D}", DateTime.Now);

System.IDisposable

One of the main advantages of the CLR is being able to use garbage collection to reclaim memory. This means that you don't have to worry about when to release objects. The runtime tracks which objects are in use and reclaims the memory of objects that are not used anymore. As I mentioned earlier, just before reclaiming the memory, the system gives the object a chance to clean up its internal state by calling Finalize (known as a destructor in C#).

For most objects this works great. However, for objects that keep system resources as part of their internal state (such as file handles or sockets), relying on garbage collection is not an option. A high-performance, scalable application must ask objects to free these resources as soon as possible. Objects can indicate this by implementing the System.IDisposable interface:

public interface IDisposable { void Dispose(); };

In an ideal world, the Dispose object would be called every time. In reality, an object that implements this interface needs to also consider the case where the Dispose method is not called. In this case, only the Finalize method is called. In both cases, all resources should be reclaimed. If the Dispose method is called, the object should clean up the internal state and then ensure that the Finalize method is not called. You can accomplish this on an object by calling the following line:

GC.SuppressFinalize(this);

Figure 6 shows a typical implementation that meets all of the objectives just mentioned.

Figure 6 Dispose

class Foo : IDisposable { ••• public void Dispose() { Dispose(true); // so that Finalize() is not called GC.SuppressFinalize(this); } ~Foo() { Dispose(false); } // The actual method that cleans the internal state protected void Dispose(boolean disposing) { ••• if (disposing) { //clean up managed objects } // Clean up unmanaged objects if any // When appropriate, dispose of base object // base.Dispose(disposing); } };

System.ICloneable

Objects are usually created using a constructor method. For instance, in C# this is done using the new operator. However, in many cases it might be beneficial to create an object by making a copy of an existing object. In object-oriented design there is a design pattern called the Prototype that describes this case (see Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison-Wesley, 1995). There is even a research object-oriented language called Self in which you can only create objects by cloning existing objects.

Think back to the MemberwiseClone method mentioned earlier. This method would be useful to clone an object, but it is protected so it cannot be called by users of an object. By default, MemberwiseClone makes a shallow copy of the object. As a result, all the ValueType fields are copied, but objects are not. Instead, a reference to the same object is shared by the original object and the cloned object. Figure 7 shows how an object looks after a shallow copy. You'll notice that the two objects get their own instance of value types, but share the object named B.

Figure 7 A Shallow Copy

Figure 7** A Shallow Copy **

If the implementor of an object wants to support cloning, it needs to implement the System.ICloneable interface:

public interface ICloneable { object Clone(); }

The .NET Framework allows Clone to be implemented either as a shallow or a deep copy. The only restriction imposed by the framework is that the type of the object returned should be the same as the type of the object that implements the ICloneable interface. If a shallow copy is appropriate, you can design an elegant implementation:

public class Foo : ICloneable{ ••• object Clone() { return MemberwiseClone(); } ••• }

System.Collections.IEnumerable

Everybody who has worked with objects for a while knows that many objects contain other objects. For instance, Windows® Forms and ASP.NET pages contain controls which, in turn, might contain other controls. Arrays, lists, dictionaries, and other container data structures contain objects. It is a very common pattern to iterate through all the objects contained in an object. In .NET, objects that need to provide enumeration over a collection of objects they contain should do it using the IEnumerable and IEnumerator interfaces, which are shown here:

public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { public object Current {get;} public bool MoveNext(); public void Reset(); }

The following code shows how you use these interfaces to iterate over the contents of an array object:

String[] ars = {"alpha", "beta", "gamma"}; IEnumerator ie = ars.GetEnumerator(); while (ie.MoveNext()) { Console.Out.WriteLine("{0}", ie.Current.ToString()); }

You first use the GetEnumerator method to get your hands on an object that implements the IEnumerator object. This separation between the IEnumerable and IEnumerator interfaces allows you to have multiple iterators on the same collection.

Since IEnumerable is such a popular interface, .NET languages provide direct support for it. For instance, C# provides the foreach statement. The previous example could be rewritten in the following manner using foreach:

String[] ars = {"alpha", "beta", "gamma"}; foreach (String s in ars) { Console.Out.WriteLine("{0}", s); }

System.Globalization.CultureInfo

The experience Microsoft has had with internationalized applications has influenced .NET. At the center of support for the i18n model is the CultureInfo class, which contains all the information needed to fully identify the differences between two cultures. A culture is identified by a language, a calendar, a date/time format, a number format, and so on. Methods that do not take a CultureInfo argument but need one, operate using the thread's default culture, which can be retrieved through the CultureInfo.CurrentCulture property. For instance, consider the TypeConverter interface, which contains methods to convert one type to another. This interface has the ConvertToString method, designed to convert a type to String. The method is overloaded as shown in this code:

public string ConvertToString(object); public string ConvertToString(ITypeDescriptorContext, CultureInfo, object);

Notice that the same method can use either the default thread culture or the CultureInfo object passed as an argument. Here is an example using the DateTimeConverter:

TypeConverter tc = new DateTimeConverter(); DateTime dt = DateTime.Now; // Outputs the date/time using the local // CultureInfo.CurrentCulture culture Console.Out.WriteLine("{0}", tc.ConvertToString(dt)); // Outputs the date/time using the Greek culture Console.Out.WriteLine("{0}", tc.ConvertToString(null, CultureInfo.CreateSpecificCulture("el-GR"), dt));

Being able to have different cultures for each thread may not be very important for client-side applications, but it's essential to server-side applications. For instance, a Web server might be serving a U.S. customer and a French customer at the same time. As long as the customers are served by different threads, they will see a localized user interface.

System.IAsyncResult

The .NET Framework provides first-class support for asynchronous communication. Asynchronous communication is nothing new; after all, this is how computers have always worked. However, most high-level APIs and platforms have isolated the programmer from this concept. For example, let's consider a File object. Every platform has a read method that reads bytes from a file on the disk. Because disks are much slower than the main processor, when you call read, your thread will most likely block for a while. In certain cases, it would be very useful to regain control while the operation is taking place. For instance, you might want to update the UI or check whether the user has clicked the Cancel button.

In several places in the .NET Framework where a thread can block while waiting for an operation to complete, the same operation is also implemented as a pair of BeginXxx and EndXxx methods. Here are the methods implementing a Read operation:

// Synchronous read unsigned long Read(Byte[] buffer, unsigned long NumToRead); // Asynchronous read IAsyncResult BeginRead(Byte[] buffer, unsigned long NumToRead, AsyncCallbackDelegate cb); unsigned long EndRead(IAsyncResult ar);

In the synchronous read, you simply call the Read method and wait until the read has completed. In the asynchronous case, you call the BeginRead method. The method schedules the operation and returns immediately. It returns an object that implements the IAsyncResult interface. The caller can use the interface to check the status of the call. When the caller is ready to get the results or is willing to wait, he completes the call by passing the IAsyncResult object to the EndRead method, as shown here:

public interface IAsyncResult { object AsyncState {get;} WaitHandle AsyncWaitHandle {get;} bool CompletedSynchronously {get;} bool IsCompleted {get;} }

The IsCompleted property returns true when the operation has completed. At this point, calling the EndXxx method will return without blocking. The AsyncState property returns the object that was passed as the last parameter in the BeginXxx method. The AsyncWaitHandle property contains a handle that threads can use to wait for the operation to complete. Finally, the CompletedSynchronously property is set to true if the operation was completed by the BeginXxx method. For a more complete description of asynchronous communication in .NET see "Making Asynchronous Method Calls in the .NET Environment" by Richard Grimes in the August 2001 issue.

Conclusion

C#, Visual Basic .NET, and the CLR have great features that will undoubtedly increase your productivity as a developer. However, what will have an even bigger impact are the design patterns and implementations that come with the .NET Framework. If experience with other frameworks in the past (Smalltalk, for example) is any indication, I would say that developers using .NET should expect to devote a significant portion of their learning to the types of the CLR.

For background information see:
.NET Delegates: Making Asynchronous Method Calls in the .NET Environment
.NET: Implementation of Events with Delegates
Avoiding DLL Hell: Introducing Application Metadata in the Microsoft .NET Framework
ftp://ftp.omg.org/pub/docs/formal/01-09-72.pdf
Design Patterns by Gamma, Helm, Johnson, and Vlissides (Addison-Wesley, 1995)

Panos Kougiourishas architected several Internet applications using IIS and other technologies. He can be reached at panos@acm.org.