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

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

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

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

public T Magnitude { get {

// WON'T COMPILE!!!

return Math.Sqrt( real * real +

imaginary * imaginary );

}

}

private T real; private T imaginary;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 3, 4 );

Console.WriteLine( "Magnitude is {0}", c.Magnitude );

}

}

If you attempt to compile the previous code, you may be surprised to get the following compiler error:

error CS0019: Operator '*' cannot be applied to operands of type 'T' and 'T'

This is a perfect example of the problem with using operators in generic code. The compilation problem stems from the fact that you must compile generic code in a generic way to accommodate the fact that constructed types formed at run time can be formed from a value type that may not support the operator. In this case, it’s impossible for the compiler to know if the type given for T in a constructed type at some point in the future even supports the multiplication operator. What are you to do? A common technique is to externalize the operation from the Complex<T> definition itself and then require the user of the definition to provide the operation. A delegate is the perfect tool for doing this. Let’s look at an example of Complex<T> that does that:

using System;

public struct Complex<T>

where T: struct, IConvertible

{

// Delegate for doing multiplication.

public delegate T BinaryOp( T val1, T val2 );

public Complex( T real, T imaginary, BinaryOp mult, BinaryOp add,

Converter<double, T> convToT ) { this.real = real;

this.imaginary = imaginary; this.mult = mult;

this.add = add; this.convToT = convToT;

}

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

285

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

public T Magnitude { get {

double magnitude =

Math.Sqrt( Convert.ToDouble(add(mult(real, real), mult(imaginary, imaginary))) );

return convToT( magnitude );

}

}

private T real; private T imaginary; private BinaryOp mult; private BinaryOp add;

private Converter<double, T> convToT;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 3, 4,

EntryPoint.MultiplyInt64,

EntryPoint.AddInt64, EntryPoint.DoubleToInt64 );

Console.WriteLine( "Magnitude is {0}", c.Magnitude );

}

static Int64 MultiplyInt64( Int64 val1, Int64 val2 ) { return val1 * val2;

}

static Int64 AddInt64( Int64 val1, Int64 val2 ) { return val1 + val2;

}

static Int64 DoubleToInt64( double d ) { return Convert.ToInt64( d );

}

}

You’re probably looking at the previous code and wondering what went wrong and why the complexity seems so much higher when all you’re trying to do is find the magnitude of a complex number. As mentioned previously, you had to provide a delegate to handle the multiplication external to the generic type. Thus, I’ve defined the Complex<T>.Multiply delegate. At construction time,

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

the Complex<T> constructor must be passed a third parameter that references a method for the multiplication delegate to refer to. In this case, EntryPoint.MultiplyInt64 handles multiplication. So, when the Magnitude property needs to multiply the components, it must use the delegate rather than the multiplication operator. Naturally, when the delegate is called, it boils down to a call to the multiplication operator. However, the application of the operator is now effectively external to the generic type Complex<T>.

No doubt, you have noticed the extra complexities to the property accessor. First, Math.Sqrt accepts a type of System.Double. This explains the call to the Convert.ToDouble method. And to make sure things go smoothly, I added a constraint to T so that the type supplied supports IConvertible. But you’re not done yet. Math.Sqrt returns a System.Double, and you have to convert that value type back into type T. In order to do so, you cannot rely on the System.Convert class, because you don’t know what type you’re converting to at compile time. Yet again, you have to externalize an operation, which in this case is a conversion. This is precisely one reason why the Framework defines the

Converter<TInput, TOuput> delegate. In this case, Complex<T> needs a Converter<double, T> conversion delegate. At construction time, you must pass a method for this delegate to call through to, which in this case is EntryPoint.DoubleToInt64. Now, after all of this, the Complex<T>.Magnitude property works as expected, but not without an extra amount of work.

Let’s say you want instances of Complex<T> to be able to be used as key values in a SortedList<TKey, TValue> generic type. In order for that to work, Complex<T> needs to implement IComparable<T>. Let’s see what you need to do to make that a reality:

using System;

public struct Complex<T> : IComparable<Complex<T> > where T: struct, IConvertible, IComparable

{

// Delegate for doing multiplication.

public delegate T BinaryOp( T val1, T val2 );

public Complex( T real, T imaginary, BinaryOp mult, BinaryOp add,

Converter<double, T> convToT ) { this.real = real;

this.imaginary = imaginary; this.mult = mult;

this.add = add; this.convToT = convToT;

}

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

public T Magnitude { get {

double magnitude =

Math.Sqrt( Convert.ToDouble(add(mult(real, real), mult(imaginary, imaginary))) );

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

287

return convToT( magnitude );

}

}

public int CompareTo( Complex<T> other ) {

return Magnitude.CompareTo( other.Magnitude );

}

private T real; private T imaginary; private BinaryOp mult; private BinaryOp add;

private Converter<double, T> convToT;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 3, 4,

EntryPoint.MultiplyInt64,

EntryPoint.AddInt64, EntryPoint.DoubleToInt64 );

Console.WriteLine( "Magnitude is {0}", c.Magnitude );

}

