Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005

.pdf
Скачиваний:
81
Добавлен:
12.02.2016
Размер:
2.09 Mб
Скачать

264 C H A P T E R 1 1 G E N E R I C S

Generic Methods

C# supports generic methods. Any method declaration that exists within a struct, a class, or an interface may be declared as a generic method. That includes static as well as virtual or abstract methods. To declare a generic method, simply append a type argument list to the end of the method name but before the parameter list for the method. You can declare any of the types in the method parameter list, including the method return type, using one of the generic parameters. As with nested classes, it is bad form to hide outer type identifiers by reusing the same identifier in the nested scope, which in this case, is the scope of the generic method. Let’s consider an example of where a generic method may be useful. Take a look at this example where I’ve created a container to which I want to add the contents of another generic container:

using System;

using System.Collections.Generic;

public class MyContainer<T> : IEnumerable<T>

{

public void Add( T item ) { impl.Add( item );

}

//Converter<TInput, TOutput> is a new delegate type introduced

//in the .NET Framework that can be wired up to a method that

//knows how to convert the TInput type into a TOutput type. public void Add<R>( MyContainer<R> otherContainer,

Converter<R, T> converter ) { foreach( R item in otherContainer ) {

impl.Add( converter(item) );

}

}

public IEnumerator<T> GetEnumerator() { foreach( T item in impl ) {

yield return item;

}

}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator();

}

private List<T> impl = new List<T>();

}

public class EntryPoint

{

static void Main() {

MyContainer<long> lContainer = new MyContainer<long>(); MyContainer<int> iContainer = new MyContainer<int>();

lContainer.Add( 1 ); lContainer.Add( 2 ); iContainer.Add( 3 ); iContainer.Add( 4 );

lContainer.Add( iContainer, EntryPoint.IntToLongConverter );

C H A P T E R 1 1 G E N E R I C S

265

foreach( long l in lContainer ) { Console.WriteLine( l );

}

}

static long IntToLongConverter( int i ) { return i;

}

}

static long IntToLongConverter( int i ) { return i;

}

}

First of all, take note of the syntax of the generic Add<T> method, and also notice that there are two overloads of Add() in MyContainer<T>. Clearly, you need to have a method to add instances of type T—thus, the need for Add( T ). However, it would be really handy to be able to add an entire range of objects from another closed type formed from MyContainer<T>, as long as the enclosed type of the source container is convertible to the enclosed type to the target. If you look at Main(), you can see the intent here. I want to place the objects contained within an instance of MyContainer<int> into an instance of MyContainer<long>. Therefore, I created a generic method, Add<R>, to allow me to accept another container that contains any arbitrary type.

This technique involves a twist, though. Logically, what I’m trying to do makes perfect type sense. I want to add a collection of ints to a collection of longs, and I know that an int is easily implicitly convertible to a long, so I should be able to do this. Although this is true, you have to take into consideration that generics are formed dynamically at run time. And at run time, there is no guarantee as to what closed type formed from MyContainer<T> the Add<R> method will see. It could be MyContainer<Apples>, and an Apple may not be implicitly convertible to a long, assuming it was passed to MyContainer<long>.Add<Apples>(). Those of you who are used to C++ templates will recognize that doing such a thing won’t work, since the compiler will let you know if you’re trying to perform an invalid conversion at compile time. However, generics don’t have this compile-time luxury, so more restrictions are in place during compile time to disallow such a thing. Therefore, you must seek out a different solution, and a good one is to provide a conversion delegate to get the job done.

The base class library provides the System.Converter<T, R> delegate specifically for this case. The syntax for this delegate may seem a bit foreign, but it’s simply a generic delegate declaration, which I cover in detail in the section “Generic Delegates.” When callers call Add<R>(), they must also provide an instance of the generic Converter<T, R> delegate pointing to a method that knows how to convert from the source type to the target type. This explains the need for the IntToLongConverter method in the previous example. The Add<R> method then uses this delegate to do the actual conversion from one type to another. In this case, the conversion is an implicit one, but it still must be externalized this way since, at compile time, the compiler must accommodate the fact that the Add<R> method can have any type thrown at it.

