The emergence of generics in. NET 2.0 is an exciting feature. But what is generic? Do you need them? Will you use them in your own applications? In this article, we will answer these questions and carefully analyze the usage, capabilities, and limitations of generics.
Preface
The emergence of generics in. NET 2.0 is an exciting feature. But what is generic? Do you need them? Will you use them in your own applications? In this article, we will answer these questions and carefully analyze the usage, capabilities, and limitations of generics.
Type Security
Many languages in. NET, such as C #, C ++, and VB. NET (the option strict is on) are strongly-typed languages. As a programmer, when you use these languages, you always expect the compiler to perform type security checks. For example, if you convert a reference to a Book type into a Vehicle reference, the compiler will tell you that such cast is invalid.
However, when talking about collections in. NET 1.0 and 1.1, they do not help type security. Consider an ArrayList example, which has an object set-this allows you to store any type of objects in this ArrayList. Let's take a look at the code in Example 1.
Example 1. Lack of type-safe ArrayList
Using System;
Using System. Collections;
Namespace TestApp
{
Class Test
{
[STAThread]
Static void Main (string [] args)
{
ArrayList list = new ArrayList ();
List. Add (3 );
List. Add (4 );
// List. Add (5.0 );
Int total = 0;
Foreach (int val in list)
{
Total = total + val;
}
Console. WriteLine ("Total is {0}", total );
}
}
}
In this example, we create an ArrayList instance and add 3 and 4 to it. Then, I cyclically traverse the ArrayList, retrieve the integer values, and add them together. This program will generate the result "Total is 7 ". Now, if I comment out the following sentence:
List. Add (1, 5.0 );
The program will generate the following runtime exceptions:
Unhandled Exception: System. InvalidCastException: Specified cast is not valid.
AtTestApp. Test. Main (String [] args) in: workareatestappclass1.cs: line 17
What went wrong? Remember that ArrayList has an object in a set. When you add 3 to the ArrayList, you have packed the value 3. When you loop through the list, you split the element into int type. However, when you add a value of 5.0, you are packing a double value. In row 17th, the double value is split into an int type. This is the cause of failure.
Note: If the above example is written in VB.net, it will not fail. The reason is that VB. NET does not use the packing mechanism. It activates a method to convert the double type to an integer type. However, if the value in the ArrayList cannot be converted to an integer, the VB. NET Code fails.
As a programmer who is used to the type security provided by the language, you want such problems to emerge during compilation rather than during runtime. This is why generics are generated.
What is generic
Generics allow you to implement type security during compilation time. They allow you to create a data structure but not limited to a specific data type. However, when this data structure is used, the compiler ensures that the type used is consistent with the type security. Generics provide type security, but do not cause any performance loss or code bloated. In this regard, they are similar to the templates in C ++, but they are quite different in implementation.
Use a generic set
The. NET 2.0 System. Collections. Generics namespace contains generic set definitions. Different Collection/container classes are "parameterized. To use them, simply specify the parameterized type.
Example 2. Type-safe generic list
List <int> aList = new List <int> ();
AList. Add (3 );
AList. Add (4 );
// AList. Add (5.0 );
Int total = 0;
Foreach (int val in aList)
{
Total = total + val;
}
Console. WriteLine ("Total is {0}", total );
In Example 2, I wrote an example of a generic list, specifying the parameter type as int in angle brackets. The execution of this code will produce the result "Total is 7 ". Now, if I remove the comments of the statement doubleList. Add (5.0), I will get a compilation error. The compiler points out that it cannot send a value of 5.0 to the Method Add (), because this method only accepts the int type. Different from example 1, the code here implements type security.
CLR support for generics
Generics are not only a linguistic feature .. Net clr can recognize generics. In this sense, the use of generics is one of the most outstanding features of. NET. For each parameter used for generic type, the class is also not removed from the Microsoft intermediate language (MSIL ). In other words, your accessory set contains only one definition of your parameterized data structure or class, regardless of how many different types are used to express the parameterized type. For example, if you define a generic type MyList <T>, only one definition of this type appears in MSIL. When a program is executed, different classes are dynamically created, and each class should be a type of parameterized type. If you use MyList <int> and MyList <double>, two types are created. When your program is executed, let's further analyze this in Example 3.
Example 3. Create a generic class
// MyList. cs
# Region Using directives
Using System;
Using System. Collections. Generic;
Using System. Text;
# Endregion
Namespace CLRSupportExample
{
Public class MyList <T>
{
Private static int objCount = 0;
Public MyList ()
{ObjCount ++ ;}
Public int Count
{
Get
{Return objCount ;}
}
}
}
// Program. cs
# Region Using directives
Using System;
Using System. Collections. Generic;
Using System. Text;
# Endregion
Namespace CLRSupportExample
{
Class SampleClass {}
Class Program
{
Static void Main (string [] args)
{
MyList <int> myIntList = new MyList <int> ();
MyList <int> myIntList2 = new MyList <int> ();
MyList <double> myDoubleList = new MyList <double> ();
MyList <SampleClass> mySampleList = new MyList <SampleClass> ();
Console. WriteLine (myIntList. Count );
Console. WriteLine (myIntList2.Count );
Console. WriteLine (myDoubleList. Count );
Console. WriteLine (mySampleList. Count );
Console. WriteLine (new MyList <sampleclass> (). Count );
Console. ReadLine ();
}
}
}
In this example, I created a generic class called MyList. To parameterize It, I simply inserted a angle bracket. T in <> represents the type to be specified when the class is used. In the MyList class, a static field objCount is defined. I add its value to the constructor. So I can find out how many objects of that type are created by users of my class. Attribute Count returns the number of instances of the same type as the called instance.
In the Main () method, I created two instances of MyList <int> and one instance of MyList <double>, there are also two instances of MyList <SampleClass>, where SampleClass is a class that I have defined. Question: What is the value of Count (output of the above program? Try to answer this question before you continue reading.
Solved the problem above? Have you obtained the following answers?
The first two correspond to MyList <int>, the first one corresponds to MyList <double>, and the second one corresponds to MyList <SampleClass> -- here, only one instance of this type is created. The last 2 corresponds to MyList <SampleClass>, because another instance of this type is created in the code. The preceding example shows that MyList <int> is a different class from MyList <double>, while MyList <double> is a different class from MyList <SampleClass>. Therefore, in this example, we have four classes: MyList <T>, MyList <int>, MyList <double>, and MyList <X>. Note that although there are four MyList classes, only one is stored in MSIL. How can this be proved? See the MSIL code generated by the ildasm.exe tool.
(MSIL in Example 3)
Generic Method
In addition to generic classes, you can also have generic methods. Generic methods can be part of any class.
Example 4. A generic Method
Public class Program
{
Public static void Copy <T> (List <T> source, List <T> destination)
{
Foreach (T obj in source)
{
Destination. Add (obj );
}
}
Static void Main (string [] args)
{
List <int> lst1 = new List <int> ();
Lst1.Add (2 );
Lst1.Add (4 );
List <int> lst2 = new List <int> ();
Copy (lst1, lst2 );
Console. WriteLine (lst2.Count );
}
}
The Copy () method is a generic method that works with parameterized type T. When Copy () is activated in Main (), the compiler determines the type to be used based on the parameters provided to the Copy () method.
Unlimited type parameters
If you create a generic data structure or class, it is like the MyList in Example 3. Note that it does not constrain the type you should use to create a parameterized type. However, this imposes some restrictions. For example, you cannot use the parameter = In a parameterized instance ,! = Or <and other operators, such:
If (obj1 = obj2 )...
Like = and! = The implementation of such operators is different for the value type and reference type. If you allow it at will, the Code behavior may be unexpected. Another restriction is the use of the default constructor. For example, if your code is like new T (), a compilation error occurs, because not all classes have a constructor without parameters. If you create an object by encoding like new T (), or use the like = and! = What will happen to such operators? You can do this, but first you must limit the types that can be used for parameterized types. Readers can consider how to implement it first.
Constraints and their advantages
A generic class allows you to write your own classes without sticking to any types, but allows users of your classes to specify the specific types to be used in the future. By imposing constraints on types that may be used for parameterization, this gives your programming great flexibility-you can control the creation of your own classes. Let's analyze an example:
Example 5. constraints: the code will not be compiled successfully.
Public static T Max <T> (T op1, T op2)
{
If (op1.CompareTo (op2) <0)
Return op1;
Return op2;
}
The code in Example 5 produces a compilation error:
Error 1 't'does not contain a definition for 'compute'
Suppose I need this type to support the implementation of the CompareTo () method. I can specify this by imposing constraints -- the type specified for the parameterized type must implement the IComparable interface. The code in Example 6 is as follows:
Example 6. Specify a constraint
Public static T Max <T> (T op1, T op2) where T: IComparable
{
If (op1.CompareTo (op2) <0)
Return op1;
Return op2;
}
In Example 6, the constraint I specify is that the type used for parameterized types must inherit from (Implementation) Icomparable. The following constraints can be used:
Where T: the struct type must be a value type (struct)
Where T: The class type must be a reference type (class)
Where T: The new () type must have a non-parameter constructor.
Where T: The class_name type can be class_name or a subclass of it.
Where T: The specified interface must be implemented for the interface_name type.
You can specify a combination of constraints, such as: where T: IComparable, new (). That is to say, the type used for parameterized types must implement the Icomparable interface and have a no-argument constructor.
Inheritance and generics
A generic class that uses parameterized types, such as MyClass1 <T>, is called an open-structure generic class. A generic class that does not use parameterized types, such as MyClass1 <int>, is called a generic closed structure.
You can derive from a generic closed structure. That is to say, you can derive a class called MyClass2 from another class called MyClass1, just like: public class MyClass2 <T>: myClass1 <int>; you can also derive from an open-structure generic type. If the type is parameterized, for example, public class MyClass2 <T>: yClass2 <T>, is valid, but public class MyClass2 <T>: MyClass2 <Y> is invalid. Here, Y is a parameterized type. Non-generic classes can be derived from a closed-structure generic class, but not from an open-structure generic class. That is, public class MyClass: MyClass1 <int> is valid, but public class MyClass: MyClass1 <T> is invalid.
Generic and alternative
When using generics, we need to be careful about substitution. If B inherits from A, object B may be used where object A is used. Assume that we have a Basket of Fruits (Basket <Fruit>) and Apple and Banana (all of which belong to the Fruit class ). A Basket of Apples-Can Basket of Apples (Basket <apple>) be inherited from Basket of Fruits (Basket <Fruit> )? The answer is no. If we consider alternative. Why? Consider how a Basket of Fruits can work:
Public void Package (Basket <Fruit> aBasket)
{
ABasket. Add (new Apple ());
ABasket. Add (new Banana ());
}
If you send a Basket <Fruit> instance to this method, this method adds an Apple object and a Banana object. However, what happens when I send a Basket <Apple> instance to this method? You see, this is full of tips. This explains why the following code:
Basket <Apple> anAppleBasket = new Basket <Apple> ();
Package (anAppleBasket );
An error occurs:
Error 2 Argument '1 ':
Cannot convert from 'testapp. Basket <TestApp. apple>'
To 'testapp. Basket <TestApp. fruit>'
The compiler protects our code by ensuring that we do not pass a set's derived class at will (a set's base class is required at this time. Isn't that good?
This is successful in the above example, but there are also special circumstances: Sometimes we really want to pass a set of derived classes, at this time we need a set of base classes. For example, if Animal (such as Monkey) is considered, it has a Basket <Fruit> as the parameter method Eat, as shown below:
Public void Eat (Basket <Fruit> fruits)
{
Foreach (Fruit aFruit in fruits)
{
// Code www.2cto.com
}
}
Now you can call:
Basket <Fruit> fruitsBasket = new Basket <Fruit> ();
... // Add the object Fruit to the Basket object
AnAnimal. Eat (fruitsBasket );
What if you have a Basket (a Basket of) Banana-a Basket <Banana>? Does it make sense to send a Basket (a Basket of) Banana-a Basket <Banana> to the Eat method? In this case, will it succeed? In this case, the compiler will give an error message:
Basket <Banana> bananaBasket = new Basket <Banana> ();
//...
AnAnimal. Eat (bananaBasket );
The compiler protects our code here. How can we ask the compiler to allow such special circumstances? The constraint mechanism once again helps us:
Public void Eat <t> (Basket <t> fruits) where T: Fruit
{
Foreach (Fruit aFruit in fruits)
{
// The code for eating fruit
}
}
When establishing the method Eat (), I asked the compiler to allow a Basket (a Basket of) of any type T. Here T is the Fruit type or any class inherited from Fruit.
Generic and proxy
Proxy can also be generic. This brings great flexibility.
Suppose we are very interested in writing a framework program. We need to provide a mechanism for event sources to communicate with objects of interest to the event. Our framework may not be able to control what the event is. You may be dealing with some kind of Stock price Change (double price), and I may be dealing with temperature changes (temperature value) in the kettle ), here, Temperature can be an object with values, units, thresholds, and other information. So how can we define an interface for these events?
Let's use the pre-generic proxy technology to analyze how to implement these:
Public delegate void policydelegate (Object info );
Public interface ISource
{
Event policydelegate policyactivity;
}
Let yydelegate accept an object. This is the best practice we have taken in the past, because the Object can be used to represent different types, such as double, Temperature, and so on-although the Object contains the overhead of packing because of the value type. ISource is an interface that is supported by different sources. The framework shows the NotifyDelegate proxy and ISource interface.
Let's look at two different source codes:
Public class StockPriceSource: ISource
{
Public event policydelegate policyactivity;
//...
}
Public class BoilerSource: ISource
{
Public event policydelegate policyactivity;
//...
}
If we each have an object of the above class, we will register a processor for the event, as shown below:
StockPriceSource stockSource = new StockPriceSource ();
StockSource. policyactivity
+ = New NotifyDelegate (stocksource_policyactivity );
// This does not need to appear in the same program
BoilerSource boilerSource = new BoilerSource ();
BoilerSource. policyactivity
+ = New NotifyDelegate (boilersource_policyactivity );
In the proxy processor method, we need to do the following:
For the stock event processor, we have:
Void stocksource_policyactivity (object info)
{
Double price = (double) info;
// Type required by downcast before use
}
The temperature event processor looks like:
Void boilersource_policyactivity (object info)
{
Temperature value = info as Temperature;
// Type required by downcast before use
}
The above code is not intuitive and messy because downcast is used. With the help of generics, the code will become easier to read and use. Let's take a look at the working principle of generics:
The following are proxies and interfaces:
Public delegate void policydelegate <t> (T info );
Public interface ISource <t>
{
Event policydelegate <t> policyactivity;
}
We have parameterized the proxies and interfaces. The implementation of the current interface should be able to determine what type it is.
The source code of Stock looks like this:
Public class StockPriceSource: ISource <double>
{
Public event policydelegate <double> policyactivity;
//...
}
The source code of Boiler looks like this:
Public class BoilerSource: ISource <temperature>
{
Public event policydelegate <temperature> policyactivity;
//...
}
If we each have an object of each of the above categories, we will register a processor for the event as follows:
StockPriceSource stockSource = new StockPriceSource ();
StockSource. yyactivity + = new NotifyDelegate <double> (stocksource_policyactivity );
// This does not need to appear in the same program
BoilerSource boilerSource = new BoilerSource ();
BoilerSource. NotifyActivity + = new NotifyDelegate <temperature> (boilersource_policyactivity );
Now, the event processor for the stock price will be:
Void stocksource_policyactivity (double info)
{//... }
The temperature event processor is:
Void boilersource_policyactivity (Temperature info)
{//... }
The code here is not used as downcast and the type used is clear.
Generics and reflection
Since generics are supported at the CLR level, you can use the reflection API to obtain information about generics. If you are a beginner in programming, you may be confused about one thing: You must remember that there are both generic classes you have written and types created from this generic class at runtime. Therefore, when using the reflection API, you need to remember which type you are using. I will illustrate this in Example 7:
Example 7. Reflection on Generics
Public class MyClass <t> {}
Class Program
{
Static void Main (string [] args)
{
MyClass <int> obj1 = new MyClass <int> ();
MyClass <double> obj2 = new MyClass <double> ();
Type type1 = obj1.GetType ();
Type type2 = obj2.GetType ();
Console. WriteLine ("obj1's Type ");
Console. WriteLine (type1.FullName );
Console. WriteLine (type1.GetGenericTypeDefinition (). FullName );
Console. WriteLine ("obj2's Type ");
Console. WriteLine (type2.FullName );
Console. WriteLine (type2.GetGenericTypeDefinition (). FullName );
}
}
In this example, there is an instance of MyClass <int>, and the program needs to query the class name of this instance. Then I query this type of GenericTypeDefinition (). GenericTypeDefinition () returns the type metadata of MyClass <T>. You can call IsGenericTypeDefinition to check whether this is a generic type (like MyClass <T>) or whether its type parameter has been specified (like MyClass <int> ). Similarly, I query the metadata of an instance of MyClass <double>. The above program output is as follows:
Obj1's Type
TestApp. MyClass '1
[[System. Int32, mscorlib, Version = 2.0.0.0, Culture = neutral,
PublicKeyToken = b77a5c561934e089]
TestApp. MyClass '1
Obj2's Type
TestApp. MyClass '1
[[System. Double, mscorlib, Version = 2.0.0.0, Culture = neutral,
PublicKeyToken = b77a5c561934e089]
TestApp. MyClass '1
As you can see, MyClass <int> and MyClass <double> belong to the mscorlib accessory set (dynamically created), while MyClass <t> belongs to the self-built accessory set.
Limitations of generics
So far, we have learned the power of generics. Is there any deficiency? I found one. I hope Microsoft can clearly point out the existence of generics. When expressing constraints, we can specify that the parameter type must inherit from a class. However, what should I do if the specified parameter must be of a certain type of base type? Why?
In Example 4, I demonstrate a Copy () method that can Copy the content of a source List to a target list. I can use it as follows:
List <Apple> appleList1 = new List <Apple> ();
List <Apple> appleList2 = new List <Apple> ();
...
Copy (appleList1, appleList2 );
However, what if I want to copy an apple object from one list to another Fruit list (Apple inherits from Fruit? Of course, a Fruit list can accommodate Apple objects. So I want to write code like this:
List <Apple> appleList1 = new List <Apple> ();
List <Fruit> fruitsList2 = new List <Fruit> ();
...
Copy (appleList1, fruitsList2 );
This will not be compiled successfully. You will get an error:
Error 1 The type arguments for method
'Testapp. Program. Copy <t> (System. Collections. Generic. List <t>,
System. Collections. Generic. List <t>) 'Could not be inferred from the usage.
The compiler does not determine what T should be based on the call parameters. In fact, I want to say that the Copy method should take a List of certain data types as the first parameter, and a List of the same type or its base type as the second parameter.
Although it cannot be said that one type must be another type of base type, you can still use the constraint mechanism to overcome this restriction. The following is the implementation of this method:
Public static void Copy <T, E> (List <t> source,
List <e> destination) where T: E
Here, I have specified that the type T must belong to the same type as E or a subtype of E. We are lucky. Why? T and E are defined here! We can specify such constraints (however, C # does not encourage E to define T constraints when E is also defined ).
However, consider the following code:
Public class MyList <t>
{
Public void CopyTo (MyList <t> destination)
{
//...
}
}
I should be able to call CopyTo:
MyList <apple> appleList = new MyList <apple> ();
MyList <apple> appleList2 = new MyList <apple> ();
//...
AppleList. CopyTo (appleList2 );
I must also do the following:
MyList <apple> appleList = new MyList <apple> ();
MyList <fruit> fruitList2 = new MyList <fruit> ();
//...
AppleList. CopyTo (fruitList2 );
Of course this will not succeed. How to modify it? We can say that the CopyTo () parameter can be a certain type of MyList or a base type of MyList. However, the constraint mechanism does not allow us to specify a base type. What should we do next?
Public void CopyTo <e> (MyList <e> destination) where T: E
Sorry, this does not work. It will give a compilation error:
Error 1 'testapp. MyList <t>. CopyTo <e> () 'does not define type
Parameter 'T'
Of course, you can write the code to receive any type of MyList, and then verify that this type is a type that can be received in the code. However, this pushes the check work to the running time and removes the advantage of type security during compilation.
Conclusion
The generic type in. NET 2.0 is powerful. The code you write does not have to be limited to a specific type, but your code can have type security. The objective of generic implementation is to improve the program performance without causing code bloated. However, when its constraint mechanism is insufficient (one type must be another type of the base type cannot be specified, this constraint mechanism also gives you great flexibility in writing code, because you do not have to stick to the various types of "minimum public Denominator" capabilities.
I hope you can post your comments to the registered users after reading this article. More learning materials will be provided!
From Hao_Guo