C #2.0 Introduction
C #2.0 introduces many language extensions. The most important ones are generic, anonymous methods, iterators, and partial types ).
• Generics allow classes, structures, interfaces, delegates, and methods to be parameterized by the data types they store and operate on. Generics are useful because they provide more powerful type checks during compilation and require Explicit conversions between fewer data types, in addition, it reduces the need for packing and runtime type checks.
• The anonymous method allows you to write code blocks in "in-line" mode when delegate values are required. The anonymous method is similar to Lambda functions in lisp.
• The iterator can incrementally calculate and generate a series of worthwhile methods. The iterator enables a class to easily explain how the foreach statement iterates each of its elements.
• Incomplete types allow classes, structures, and interfaces to be divided into multiple small blocks and stored in different source files for easy development and maintenance. In addition, incomplete types can be used to separate the code generated by the machine and the part written by the user, which makes it easy to use tools to enhance the generated code.
This chapter first introduces these new features. The introduction is followed by four chapters that provide complete technical specifications for these features.
The Language extension design in C #2.0 ensures high compatibility with existing code. For example, although C #2.0 assigns special meaning to the word where, yield, and partial in a specific environment, these words can still be used as identifiers. Indeed, C #2.0 does not add a keyword that will conflict with the identifier in the existing code.
19.1 generic
Generics allow classes, structures, interfaces, delegates, and methods to be parameterized by the data types they store and operate on. C # generics are very friendly to users who use extensions in the Eifel Or Ada Language and users who use the C ++ template, although they may not be able to tolerate the complexity of the latter.
19.1.1 why is it generic?
No generics. Some common data structures can only store various types of data using the object type. For example, the following simple Stack class stores its data in an object array, and its two methods, push and pop, use the object to accept and return data respectively:
Public class Stack
{
Object [] items;
Int count;
Public void push (Object item ){...}
Public object POP (){...}
}
Although the object type is used to make the Stack class very flexible, it is not a disadvantage. For example, you can press any type of value to the stack, such as a customer instance. However, when retrieving a value again, you must explicitly convert the value returned by the POP method to an appropriate type. Writing these conversion changes is tedious to guard against runtime type check errors:
Stack stack = new stack ();
Stack. Push (new customer ());
Customer c = (customer) stack. Pop ();
If a value of the value type, such as int, is passed to the push method, it is automatically packed. When you retrieve the int value later, you must perform explicit type conversion to unpack it:
Stack stack = new stack ();
Stack. Push (3 );
Int I = (INT) stack. Pop ();
This kind of packing and unpacking operation increases the execution burden, because it brings about dynamic memory allocation and runtime type check.
Another problem with the Stack class is that it is impossible to force the types of data in the stack. Indeed, a customer instance can be pushed into the stack, but it will be converted to an incorrect type when retrieved:
Stack stack = new stack ();
Stack. Push (new customer ());
String S = (string) stack. Pop ();
Although the above Code is an incorrect use of the stack class, it is technically correct and will not cause errors during compilation. This error occurs only when the code is run. An invalidcastexception is thrown.
The Stack class will undoubtedly benefit from its ability to limit its element types. Using Generics makes it possible.
19.1.2 create and use generic
Generics provide a technique to create a type with type parameters. The following example declares a generic Stack class with type parameter T. The delimiters "<" and ">" after the type parameter and class name are specified. The <t> instance created by a stack of a certain type can accept the data of this type without any conversion, which is too strong to replace with the object. Type parameter t acts as a placeholder until an actual type is specified during use. Note that t is equivalent to the Data Type of the internal array, the parameter type accepted by the push method, and the return value type of the POP method:
Public class Stack <t>
{
T [] items;
Int count;
Public void push (T item ){...}
Public t POP (){...}
}
When using a generic stack <t>, you must specify the actual type to replace T. In the following example, int is specified as the parameter type T:
Stack <int> stack = new stack <int> ();
Stack. Push (3 );
Int x = stack. Pop ();
The stack <int> type is called the constructed type ). All t values in the <int> type of the stack are replaced with the type parameter Int. When a stack <int> instance is created, the local storage of the items array is int [] rather than object [], which provides a substantial storage, it is more efficient than non-generic stack. Similarly, the push and pop methods in the stack <int> only operate on Int values. If you press other types of values into the stack, errors during compilation will be obtained, when retrieving a value, it cannot be displayed as the original type.
A wildcard can provide a strong type, which means that, for example, pushing an int to the stack of a customer object will produce an error. This is because Stack <int> can only operate on Int values, while stack <customer> can only operate on customer objects. The last two lines in the following example cause the compiler to report an error:
Stack <customer> stack = new stack <customer> ();
Stack. Push (new customer ());
Customer c = stack. Pop ();
Stack. Push (3); // Type Mismatch Error
Int x = stack. Pop (); // Type Mismatch Error
The generic type declaration allows any number of type parameters. In the preceding stack <t> example, there is only one type parameter, but a generic dictionary class may have two types of parameters, one is the key type and the other is the value type:
Public class dictionary <K, V>
{
Public void add (K key, V value ){...}
Public v This [k key] {...}
}
When using dictionary <K, V>, you must provide two types of parameters:
Dictionary <string, Customer> dict = new dictionary <string, Customer> ();
Dict. Add ("Peter", new customer ());
Customer c = dict ["Peter"];
19.1.3 generic type instantiation
Similar to non-generic types, compiled generic types are also represented by intermediate language (IL, intermediate language) commands and metadata. The Il of the generic type is encoded by the type parameter.
When a program creates a constructed generic type instance for the first time, such as stack <int> ,. net instant Compiler (JIT, just-in-time) in the public language runtime converts generic Il and metadata into local code, and replaces type parameters with actual types in the process. The reference to the constructed generic type follows the same local code. The process of creating a specific construction type from the generic type is called the generic type instantiation ).
. Net public language runtime creates a special copy for each generic type instantiated by the type, and all the reference types share a separate copy (because, at the local code level, reference knowledge has pointers of the same performance ).
19.1.4 Constraints
Generally, a generic class does not store data based on a certain type of parameters, but also calls methods for a given type of objects. For example, the add method in dictionary <K, V> may need to use the compareto method to compare the key values:
Public class dictionary <K, V>
{
Public void add (K key, V value)
{
...
If (key. compareto (x) <0) {...} // error, no compareto Method
...
}
}
Because the specified type parameter K can be of any type, it can be assumed that the existing parameter key has only members from the object, such as equals, gethashcode, and tostring; therefore, a compilation error occurs in the preceding example. Of course, you can convert the parameter key to a compareto method type. For example, the parameter key can be converted to icomparable:
Public class dictionary <K, V>
{
Public void add (K key, V value)
{
...
If (icomparable) Key). compareto (x) <0 ){...}
...
}
}
When this scheme works, it will cause dynamic type conversion at runtime, which will increase the overhead. Even worse, it may also delay the error report to runtime. If a key does not implement the icomparable interface, an invalidcastexception is thrown.
To provide more powerful type checking during compilation and reduce type conversion, C # allows an optional Constraints List for each type parameter. The constraints of a type parameter specify the requirements that a type must comply with, so that this type parameter can be used as a variable. The constraint is declared by the keyword where, followed by the name of the type parameter, followed by a list of classes or interface types, or the constructor constraint new ().
To enable the dictionary <K, V> class to ensure that the key value always implements the icomparable interface, the class declaration should specify a constraint for the type parameter K:
Public class dictionary <K, V> where K: icomparable
{
Public void add (K key, V value)
{
...
If (key. compareto (x) <0 ){...}
...
}
}
With this declaration, the compiler can ensure that all types provided to the type parameter k implement the icomparable interface. Furthermore, you do not need to explicitly convert a key value to an icomparable interface before calling the compareto method. All members of a value of the constrained type parameter type can directly use it.
For a given type parameter, you can specify any number of interfaces as constraints, but you can only specify one class (as constraints ). Each constrained type parameter has an independent where clause. In the following example, type parameter K has two interface constraints, and type parameter E has a class constraint and a constructor constraint:
Public class entitytable <K, E>
Where K: icomparable <k>, ipersistable
Where E: entity, new ()
{
Public void add (K key, e entity)
{
...
If (key. compareto (x) <0 ){...}
...
}
}
The constructor constraint in the preceding example, new (), ensures that the type of the E-type variable has a common, no-argument constructor, and allows generic classes to use new E () to create an instance of this type.
Be careful when using the type parameter constraints. Although they provide more powerful type checks during compilation and improve performance in some cases, they still limit the use of generic types. For example, a generic list <t> may constrain t to implement the icomparable interface so that the sort method can compare the elements. However, in this case, list <t> cannot be used for those types that do not implement the icomparable interface, even though the sort method has never been actually called in this case.
19.1.5 generic Method
Sometimes a type parameter is not required by the entire class, but only used in a specific method. Generally, this occurs when you create a method that requires a generic type as a parameter. For example, when using the stack <t> class described earlier, a common mode is to input multiple values in one row, it is very convenient to write a method to complete this work by calling its class separately. For a specific constructed type, such as stack <int>, this method looks like this:
Void pushmultiple (stack <int> stack, Params int [] values ){
Foreach (INT value in values) stack. Push (value );
}
This method can be used to press multiple int values into one stack <int>:
Stack <int> stack = new stack <int> ();
Pushmultiple (stack, 1, 2, 3, 4 );
However, the above method can only work on a specific constructed type stack <int>. To make him work on any stack <t>, this method must be written as a generic method ). A generic method has one or more type parameters, which are specified by the "<" and ">" delimiters after the method name. This type of parameter can be used in the parameter list, return to, and method body. A generic pushmultiple method looks like this:
Void pushmultiple <t> (stack <t> stack, Params T [] values ){
Foreach (T value in values) stack. Push (value );
}
With this method, you can press multiple elements into any stack <t>. When calling a generic method, type parameters should be placed in angle brackets in the function call. For example:
Stack <int> stack = new stack <int> ();
Pushmultiple <int> (stack, 1, 2, 3, 4 );
This generic pushmultiple method is more reusable than the previous version because it can work on any stack <t>, but it does not seem comfortable because it must provide a type parameter for T. However, many times the compiler can infer the correct type parameter by passing other parameters to the method. This process is called type inferencing ). In the preceding example, because the first formal parameter type is stack <int> and all subsequent parameter types are int, the compiler can determine that the type parameter must be Int. Therefore, you do not need to provide the type parameter when calling the generic pushmultiple method:
Stack <int> stack = new stack <int> ();
Pushmultiple (stack, 1, 2, 3, 4 );
19.2 anonymous method
Practice and other callback methods usually need to be called through a special delegate, rather than directly calling. Therefore, so far, we can only put a code for practical processing and callback in a specific method, and then explicitly create a delegate for it. Instead, the anonymous method allows the code "in-line" associated with a delegate to the place where the delegate is used, we can easily write the code directly in the delegated instance. In addition to being comfortable, the anonymous method also shares access to function members contained in local statements. If you want to achieve this type of sharing in the naming method (different from the anonymous method), you need to manually create a helper class and "upgrade (Lifting)" The local member to the class domain.
The following example shows how to obtain a simple input from a form that contains a list box, a text box, and a button. When the button is pressed, the text in the text box is added to the list box.
Class inputform: Form
{
ListBox;
Textbox;
Button addbutton;
Public myform (){
ListBox = new ListBox (...);
Textbox = new Textbox (...);
Addbutton = new button (...);
Addbutton. Click + = new eventhandler (addclick );
}
Void addclick (Object sender, eventargs e ){
ListBox. Items. Add (textbox. Text );
}
}
Although there is only one statement in the response to the button click event, this statement must also be placed in an independent method with a complete parameter list, and you must manually create the eventhandler delegate that references this method. With the anonymous method, the code for event processing becomes more concise:
Class inputform: Form
{
ListBox;
Textbox;
Button addbutton;
Public myform (){
ListBox = new ListBox (...);
Textbox = new Textbox (...);
Addbutton = new button (...);
Addbutton. Click + = delegate {
ListBox. Items. Add (textbox. Text );
};
}
}
An anonymous method consists of the keyword delegate and an optional parameter list, and puts the statement into the "{" and "}" qualifiers. The anonymous method in the preceding example does not use the parameters provided to the delegate. Therefore, the parameter list can be omitted. To access parameters, your name and method should contain a list of parameters:
Addbutton. Click + = delegate (Object sender, eventargs e ){
MessageBox. Show (button) sender). Text );
};
In the preceding example, an implicit conversion occurs between the anonymous method and the eventhandler delegate type (the type of the click event. This implicit conversion is feasible because the parameter list and return value types of this delegate are compatible with anonymous methods. The exact compatibility rules are as follows:
• If one of the following statements is true, the delegated parameter list is compatible with the anonymous method:
O The anonymous method does not have a parameter list and the delegate does not have an output (out) parameter.
O The parameter list of the anonymous method precisely matches the number, type, and modifier with the delegate parameter.
• If one of the following statements is true, the delegate return value is compatible with the anonymous method:
O the return value type of the delegate is void, and the anonymous method does not have a return statement or its return statement does not contain any expressions.
O the return value type of the delegate is not void, but the value of the expression associated with the return statement of the anonymous method can be explicitly converted to the return value type of the delegate.
Only when the parameter list and return value types are compatible can an anonymous type be implicitly converted to the delegate type.
The following example uses the anonymous method to "inline (in-lian)" the function )". The anonymous method is passed as a function delegate type.
Using system;
Delegate double function (Double X );
Class Test
{
Static double [] apply (double [] A, function f ){
Double [] result = new double [A. Length];
For (INT I = 0; I <A. length; I ++) Result = f ();
Return result;
}
Static double [] multiplyallby (double [] A, double factor ){
Return apply (A, delegate (Double X) {return x * factor ;});
}
Static void main (){
Double [] A = {0.0, 0.5, 1.0 };
Double [] squares = Apply (A, delegate (Double X) {return x * X ;});
Double [] doubles = multiplyallby (A, 2.0 );
}
}
The apply method requires a given function that accepts the double [] element and returns the double [] as the result. In the main method, the second parameter passed to the apply method is an anonymous method, which is compatible with the function delegate type. This anonymous method only returns the square value of each element. Therefore, the double [] obtained by calling the apply method contains the square value of each value in.
The multiplyallby method creates a double [] by multiplying each value in the parameter array by a given factor and returns the result. To generate this result, the multiplyallby method calls the apply method and passes it an anonymous method that can multiply the X parameter and the factor.
If the scope of a local variable or parameter includes an anonymous method, the variable or parameter is called an external variable (outer variables) of the anonymous method ). In the multiplyallby method, A and factor are the external variables passed to the anonymous method of the apply method. Generally, the lifetime of a local variable is restricted within the block or associated statement. However, the lifetime of a captured external variable needs to be extended to at least when the delegate reference to the anonymous method meets the garbage collection condition.
19.2.1 method group Conversion
As described in the previous section, an anonymous method can be implicitly converted to a compatible delegate type. C #2.0 allows the same conversion for a group of methods, that is, the explicit instantiation of a delegate can be omitted at any time. For example, the following statement:
Addbutton. Click + = new eventhandler (addclick );
Apply (A, new function (math. Sin ));
You can also write:
Addbutton. Click + = addclick;
Apply (A, math. Sin );
When short form is used, the compiler can automatically infer which delegate type should be instantiated, but the effect is the same as that of long form.
19.3 iterator
The foreach statement in C # is used to iterate the elements in an enumerable set. To implement enumerable functions, a set must have a getenumerator method with no parameters and an enumerator. Generally, enumerators are difficult to implement. Therefore, it is important to simplify the tasks of enumerators.
Iterator is a statement block that generates an ordered sequence of (yields) values. The iterator distinguishes one or more yield statements from other statement blocks:
• The yield Return Statement generates the next value of this iteration.
• The yield break statement indicates that this iteration is complete.
As long as the return value of a function member is an enumerator interfaces or enumerable interfaces, we can use the iterator:
• The so-called enumerator is an excuse for system. Collections. ienumerator and a type constructed from system. Collections. Generic. ienumerator <t>.
• The enumerated interface refers to the system. Collections. ienumerable and constructed from system. Collections. Generic. ienumerable <t>.
It is important to understand that an iterator is not a member, but a function member. A member implemented by the iterator can be overwritten or overwritten by one or more members that use or do not use the iterator.
The following stack <t> class uses the iterator to implement its getenumerator method. The iterator enumerates the elements in the stack in the order from the top to the end.
Using system. Collections. Generic;
Public class Stack <t>: ienumerable <t>
{
T [] items;
Int count;
Public void push (t data ){...}
Public t POP (){...}
Public ienumerator <t> getenumerator (){
For (INT I = count-1; I> = 0; -- I ){
Yield return items;
}
}
}
The getenumerator method makes the stack <t> An Enumeration type, which allows the instance of the stack <t> to use the foreach statement. In the following example, values 0 to 9 are pushed into an integer stack, and each value is displayed in the order from the top to the end using the foreach loop.
Using system;
Class Test
{
Static void main (){
Stack <int> stack = new stack <int> ();
For (INT I = 0; I <10; I ++) stack. Push (I );
Foreach (int I in stack) console. Write ("{0}", I );
Console. writeline ();
}
}
The output in this example is:
9 8 7 6 5 4 3 2 1 0
The statement implicitly calls the getenumerator method without parameters of the set to obtain an enumerator. Only one getenumerator method without parameters can be defined in a collection class. However, there are many ways to implement enumeration, including using parameters to control enumeration. In these cases, an iterator can be used to return the attributes and methods of the enumerated interface. For example, stack <t> can introduce two new attributes: ienumerable <t> toptobottom and bottomtotop:
Using system. Collections. Generic;
Public class Stack <t>: ienumerable <t>
{
T [] items;
Int count;
Public void push (t data ){...}
Public t POP (){...}
Public ienumerator <t> getenumerator (){
For (INT I = count-1; I> = 0; -- I ){
Yield return items;
}
}
Public ienumerable <t> toptobottom {
Get {
Return this;
}
}
Public ienumerable <t> bottomtotop {
Get {
For (INT I = 0; I <count; I ++ ){
Yield return items;
}
}
}
}
The get accessor of the toptobottom attribute only returns this, because the stack itself is an enumerable type. The bottomtotop attribute uses the C # iterator to return an enumeration interface. The following example shows how to use these two attributes to enumerate the elements in the stack in any order:
Using system;
Class Test
{
Static void main (){
Stack <int> stack = new stack <int> ();
For (INT I = 0; I <10; I ++) stack. Push (I );
Foreach (int I in stack. toptobottom) console. Write ("{0}", I );
Console. writeline ();
Foreach (int I in stack. bottomtotop) console. Write ("{0}", I );
Console. writeline ();
}
}
Of course, these attributes can also be used outside the foreach statement. The following example passes the result of calling the property to an independent print method. This example also shows the method body used by an iterator as a fromtoby method with parameters:
Using system;
Using system. Collections. Generic;
Class Test
{
Static void print (ienumerable <int> collection ){
Foreach (int I in Collection) console. Write ("{0}", I );
Console. writeline ();
}
Static ienumerable <int> fromtoby (int from, int to, int ){
For (INT I = from; I <= to; I + = ){
Yield return I;
}
}
Static void main (){
Stack <int> stack = new stack <int> ();
For (INT I = 0; I <10; I ++) stack. Push (I );
Print (stack. toptobottom );
Print (stack. bottomtotop );
Print (fromtoby (10, 20, 2 ));
}
}
The output in this example is:
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
10 12 14 16 18 20
The generic and non-generic enumerated interfaces have only one independent Member, and the getenumerator method without parameters returns an enumeration interface. An enumerator factory is similar to an enumerator factory ). Each time you call the getenumerator method of a class that correctly implements the enumeration interface, an independent enumerator is generated.
Using system;
Using system. Collections. Generic;
Class Test
{
Static ienumerable <int> fromto (int from, int ){
While (from <= to) yield return from ++;
}
Static void main (){
Ienumerable <int> E = fromto (1, 10 );
Foreach (int x in E ){
Foreach (INT y in E ){
Console. Write ("{0, 3}", x * y );
}
Console. writeline ();
}
}
}
The code above prints a simple multiplication table from 1 to 10. Note that the fromto method is called only once to generate enumeration interface E. E. getenumerator () is called multiple times (using the foreach statement) to generate multiple identical enumerators. These enumerators encapsulate the code specified in the fromto declaration. Note that the from parameter is changed during iteration. However, the enumerator is independent, because for the from parameter and to parameter, each enumerator has its own copy. When you implement an enumerative class or an enumerator class, the transition between the enumerators (an unstable state) is one of the many subtle flaws that must be eliminated. The design of the iterator in C # can help eliminate these problems and implement robust enumeration classes and enumerators classes in a simple and instinctive way.
19.4 incomplete type
Although maintaining all types of code in a single file is a good programming practice, sometimes when a class becomes very large, this becomes an unrealistic constraint. In addition, programmers often use the code generator to generate an initial structure of an application and then modify the generated code. Unfortunately, when the original code needs to be released again in the future, the existing corrections will be overwritten.
Incomplete types allow classes, structures, and interfaces to be divided into multiple small blocks and stored in different source files for easy development and maintenance. In addition, incomplete types can be used to separate the code generated by the machine and the part written by the user, which makes it easy to use tools to enhance the generated code.
To define a type in multiple parts, we use a new modifier -- partial. The following example implements an incomplete class in two parts. These two parts may be in different source files. For example, the first part may be generated by the machine through the database shadow tool, and the second part is manually created:
Public partial Class Customer
{
Private int ID;
Private string name;
Private string address;
Private list <order> orders;
Public customer (){
...
}
}
Public partial Class Customer
{
Public void submitorder (order ){
Orders. Add (order );
}
Public bool hasoutstandingorders (){
Return orders. Count> 0;
}
}
When the two parts are compiled together, the generated code is as if this class was written in a unit:
Public Class Customer
{
Private int ID;
Private string name;
Private string address;
Private list <order> orders;
Public customer (){
...
}
Public void submitorder (order ){
Orders. Add (order );
}
Public bool hasoutstandingorders (){
Return orders. Count> 0;
}
}
All parts of incomplete types must be compiled together before they can be merged during compilation. Note that incomplete types do not allow extension of compiled types.