To facilitate enumeration of the container, I have also declared MyContainer<T> such that it implements IEnumerable<T>. This allows you to use the syntactically intuitive foreach construct. You’ll notice some syntax that may look foreign to you if you’re not familiar with C# 2005 iterators.1 However, notice how easy it is to create an enumerator for this class using the yield keyword. This is such a welcome addition to the language, since declaring and constructing objects that enumerate containers is traditionally a laborious task.

1. I covered iterators fully in Chapter 9.

266 C H A P T E R 1 1 G E N E R I C S

Generic Delegates

Quite often, generics are used in the context of container types, where a closed type’s field or internal array is based on the type argument given. Generic methods extend the capability of generic types by providing a finer granularity of generic scope. I have yet to discuss the power of generic delegates.

You’re already familiar with the venerable delegate. If you were to declare a delegate that takes two parameters—the first being a long, and the second being an object—you would declare a delegate such as the following:

public delegate void MyDelegate( long l, object o );

In the previous section, you got a preview of a generic delegate when I showed the use of the generic converter delegate. The declaration for the generic converter delegate looks like this:

public delegate TOutput Converter<TInput, TOutput>( TInput input

);

It looks just like any other delegate, except it has the telltale form of a generic with a type parameter list immediately following the name of the delegate. Just as nongeneric delegates look similar to method declarations without a body, generic delegate declarations look almost identical to generic method declarations without a body. The type parameter list follows the name of the delegate, but it precedes the parameter list of the delegate.

The generic converter uses the placeholder identifiers TInput and TOutput within its type parameter list, and those types are used elsewhere in the declaration for the delegate. In generic delegate declarations, the types in the type parameter list are in scope for the entire declaration of the delegate, including the return type as shown in the previous declaration for the generic converter delegate.

Creating an instance of the Converter<TInput, TOutput> delegate is the same as creating an instance of any other delegate. When you create an instance of the generic delegate, you may use the new operator, and you may explicitly provide the type list at compile time. Or, you may simply use the abbreviated syntax that I used in the MyContainer<T> example in the previous section, in which case the compiler deduces the type parameters. For convenience, I have reprinted the Main method of that example:

static void Main() {

MyContainer<long> lContainer = new MyContainer<long>(); MyContainer<int> iContainer = new MyContainer<int>();

lContainer.Add( 1 ); lContainer.Add( 2 ); iContainer.Add( 3 ); iContainer.Add( 4 );

lContainer.Add( iContainer, EntryPoint.IntToLongConverter );

foreach( long l in lContainer ) { Console.WriteLine( l );

}

}

Note that the second parameter to the last Add method is simply a reference to the method rather than an explicit creation of the delegate itself. This works due to the method group conversion rules defined by the C# language. When the actual delegate is created from the method, the closed type of the generic is inferred using a complex pattern-matching algorithm from the parameter types of the IntToLongConverter method itself. As a matter of fact, the call to Add<T> is devoid of

C H A P T E R 1 1 G E N E R I C S

267

any explicit type parameter list at the point of invocation. The compiler is able to do the exact same type of pattern matching to infer the closed form of the Add<T> method called, which, in this case, is Add<int>. You just as well could have written the code as follows, where every type is provided explicitly:

static void Main() {

MyContainer<long> lContainer = new MyContainer<long>(); MyContainer<int> iContainer = new MyContainer<int>();

lContainer.Add( 1 ); lContainer.Add( 2 ); iContainer.Add( 3 ); iContainer.Add( 4 );

lContainer.Add<int>( iContainer,

new Converter<int, long>( EntryPoint.IntToLongConverter) );

foreach( long l in lContainer ) { Console.WriteLine( l );

}

}

In the previous example, all types are given explicitly, and the compiler is not left with the task of inferring them at compile time. Either way, the generated IL code is the same. Most of the time, you can rely on the type inference engine of the compiler. However, depending on the complexity of your code, you may find yourself needing to throw the compiler a bone by providing an explicit type list.

Along with providing a way to externalize type conversions from a container type, as in the previous examples, generic delegates help solve a special problem that I describe in the following code:

