More Effective C# Item 7. Do Not Create Generic Specialization on Base Classes or Interfaces

来源:互联网 发布:文章得失寸心知 编辑:程序博客网 时间:2024/05/22 10:27

 

Item 7. Do Not Create Generic Specialization on Base Classes or Interfaces

Introducing generic methods can make it highly complicated for the compiler to resolve method overloads. Each generic method can match any possible substitute for each type parameter. Depending on how careful you are (or aren't), your application will behave very strangely. When you create generic classes or methods, you are responsible for creating a set of methods that will enable developers using that class to safely use your code with minimal confusion. This means that you must pay careful attention to overload resolution, and you must determine when generic methods will create better matches than the methods developers might reasonably expect.

泛型是不错的技术,但是如果我们不够仔细,很容易发生误用,尤其是在重载函数的时候。下面是个例子。

Examine this code, and try to guess the output:

public class MyBase{}public interface IMessageWriter{    void WriteMessage();}public class MyDerived : MyBase, IMessageWriter{    #region IMessageWriter Members    void IMessageWriter.WriteMessage()    {        Console.WriteLine("Inside MyDerived.WriteMessage");    }    #endregion}public class AnotherType : IMessageWriter{    #region IMessageWriter Members    public void WriteMessage()    {        Console.WriteLine("Inside AnotherType.WriteMessage");    }    #endregion}class Program{    static void WriteMessage(MyBase b)    {        Console.WriteLine("Inside WriteMessage(MyBase)");    }    static void WriteMessage<T>(T obj)    {        Console.Write("Inside WriteMessage<T>(T):  ");        Console.WriteLine(obj.ToString());    }    static void WriteMessage(IMessageWriter obj)    {        Console.Write(            "Inside WriteMessage(IMessageWriter):  ");        obj.WriteMessage();    }    static void Main(string[] args)    {        MyDerived d = new MyDerived();        Console.WriteLine("Calling Program.WriteMessage");        WriteMessage(d);        Console.WriteLine();        Console.WriteLine(            "Calling through IMessageWriter interface");        WriteMessage((IMessageWriter)d);        Console.WriteLine();        Console.WriteLine("Cast to base object");        WriteMessage((MyBase)d);        Console.WriteLine();        Console.WriteLine("Another Type test:");        AnotherType anObject = new AnotherType();        WriteMessage(anObject);        Console.WriteLine();        Console.WriteLine("Cast to IMessageWriter:");        WriteMessage((IMessageWriter)anObject);    }}

这个例子里面有几个重载:利用了泛型参数的重载,利用接口参数的重载,利用基类参数的重载。

Some of the comments might make it a giveaway, but make your best guess before looking at the output. It's important to understand how the existence of generic methods affects the method resolution rules. Generics are almost always a good match, and they wreak havoc with our assumptions about which methods get called. Here's the output:

我承认,我猜到了结局,却没猜到开头。这里需要注意的是泛型方法对编译器做方法调用推断的影响。也就是说,在泛型方法存在的时候,编译器是如何来确定调用哪个方法。这里也印证了一句话,灵活性带来了复杂性,这里的复杂性不一定指的是代码结构的复杂,而是我们理解调用关系的复杂。

Calling Program.WriteMessageInside WriteMessage<T>(T):  Item14.MyDerivedCalling through IMessageWriter interfaceInside WriteMessage(IMessageWriter):    Inside MyDerived.WriteMessageCast to base objectInside WriteMessage(MyBase)Another Type test:Inside WriteMessage<T>(T):  Item14.AnotherTypeCast to IMessageWriter:Inside WriteMessage(IMessageWriter):    Inside AnotherType.WriteMessage

 

The first test shows one of the more important concepts to remember: WriteMessage<T>(T obj) is a better match than WriteMessage(MyBase b) for an object that is derived from MyBase. That's because the compiler can make an exact match by substituting MyDerived for T in that message, and WriteMessage(MyBase) requires an implicit conversion. The generic method is better. This concept will become even more important when you see the extension methods defined in the Queryable and Enumerable classes added in C# 3.0. Generic methods are always perfect matches, so they win over base class methods.

简单来说,如果有两个重载方法,其参数类型分别是基类泛型,那么当传入的参数是子类对象时,编译器会选择调用泛型的重载方法。可以从编译器解析方法调用的思路去理解,就是那个方法更容易被匹配,他就调用哪个方法。如果不知道这点而推断代码行为,就有可能发生错误。

 

The next two tests show how you can control this behavior by explicitly invoking the conversion (either to MyBase or to an IMessageWriter type). And the last two tests show that the same type of behavior is present for interface implementations even without class inheritance.

针对泛型,基类,接口的重载,编译器优先match泛型方式的重载。如果我们不想按照编译器的方式来选择,我们需要显式类型转换。

 

Name resolution rules are interesting, and you can show off your arcane knowledge about them at geek cocktail parties. But what you really need is a strategy to create code that ensures that your concept of "best match" agrees with the compiler's concept. After all, the compiler always wins this battle.

根据编译器的规则来制定best match.

 

It's not a good idea to create generic specializations for base classes when you intend to support the class and all its descendents. It's equally error prone to create generic specializations for interfaces.

由此可见,对基类和接口创建generic specializations是不好的做法。原因是由于继承关系的存在,对泛型方法的调用容易混淆。

 

But numeric types do not present those pitfalls. There is no inheritance chain between integral and floating-point numeric types. As Item 2 explains, often there are good reasons to provide specific versions of a method for different value types. Specifically, the .NET Framework includes specialization on all numeric types for Enumerable.Max<T>, Enumerable.Min<T>, and similar methods.

针对值类型没有这样的问题,因为值类型没有继承关系。

 

But it's best to use the compiler instead of adding runtime checks to determine the type. That's what you're trying to avoid by using generics in the first place, right?

当然,我们也可以用运行时的类型检查来明确这种容易混淆的逻辑。下面是个例子。

// Not the best solution// this uses runtime type checkingstatic void WriteMessage<T>(T obj){    if (obj is MyBase)        WriteMessage(obj as MyBase);    else if (obj is IMessageWriter)        WriteMessage((IMessageWriter)obj);    else    {        Console.Write("Inside WriteMessage<T>(T):  ");        Console.WriteLine(obj.ToString());    }}

 

This code might be fine, but only if there are only a few conditions to check. It does hide all the ugly behavior from your customers, but notice that it introduces some runtime overhead. Your generic method is now checking specific types to determine whether they are (in your mind) a better match than the one the compiler would choose if left to its own devices.

很显然,这段代码可以根据参数类型,来决定不同的逻辑。的确符合我们的使用预期。不足之处就是在运行时做类型检查是一种额外的开销。如果有可能,我们尽量让编译器来做类型判断。(因此我们才需要知道编译器的判断规则)一个负责人的程序员,总是要考虑代码的性能问题的。可读性,复杂度,性能,都需要权衡。

 

Use this technique only when it's clear that a better match is quite a bit better, and measure the performance to see whether there are better ways to write your library to avoid the problem altogether.

总之,尽量不要使用运行时检查,优先使用编译器做类型判断。

 

Of course, this is not to say that you should never create more-specific methods for a given implementation. Item 3 shows how to create a better implementation when advanced capabilities are available. The code in Item 3 creates a reverse iterator that adapts itself correctly when advanced capabilities are created. Notice that the Item 3 code does not rely on generic types for any name resolution. Each constructor expresses the various capabilities correctly to ensure that the proper method can be called at each location.

就是事无绝对,狡猾的作者:)

 

However, if you want to create a specific instantiation of a generic method for a given type, you need to create that instantiation for that type and all its descendents. If you want to create a generic specialization for an interface, you need to create a version for all types that implement that interface.

如果我们想为一个已知类型--并且这个类型是基类或者接口--创建针对于该类型的泛型方法时,我们需要创建两个版本,一个就是针对这个基类或者接口本身的泛型方法,另一个就是针对子类或者该接口的实现类的泛型方法。

 

原创粉丝点击