static Int64 MultiplyInt64( Int64 val1, Int64 val2 ) { return val1 * val2;

}

static Int64 AddInt64( Int64 val1, Int64 val2 ) { return val1 + val2;

}

static Int64 DoubleToInt64( double d ) { return Convert.ToInt64( d );

}

}

My implementation of the IComparable<Complex<T>> interface considers two Complex<T> types equivalent if they have the same magnitude. Therefore, most of the work required to do the comparison is done already. However, instead of being able to rely upon the inequality operator of the C# language, again you need to use a mechanism that doesn’t rely upon operators. In this case, I’ve used the CompareTo method. Of course, this requires me to force another constraint on type T, and that is that it must support the nongeneric IComparable interface.

One thing worth noting is that the previous constraint on the nongeneric IComparable interface makes it a little bit difficult for Complex<T> to contain generic structs, because generic structs might implement IComparable<T> instead. In fact, given the current definition, it is impossible to define a type of Complex<Complex<int>>. It would be nice if Complex<T> could be constructed from types that may implement either IComparable<T> or IComparable or even both. Let’s see how you can do this:

using System;

using System.Collections.Generic;

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

public struct Complex<T> : IComparable<Complex<T> > where T: struct

{

// Delegate for doing multiplication.

public delegate T BinaryOp( T val1, T val2 );

public Complex( T real, T imaginary, BinaryOp mult, BinaryOp add,

Converter<double, T> convToT ) { this.real = real;

this.imaginary = imaginary; this.mult = mult;

this.add = add; this.convToT = convToT;

}

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

public T Magnitude { get {

double magnitude =

Math.Sqrt( Convert.ToDouble(add(mult(real, real), mult(imaginary, imaginary))) );

return convToT( magnitude );

}

}

public int CompareTo( Complex<T> other ) {

return Comparer<T>.Default.Compare( this.Magnitude, other.Magnitude );

}

private T real; private T imaginary; private BinaryOp mult; private BinaryOp add;

private Converter<double, T> convToT;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 3, 4,

EntryPoint.MultiplyInt64,

EntryPoint.AddInt64, EntryPoint.DoubleToInt64 );

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

289

Console.WriteLine( "Magnitude is {0}", c.Magnitude );

}

static void DummyMethod( Complex<Complex<int> > c ) {

}

static Int64 AddInt64( Int64 val1, Int64 val2 ) { return val1 + val2;

}

static Int64 MultiplyInt64( Int64 val1, Int64 val2 ) { return val1 * val2;

}

static Int64 DoubleToInt64( double d ) { return Convert.ToInt64( d );

}

}

In this example, I had to remove the constraint on T requiring implementation of the IComparable interface. Instead, the CompareTo method relies upon the default generic comparer defined in the

System.Collections.Generic namespace.

Note The generic comparer class Comparer<T> introduces one more level of indirection in the form of a class with regards to comparing two instances. In effect, it externalizes the comparability of the instances. If you need a custom implementation of IComparer, you should derive from Comparer<T>.

Additionally, I had to remove the IConvertible constraint on T to get DummyMethod() to compile. That’s because Complex<T> doesn’t implement IConvertible and when T is replaced with Complex<T> (thus forming Complex<Complex<T>>), then T doesn’t implement IConvertible.

Note When creating generic types, try not to be too restrictive by forcing too many constraints on the contained types. For example, don’t force all the contained types to implement IConvertible. Many times, you can externalize such constraints by using a helper object coupled with a delegate.

Think about the removal of this constraint for a moment. In the Magnitude property, you rely on the Convert.ToDouble method. However, since you removed the constraint, the possibility of getting a runtime exception exists—for example, when the type represented by T doesn’t implement IConvertible. Since generics are meant to provide better type safety and help you avoid runtime exceptions, there must be a better way. In fact, there is, and you can do better by giving Complex<T> yet another converter in the form of a Convert<T, double> delegate in the constructor, as follows:

using System;

using System.Collections.Generic;

public struct Complex<T> : IComparable<Complex<T> > where T: struct

{

// Delegate for doing multiplication.

public delegate T BinaryOp( T val1, T val2 );

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

public Complex( T real, T imaginary, BinaryOp mult, BinaryOp add,

Converter<T, double> convToDouble,

Converter<double, T> convToT ) { this.real = real;

this.imaginary = imaginary; this.mult = mult;

this.add = add; this.convToDouble = convToDouble; this.convToT = convToT;

}

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

public T Magnitude { get {

double magnitude =

Math.Sqrt( convToDouble(add(mult(real, real), mult(imaginary, imaginary))) );

return convToT( magnitude );

}

}

public int CompareTo( Complex<T> other ) {

return Comparer<T>.Default.Compare( this.Magnitude, other.Magnitude );

}

private T real; private T imaginary; private BinaryOp mult; private BinaryOp add;

private Converter<T, double> convToDouble; private Converter<double, T> convToT;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 3, 4,

EntryPoint.MultiplyInt64,

EntryPoint.AddInt64,

EntryPoint.Int64ToDouble,

EntryPoint.DoubleToInt64 );

Console.WriteLine( "Magnitude is {0}", c.Magnitude );

}

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