// THIS WON'T WORK AS EXPECTED!!! using System;

using System.Collections.Generic;

public delegate void MyDelegate( int i );

public class DelegateContainer<T>

{

 

public void Add( T del ) {

 

imp.Add( del );

 

}

 

public void CallDelegates( int k ) {

 

foreach( T del in imp ) {

//

del( k );

 

}

 

}

 

private List<T> imp = new List<T>();

}

 

public class EntryPoint

{

static void Main() { DelegateContainer<MyDelegate> delegates =

new DelegateContainer<MyDelegate>();

delegates.Add( EntryPoint.PrintInt );

}

268 C H A P T E R 1 1 G E N E R I C S

static void PrintInt( int i ) { Console.WriteLine( i );

}

}

As written, the previous code will compile. However, notice the commented line within the CallDelegates method. If you uncomment this line and attempt to recompile with the Microsoft compiler, you’ll get the following error:

error CS0118: 'del' is a 'variable' but is used like a 'method'

The problem is that the compiler has no way of knowing that the type represented by the placeholder T is a delegate. Those of you who’ve been jumping ahead in this chapter may be wondering why there is no form of constraint (I cover constraints shortly) to give the compiler the hint that it is a delegate. Well, even if there were, the compiler could not possibly know how to call the delegate. The constraint would not carry the information on how many parameters the delegate accepts. Remember, unlike C++ templates, generics are dynamic, and closed types are formed at run time rather than at compile time. So, at run time, the delegate represented by del could take an arbitrary amount of parameters. I can only imagine the headache caused by trying to devise a way to push a dynamic count of parameters onto the stack before calling the delegate. For all of these reasons, it rarely makes sense to create a closed type from a generic where one of the type arguments is a delegate type, since, after all, you cannot call through to it normally.

What you can do to help in this situation is apply a generic delegate to give the compiler a bit more information about what you want to do with this delegate. For example, using a generic delegate, you can effectively say, “I would like you to use delegates that only accept two parameters and return an arbitrary type.” That’s enough information to get the compiler past the block and allow it to generate code for the generic that makes sense. After all, if you give the compiler this amount of information, it at least knows how many parameters to push onto the stack before making the call through the delegate. The following code shows how you could remedy the previous situation:

using System;

using System.Collections.Generic;

public delegate void MyDelegate<T>( T i );

public class DelegateContainer<T>

{

public void Add( MyDelegate<T> del ) { imp.Add( del );

}

public void CallDelegates( T k ) { foreach( MyDelegate<T> del in imp ) {

del( k );

}

}

private List<MyDelegate<T> > imp = new List<MyDelegate<T> >();

}

public class EntryPoint

{

static void Main() { DelegateContainer<int> delegates =

new DelegateContainer<int>();

C H A P T E R 1 1 G E N E R I C S

269

delegates.Add( EntryPoint.PrintInt ); delegates.CallDelegates( 42 );

}

static void PrintInt( int i ) { Console.WriteLine( i );

}

}

Generic Type Conversion

As I covered previously in this chapter, there is no implicit type conversion for different constructed types formed from the same generic type. The same rules that apply when determining if an object of type X is implicitly convertible to an object of type Y apply equally when determining if an object of type List<int> is convertible to an object of type List<object>. When such conversion is desired, you must create a custom implicit conversion operator just as in the case of converting objects of type X to objects of type Y when they share no inheritance relationship. Otherwise, you need to create a conversion method to go from one type to another. For example, the following code is invalid:

// INVALID CODE!!!

public void SomeMethod( List<int> theList ) { List<object> theSameList = theList; // Ooops!!!

}

If you’ve looked at the documentation of List<T>, you may have noticed a generic method named ConvertAll<TOutput>. Using this method, you can convert a generic list of type List<int> to List<object>. However, you must pass the method an instance of a generic conversion delegate as described in the previous section. That is the only way that the method can possibly know how to convert each contained instance from the source type to the destination type. Even though you may call a method to convert List<int> to List<object>, you must still provide the explicit means by which it converts an int into an object.

