A Simple Generics Example


Let’s begin with a simple example of a generic class. The following program defines two classes. The first is the generic class Gen, and the second is GenDemo, which uses Gen.

 // A simple generic class. using System; // In the following Gen class, T is a type // parameter that will be replaced by a real // type when an object of type Gen is created. class Gen<T> {   T ob; // declare an object of type T   // Notice that this constructor has a parameter of type T.   public Gen(T o) {     ob = o;   }   // Return ob, which is of type T.   public T getob() {     return ob;   }   // Show type of T.   public void showType() {     Console.WriteLine("Type of T is " + typeof(T));   } } // Demonstrate the generic class. class GenDemo {   public static void Main() {     // Create a Gen reference for int.     Gen<int> iOb;     // Create a Gen<int> object and assign its     // reference to iOb.     iOb = new Gen<int>(102);     // Show the type of data used by iOb.     iOb.showType();     // Get the value in iOb.     int v = iOb.getob();     Console.WriteLine("value: " + v);     Console.WriteLine();     // Create a Gen object for strings.     Gen<string> strOb = new Gen<string>("Generics add power.");          // Show the type of data stored in strOb.     strOb.showType();          // Get the value in strOb.     string str = strOb.getob();     Console.WriteLine("value: " + str);   } }

The output produced by the program is shown here:

 Type of T is System.Int32 value: 102 Type of T is System.String value: Generics add power.

Let’s examine this program carefully.