291

static void DummyMethod( Complex<Complex<int> > c ) {

}

static Int64 MultiplyInt64( Int64 val1, Int64 val2 ) { return val1 * val2;

}

static Int64 AddInt64( Int64 val1, Int64 val2 ) { return val1 + val2;

}

static Int64 DoubleToInt64( double d ) { return Convert.ToInt64( d );

}

static double Int64ToDouble( Int64 i ) { return Convert.ToDouble( i );

}

}

Now, the Complex<T> type can contain any kind of struct, whether it’s generic or not. However, you must provide it with the necessary means to be able to convert to and from double as well as to multiply and add constituent types. This Complex<T> struct is, by no means, meant to be a reference for complex number representation at all. Rather, it is a somewhat contrived example meant to illustrate many of the concerns you must deal with in order to create effective generic types.

You’ll see some of these techniques in practice as you deal with the generic containers that exist in the Base Class Library.

Creating Constructed Types Dynamically

Given the dynamic nature of the CLR and the fact that you can actually generate classes and code at run time, it is only natural to consider the possibility of constructing closed types from generics at run time. Up until now, all of the examples in this book have dealt with creating closed types at compile time.

This functionality stems from a natural extension of the metadata specification to accommodate generics. The type System.Type is the cornerstone of functionality whenever you need to work with types dynamically within the CLR, and naturally, it has been extended to deal with generics as well. Some of the new methods on System.Type are self-explanatory by name and include

GetGenericArguments(), GetGenericParameterConstraints(), and GetGenericTypeDefinition(). These methods are helpful when you already have a System.Type instance representing a closed type. However, the method that makes things interesting is MakeGenericType(), which allows you to pass an array of System.Type objects that represent the types that are to be used in the argument parameter list for the resultant constructed type.

Those coming from a C++ template background have probably become frustrated from time to time with generics, since they lack the static compile-time capabilities of templates. However, I think you’ll agree that the dynamic capabilities of generics make up for that in the end. Imagine how handy it is to be able to create closed types from generics at run time. For example, creating a parsing engine for some sort of XML-based language that defines new types from generics is a snap. Let’s take

a look at an example of how you use the MakeGenericType method:

using System;

using System.Collections.Generic;

public class EntryPoint

{

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

static void Main() { IList<int> intList =

(IList<int>) CreateClosedType<int>( typeof(List<>) );

IList<double> doubleList = (IList<double>)

CreateClosedType<double>( typeof(List<>) );

Console.WriteLine( intList ); Console.WriteLine( doubleList );

}

static object CreateClosedType<T>( Type genericType ) { Type[] typeArguments = {

typeof( T )

};

Type closedType =

genericType.MakeGenericType( typeArguments );

return Activator.CreateInstance( closedType );

}

}

The meat of this code is inside the generic method CreateClosedType<T>. All of the work is done in general terms via references to Type created from the available metadata. First, you need to get a reference to the generic, open type List<>, which is passed in as a parameter. After that, you simply create an array of Type instances to pass to MakeGenericType() to obtain a reference to the closed type. Once that stage is complete, the only thing left to do is to call CreateInstance() on the System.Activator class. System.Activator is the facility that you must use to create instances of types that are known only at run time. In this case, I’m calling the default constructor for the closed type. However, Activator has overloads of CreateInstance() that allow you to call constructors that require parameters.

Note I used the C# typeof operator rather than the Type.GetType method to obtain the Type instance for the types. If the type is known at compile time, the typeof operator performs the metadata lookup then rather than at run time—therefore, it is more efficient.

When you run the previous example, you’ll see that the closed types get streamed to the console showing their fully qualified type names, thus proving that the closed types were created properly after all.

The ability to create closed types at run time is yet another powerful tool in your toolbox for creating highly dynamic systems. Not only can you declare generic types within your code so that you can write flexible code, but you can also create closed types from those generic definitions at run time. Take a moment to consider the amount of problems you could solve with these techniques, and it’s easy to see that generics are an extremely potent addition to C# and the CLR.

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

293

Summary

In this chapter, I’ve shown how to declare and use generics using C# 2005, including generic classes, structs, interfaces, methods, and delegates. I also discussed generic constraints, which are necessary for the compiler to create code where certain functional assumptions are placed upon the type arguments provided for the generic type arguments at run time. Collection types gain a real and measurable gain in efficiency and safety with generics.

Support for generics in .NET 2.0 and C# 2005 is a welcome addition to the language. Not only do generics allow you to generate more efficient code when using value types with containers, but they also give the compiler much more power when enforcing type safety. As a rule, you should always prefer compile-time type safety over runtime type safety. You can fix a compile-time failure before software is deployed, but a runtime failure usually results in an InvalidCastException thrown in a production environment. Such a runtime failure could cost the end user huge sums of money, depending on the situation, and it could cause large amounts of embarrassment for you as the developer. Therefore, always provide the compiler with as much power as possible to enforce type safety, so it can do what it’s meant to do best—and that’s to be your friend.

The next chapter tackles the topic of threading in C# and the .NET runtime. Along with threading comes the ever-so-important topic of synchronization.

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