Those familiar with the Strategy pattern may find this a familiar notion. In essence, you can provide the ConvertAll<TOutput> method at run time with a means of doing the conversion on the contained instances that, depending on the complexity of the conversion, may be tuned for the platform that it is running on. In other words, if you were converting List<Apples> to List<Oranges>, you could provide a few different conversion methods to select from, depending on the circumstances. For example, maybe one of them is highly tuned for an environment with lots of resources, so it runs faster in those environments. On the other hand, maybe another version is optimized for minimal resource usage but is much slower. At run time, the proper conversion delegate is built to bind to the conversion method that is logical for the job at hand.

Default Value Expression

Sometimes when working with generic type definitions and generic method definitions, you need to initialize an object or value instance of a parameterized type to its default value. Recall that the default value for a reference is the same as setting it to null, whereas the default value for a value type is equivalent to setting all of its underlying bits to 0. You need an expression for generics to account for these two semantic differences, and for that task, you can use the default value expression shown in the following code example:

using System;

public class MyContainer<T>

{

270 C H A P T E R 1 1 G E N E R I C S

public MyContainer() {

// Create initial capacity. imp = new T[ 4 ];

for( int i = 0; i < imp.Length; ++i ) { imp[i] = default(T);

}

}

public bool IsNull( int i ) {

if( i < 0 || i >= imp.Length ) {

throw new ArgumentOutOfRangeException();

}

if( imp[i] == null ) { return true;

} else {

return false;

}

}

private T[] imp;

}

public class EntryPoint

{

static void Main() { MyContainer<int> intColl =

new MyContainer<int>();

MyContainer<object> objColl = new MyContainer<object>();

Console.WriteLine( intColl.IsNull(0) ); Console.WriteLine( objColl.IsNull(0) );

}

}

Pay attention to the syntax within the MyContainer<T> constructor, where each item in the array is initialized explicitly to its default value. At run time, the type of T may be a value type or a reference type, so you cannot simply set the value to null and expect it to work for value types. In fact, if you attempt to assign imp[i] to null, the compiler will give you a friendly reminder with the following error:

default_value_1.cs(8,13): error CS0403: Cannot convert null to type parameter 'T' because it could be a value type. Consider using 'default(T)' instead.

You should also use the default expression when testing a variable for null, since, after all, it could be a value type. However, in this case, the compiler cannot help you sniff out when you should do this, as you can see in the example. If you run the previous code, you get the output as follows:

False

True

This is probably not the intended result, and if you modify the code to where the IsNull method looks like the following example, you’ll get a result that is more in line with the intended result:

C H A P T E R 1 1 G E N E R I C S

271

public class MyContainer<T>

{

public MyContainer() {

// Create initial capacity. imp = new T[ 4 ];

for( int i = 0; i < imp.Length; ++i ) { imp[i] = default(T);

}

}

public bool IsNull( int i ) {

if( i < 0 || i >= imp.Length ) {

throw new ArgumentOutOfRangeException();

}

if( Object.Equals(imp[i], default(T)) ) { return true;

} else {

return false;

}

}

private T[] imp;

}

Nullable Types

Related to the previous discussion is the concept of null values and what semantic meaning they carry. The null state for reference types is easily representable. If the value of the reference is set to null, it typically means that the variable has no value. This is much different, semantically, than saying that the value is 0. Semantically, a variable set to null has no value, not even the value of 0. With respect to value types, it has traditionally been much more cumbersome to represent the semantic meaning of null. If you set the value to 0, that could mean that the value is null. Then what do you do to represent the case of when the value is actually 0 but not null? Many techniques involve maintaining another Boolean value to indicate that the value type actually conveys meaning, such as a bool value named isNull.

To help you avoid having to manage such a mundane, error-prone mechanism, the .NET base class library provides you with the System.Nullable<T> type, as demonstrated in the following code:

using System;

public class Employee

{

public Employee( string firstName, string lastName ) {

this.firstName = firstName; this.lastName = lastName;

this.terminationDate = null; this.ssn = default(Nullable<long>);

}

public string firstName; public string lastName;

272 C H A P T E R 1 1 G E N E R I C S

public Nullable<DateTime> terminationDate;

public long? ssn;

// Shorthand notation

}

 