First, notice how Gen is declared by the following line:

 class Gen<T> {

Here, T is the name of a type parameter. This name is used as a placeholder for the actual type that will be passed to Gen when an object is created. Thus, T is used within Gen whenever the type parameter is needed. Notice that T is contained within < >. This syntax can be generalized. Whenever a type parameter is being declared, it is specified within angle brackets. Because Gen uses a type parameter, Gen is a generic class.

In the declaration of Gen, there is no special significance to the name T. Any valid identifier could have been used, but T is traditional. Other commonly used type parameter names include V and E. Of course, you can also use descriptive names for type parameters, such as TValue or TKey. When using a descriptive name, it is common practice to use T as the first letter.

Next, T is used to declare a variable called ob, as shown here:

 T ob; // declare an object of type T

As explained, T is a placeholder for the actual type that will be specified when a Gen object is created. Thus, ob will be a variable of the type passed to T. For example, if type string is passed to T, then in that instance, ob will be of type string.

Now consider Gen’s constructor:

 public Gen(T o) {   ob = o; }

Notice that its parameter, o, is of type T. This means that the actual type of o is determined by the type passed to T when a Gen object is created. Also, because both the parameter o and the instance variable ob are of type T, they will both be of the same actual type when a Gen object is created.

The type parameter T can also be used to specify the return type of a method, as is the case with the getob( ) method, shown here:

 public T getob() {   return ob; }

Because ob is also of type T, its type is compatible with the return type specified by getob( ).

The showType( ) method displays the type of T by passing T to the typeof operator. Because a real type will be substituted for T when an object of type Gen is created, typeof will obtain type information about the actual type.

The GenDemo class demonstrates the generic Gen class. It first creates a version of Gen for type int, as shown here:

 Gen<int> iOb;

Look closely at this declaration. First, notice that the type int is specified within the angle brackets after Gen. In this case, int is a type argument that is passed to Gen’s type parameter, T. This creates a version of Gen in which all uses of T are replaced by int. Thus, for this declaration, ob is of type int, and the return type of getob( ) is of type int.

When you pass a type argument to a generic class, you are creating what is referred to in C# as a closed constructed type. (The term closed indicates that a type argument has been specified.) Thus, Gen<int> is a closed constructed type. In essence, a generic type, such as Gen<T>, is an abstraction. It is only after a specific version, such as Gen<int>, has been constructed that a concrete type has been created. In C# terminology, a construct such as Gen<T> is called an open constructed type, because no type argument has been specified.

The next line assigns to iOb a reference to an instance of an int version of the Gen class:

 iOb = new Gen<int>(102);

Notice that when the Gen constructor is called, the type argument int is also specified. This is necessary because the type of the object (in this case iOb) to which the reference is being assigned is of type Gen<int>. Thus, the reference returned by new must also be of type Gen<int>. If it isn’t, a compile-time error will result. For example, the following assignment will cause a compile-time error:

 iOb = new Gen<double>(118.12); // Error!

Because iOb is of type Gen<int>, it can’t be used to refer to an object of Gen<double>. This type checking is one of the main benefits of generics because it ensures type safety.

The program then displays the type of ob within iOb, which is System.Int32. This is the .NET class that corresponds to int. Next, the program obtains the value of ob by use of the following line:

 int v = iOb.getob();

Because the return type of getob( ) is T, which was replaced by int when iOb was declared, the return type of getob( ) is also int. Thus, this value can be assigned to an int variable.

Next, GenDemo declares an object of type Gen<string>:

 Gen<string> strOb = new Gen<string>("Generics add power.");

Because the type argument is string, string is substituted for T inside Gen. This creates a string version of Gen, as the remaining lines in the program demonstrate.

Generic Types Differ Based on Their Type Arguments

A key point to understand about generic types is that a reference of one specific version of a generic type is not type-compatible with another version of the same generic type. For example, assuming the program just shown, the following line of code is in error and will not compile:

 iOb = strOb; // Wrong!

Even though both iOb and strOb are based on Gen<T>, they are references to different types because their type parameters differ.

How Generics Improve Type Safety

At this point you might be asking yourself the following question: Given that the same functionality found in the generic Gen class can be achieved without generics, by simply specifying object as the data type and employing the proper casts, what is the benefit of making Gen generic? The answer is that generics automatically ensure the type safety of all operations involving Gen. In the process, generics eliminate the need for you to use casts and to type-check code by hand.

To understand the benefits of generics, first consider the following program that creates a non-generic equivalent of Gen:

 // NonGen is functionally equivalent to Gen // but does not use generics. using System; class NonGen {   object ob; // ob is now of type object   // Pass the constructor a reference of   // type object.   public NonGen(object o) {     ob = o;   }   // Return type object.   public object getob() {     return ob;   }   // Show type of ob.   public void showType() {     Console.WriteLine("Type of ob is " + ob.GetType());   } } // Demonstrate the non-generic class. class NonGenDemo {   public static void Main() {     NonGen iOb;     // Create NonGen object.     iOb = new NonGen(102);     // Show the type of data stored in iOb.     iOb.showType();     // Get the value in iOb.     // This time, a cast is necessary.     int v = (int) iOb.getob();     Console.WriteLine("value: " + v);     Console.WriteLine();     // Create another NonGen object and     // store a string in it.     NonGen strOb = new NonGen("Non-Generics Test");     // Show the type of data stored in strOb.     strOb.showType();     // Get the value of strOb.     // Again, notice that a cast is necessary.     String str = (string) strOb.getob();     Console.WriteLine("value: " + str);     // This compiles, but is conceptually wrong!     iOb = strOb;     // The following line results in a runtime exception.     // v = (int) iOb.getob(); // runtime error!   } }

This program produces the following output:

 Type of ob is System.Int32 value: 102 Type of ob is System.String value: Non-Generics Test

As you can see, the output is similar to the previous version of the program.

There are several things of interest in this version. First, notice that NonGen replaces all uses of T with object. This makes NonGen able to store any type of object, as can the generic version. However, it also prevents the compiler from having any real knowledge about the type of data actually stored in NonGen, which is bad for two reasons. First, explicit casts must be employed to retrieve the stored data. Second, many kinds of type mismatch errors cannot be found until runtime. Let’s look closely at each problem.

First, notice this line:

 int v = (int) iOb.getob();

Because the return type of getob( ) is now object, the cast to int is necessary to enable the value returned by getob( ) to be unboxed and stored in v. If you remove the cast, the program will not compile. In the generic version of the program, this cast was not needed because int was passed as a type argument when iOb was constructed. In the non-generic version, the cast must be employed. This is not only an inconvenience, but a potential source of error.

Now, consider the following sequence from near the end of the program:

 // This compiles, but is conceptually wrong! iOb = strOb; // The following line results in a runtime exception. // v = (int) iOb.getob(); // runtime error!

Here, strOb is assigned to iOb. However, strOb refers to an object that contains a string, not an integer. This assignment is syntactically valid because all NonGen references are the same, and any NonGen reference can refer to any other NonGen object. However, the statement is semantically wrong, as the commented-out line shows. In that line, the return type of getob( ) is cast to int, and then an attempt is made to assign this value to v. The trouble is that iOb now refers to an object that stores a string, not an int. Unfortunately, without use of generics, the compiler has no way to know this. Instead, a runtime exception will occur when the cast to int is attempted. To see this for yourself, try removing the comment symbol from the start of the line, and then compiling and running the program. A runtime error will occur.

The preceding sequence can’t occur when generics are used. If this sequence were attempted in the generic version of the program, the compiler would catch it and report an error, thus preventing a serious bug that results in a runtime exception. The ability to create type-safe code in which type-mismatch errors are caught at compile time is a key advantage of generics. Although using object references to create “generic” code has always been possible in C#, that code was not type-safe, and its misuse could result in runtime exceptions. Generics prevent this from occurring. In essence, through generics, what were once runtime errors have become compile-time errors. This is a major advantage.

There is one other point of interest in the NonGen program. Notice how the type of the NonGen instance variable ob is obtained by showType( ):

 Console.WriteLine("Type of ob is " + ob.GetType());

Recall from Chapter 11, object defines several methods that are available to all data types. One of these methods is GetType( ), which returns a Type object that describes the type of invoking object at runtime. Thus, even though the type of ob is specified as object in the program’s source code, at runtime, the actual type of object being referred to is known. This is why the CLR will generate an exception if you try an invalid cast during program execution.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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