public class EntryPoint

{

static void Main() {

Employee emp = new Employee( "Vasya", "Pupkin" );

emp.ssn = 1234567890;

Console.WriteLine( "{0} {1}", emp.firstName, emp.lastName );

if( emp.terminationDate.HasValue ) { Console.WriteLine( "Start Date: {0}",

emp.terminationDate );

}

long tempSSN = emp.ssn ?? -1; Console.WriteLine( "SSN: {0}", tempSSN );

}

}

The previous code demonstrates two ways to declare a nullable type. The first nullable field within type Employee is the terminationDate field, which is declared using the System.Nullable<DateTime> type. One of the properties of Nullable<T> is HasValue, which returns true when the nullable value is non-null, and false otherwise. The second nullable value within Employee is the ssn field; however, this time I chose to use a C# shorthand notation for nullable types, where you simply follow the field’s type declaration with a question mark. Internally, the compiler does the exact same thing as the declaration for the terminationDate field.

Tip Personally, I feel that even though using Nullable<T> explicitly requires more typing, it’s definitely a lot harder to overlook than the little question mark at the end of the field type when you’re reading code. Always prefer to write clearly readable code rather than trite, cute code.

One last thing to consider when using nullable types is how you assign to and from nullable types. In the constructor for Employee, you can see that I assign null to the nullable types at first. The compiler uses an implicit conversion for the null value to do the right thing. In fact, when

I assign the ssn field in the constructor, I use the default() expression syntax, which is the same thing the compiler does when I assign the terminationDate nullable value to null. Finally, you must consider what it means to assign a nullable type to a non-nullable type. For example, in the Main method, I want to assign tempSSN based upon the value of emp.ssn. However, since emp.ssn is nullable, what should tempSSN be assigned to if emp.ssn happens to have no value? This is when you must use the null coalescing operator ??. This operator allows you to designate what you want the non-nullable value to be set to in the event that the nullable value you’re assigning from has no value. So, in the previous example, I’m saying, “Set the value of tempSSN to emp.ssn, and if emp.ssn has no value, set tempSSN to -1 instead.” Armed with these tools, it’s a snap to represent values within a system that may be semantically null, which is handy when you’re using values to represent fields within a database field that is nullable.

C H A P T E R 1 1 G E N E R I C S

273

Constructed Types Control Accessibility

When you build constructed types from generic types, you must consider the accessibility of both the generic type and the types provided as the type arguments, in order to determine the accessibility of the whole constructed type. For example, the following code is invalid and will not compile:

public class Outer

{

private class Nested

{

}

public class GenericNested<T>

{

}

private GenericNested<Nested> field1;

public GenericNested<Nested> field2; // Ooops!

}

The problem is in regards to field2. The Nested type is private, so how can GenericNested<Nested> possibly be public? Of course, the answer is, it cannot. With constructed types, the accessibility is an intersection of the accessibility of the generic type and the types provided in the argument list.

Generics and Inheritance

C# generic types cannot directly derive from a type parameter. However, you can use the following type parameters to construct the base types they do derive from:

//This is invalid!! public class MyClass<T> : T

{

}

//But this is valid.

public class MyClass<T> : Stack<T>

{

}

With C++ templates, deriving directly from a type parameter provides a special flexibility. If you’ve ever used the Active Template Library (ATL) to do COM development, you have no doubt come across this technique, since ATL employs it extensively to avoid the need for virtual method calls. The same technique is used with C++ templates to generate entire hierarchies at compile time. For more examples, I suggest you read Andrei Alexandrescu’s Modern C++ Design: Generic Programming and Design Patterns Applied (Boston, MA: Addison-Wesley Professional, 2001). This is yet another example showing how C++ templates are static in nature, whereas C# generics are dynamic.

Let’s examine techniques that you can use to emulate the same behavior to some degree. As is many times the case, you can add one more level of indirection to achieve something similar. Many times, in C++, when a template type derives directly from one of the type arguments, it is assumed that the type specified for the type argument exhibits a certain desired behavior. For example, you can do the following using C++ templates:

class Employee

{

public:

long get_salary() { return salary;

Соседние файлы в папке c#