Effective C#之Item 28: Avoid Conversion Operators

来源:互联网 发布:小米note顶配网络制式 编辑:程序博客网 时间:2024/04/29 04:35
 

Item 28:Avoid Conversion Operators

避免转换操作符

Conversionoperators introduce a kind of substitutability between classes.Substitutability means that one class can be substituted for another. This canbe a benefit: An object of a derived class can be substituted for an object ofits base class, as in the classic example of the shape hierarchy. You create aShape base class and derive a variety of customizations: Rectangle, Ellipse,Circle, and so on. You can substitute a Circle anywhere a Shape is expected. That'susing polymorphism for substitutability. It works because a circle is aspecific type of shape. When you create a class, certain conversions areallowed automatically. Any object can be substituted for an instance ofSystem.Object, the root of the .NET class hierarchy. In the same fashion, anyobject of a class that you create will be substituted implicitly for aninterface that it implements, any of its base interfaces, or any of its baseclasses. The language also supports a variety of numeric conversions.

转换操作符在类之间引入了一种可替换性。可替换性意味着一个类的实例可以由另一个类的实例替换。这有一个好处:派生类的对象可以被替换为基类的对象,就像经典的shape层次的例子一样。创建Shape基类,派生出一系列的变种:RectangleEllipseCircle等等。你可以在任何期望Shape的地方使用Circle来替换。这是为替换使用了多态。因为一个圆是形状的一种,所以这行得通。当你创建一个类的时候,一些转换是自动被允许的。任何对象都可以替换System.Object(.Net类层次的根)的实例。同样,任何你创建的类的对象都可以隐式的替换下面的东西:它实现的接口、它的基类的任何接口、它的任何基类。C#语言也支持一系列的数值转换。

When youdefine a conversion operator for your type, you tell the compiler that yourtype may be substituted for the target type. These substitutions often resultin subtle errors because your type probably isn't a perfect substitute for thetarget type. Side effects that modify the state of the target type won't havethe same effect on your type. Worse, if your conversion operator returns atemporary object, the side effects will modify the temporary object and be lostforever to the garbage collector. Finally, the rules for invoking conversionoperators are based on the compile-time type of an object, not the runtime typeof an object. Users of your type might need to perform multiple casts to invokethe conversion operators, a practice that leads to unmaintainable code.

当你为自己的类型定义一个转换的时候,就是告诉编译器,你的类型可以替换一个目标类型。因为你的类型可能不是目标类型的完美替代品,所以经常导致细微的错误。改变了目标类型的副作用不会修改你的类型。更糟糕的是,如果你的转换操作符返回一个临时对象,副作用将会修改这个临时对象,将会永远遗失在垃圾收集器里。最后,调用转换操作符的规则,是基于一个对象的编译时类型的,而不是它的运行时类型。你的类型的用户可能需要执行多步转换来调用转换操作符,从经验上讲,这会导致不可维护的代码。

If youwant to convert another type into your type, use a constructor. This moreclearly reflects the action of creating a new object. Conversion operators canintroduce hard-to-find problems in your code. Suppose that you inherit the codefor a library shown in Figure 3.1. Both the Circle class and the Ellipse classare derived from the Shape class. You decide to leave that hierarchy in placebecause you believe that, although the Circle and Ellipse are related, youdon't want to have nonabstract leaf classes in your hierarchy, and severalimplementation problems occur when you try to derive the Circle class from theEllipse class. However, you realize that every circle could be an ellipse. Inaddition, some ellipses could be substituted for circles.

如果你希望将另外的类型转换成你的类型,使用构造器。这可以很明显的反应创建新对象的动作。在你的代码里面,转换操作符可以引发难以发现的错误。假设你继承了在图3.1里面展示的库的代码。CircleEllipse类都是由Shape类派生来的。因为你相信,虽然CircleEllipse是有关系的,但是在你的类继层次内,你不想要非抽象的叶子类,所以你决定使用这个层次。当你尝试从Ellipse类派生Circle类的时候,会发生一些实现上的问题。然而,你意识到每个圆都可以是椭圆。另外,一些椭圆可以用圆来代替。


Thatleads you to add two conversion operators. Every Circle is an Ellipse, so youadd an implicit conversion to create a new Ellipse from a Circle. An implicitconversion operator will be called whenever one type needs to be converted toanother type. By contrast, an explicit conversion will be called only when theprogrammer puts a cast operator in the source code.

这导致你添加2个转换操作符。每个Circle都是一个Ellipse,因此你添加一个隐式的转换来从一个Circle创建一个新的Ellipse。当一个类型需要转换成另外一个类型的时候,隐式的转换操作符会被调用。相比较来说,只有当程序员在代码里面添加强制转换操作符的时候,显式的转换才会被调用。

  1.     public class Circle : Shape
  2.     {
  3.         private PointF center;
  4.         private float radius;
  5.  
  6.         public Circle():this(PointF.Empty, 0)
  7.         {
  8.         }
  9.  
  10.         public Circle(PointF c, float r)
  11.         {
  12.             center = c;
  13.             radius = r;
  14.         }
  15.  
  16.         public override void Draw()
  17.         {
  18.             //...
  19.         }
  20.  
  21.         static public implicit operator Ellipse(Circle c)
  22.         {
  23.             return new Ellipse(c.center, c.center,c.radius, c.radius);
  24.         }
  25.   }

Now thatyou've got the implicit conversion operator, you can use a Circle anywhere anEllipse is expected. Furthermore, the conversion happens automatically:

既然你已经有了隐式转换操作符,就可以在任何期望Ellipse的地方使用Circle。进一步说,该转换会自动发生。

  1.     public double ComputeArea( Ellipse e )
  2.     {
  3.     // return the area of the ellipse.
  4.    }
  5.     // call it:
  6.     Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f );
  7.    ComputeArea(c);

Thissample shows what I mean by substitutability: A circle has been substituted foran ellipse. The ComputeArea function works even with the substitution. You gotlucky. But examine this function:

该例子展示了我说的替换的意思:一个圆替换了一个椭圆。ComputeArea方法甚至可以工作在替换品上。你走运了,但是看看这个方法:

  1.     public void Flatten(Ellipse e)
  2.     {
  3.         e.R1 /= 2;
  4.         e.R2 *= 2;
  5.     }
  6.     // call it using a circle:
  7.     Circle c = new Circle( new PointF ( 3.0f, 0 ), 5.0f );
  8.     Flatten( c );

Thiswon't work. The Flatten() method takes an ellipse as an argument. The compilermust somehow convert a circle to an ellipse. You've created an implicitconversion that does exactly that. Your conversion gets called, and theFlatten() function receives as its parameter the ellipse created by yourimplicit conversion. This temporary object is modified by the Flatten()function and immediately becomes garbage. The side effects expected from yourFlatten() function occur, but only on a temporary object. The end result isthat nothing happens to the circle, c.

这行不通。Flatten()采用椭圆做参数,编译器必须在一定程度上将一个圆转换成椭圆。你已经创建了一个隐式转换来精确的做那件事。你的转换被调用,Flatten()方法接收你的隐式转换创建的椭圆作为参数。这个临时对象由Flatten()方法修改,迅速变成垃圾。Flatten()带来的副作用发生了,但是仅仅发生在临时对象上。最终结果就是对于原来的那个圆,c,什么也没有发生。

Changingthe conversion from implicit to explicit only forces users to add a cast to thecall:

将转换从隐式改成显式,仅仅强制用户使用添加强制转换符来调用它。

  1.     Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
  2.     Flatten((Ellipse)c);

Theoriginal problem remains. You just forced your users to add a cast to cause theproblem. You still create a temporary object, flatten the temporary object, andthrow it away. The circle, c, is not modified at all. Instead, if you create aconstructor to convert the Circle to an Ellipse, the actions are clearer:

原来的问题依然存在。你仅仅是强制你的用户添加强制转换符,同样会引起这个问题。你仍然创建了一个临时对象,对这个临时对象进行flatten操作,然后丢弃它。原来的圆,c,根本就没有被修改。相反,如果你创建一个构造函数来将Circle转换成Ellipse,所做的一切就明显了。

  1.     Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
  2.     Flatten(new Ellipse(c));

Mostprogrammers would see the previous two lines and immediately realize that anymodifications to the ellipse passed to Flatten() are lost. They would fix theproblem by keeping track of the new object:

多数程序员将看到前面的2行,迅速的意识到,任何对椭圆的修改,在传递给Flatten()的时候都丢失了。他们会通过跟踪新对象来修正这个问题。

  1.     Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
  2.     // Work with the circle.
  3.     // ...
  4.  
  5.     // Convert to an ellipse.
  6.     Ellipse e = new Ellipse(c);
  7.     Flatten(e);

Thevariable e holds the flattened ellipse. By replacing the conversion operatorwith a constructor, you have not lost any functionality; you've merely made itclearer when new objects are created. (Veteran C++ programmers should note thatC# does not call constructors for implicit or explicit conversions. You createnew objects only when you explicitly use the new operator, and at no othertime. There is no need for the explicit keyword on constructors in C#.)

变量e保存变平的椭圆。通过使用构造函数替换转换操作,不但没有漏掉任何功能,而且在创建新对象的时候更清晰。(C++老手会注意到C#不会为了显式或者隐式转换调用构造函数。只有当你显式的使用new操作符而不是其他时候,才创建新对象。在C#语言里,对于构造函数没有必要存在explicit关键字。)

Conversionoperators that return fields inside your objects will not exhibit thisbehavior. They have other problems. You've poked a serious hole in theencapsulation of your class. By casting your type to some other object, clientsof your class can access an internal variable. That's best avoided for all thereasons discussed in Item 23.

返回你的对象内部字段的转换操作符,将不会展示这个行为,它们有其他的问题,在类的封装性上制造了一个严重的漏洞。通过将你的类型强制转换成其它一些对象,你的类的客户可以访问内部变量。出于在Item23讨论过的原因,这最好要避免。

Conversionoperators introduce a form of substitutability that causes problems in yourcode. You're indicating that, in all cases, users can reasonably expect thatanother class can be used in place of the one you created. When thissubstituted object is accessed, you cause clients to work with temporaryobjects or internal fields in place of the class you created. You then modifytemporary objects and discard the results. These subtle bugs are hard to findbecause the compiler generates code to convert these objects. Avoid conversionoperators.

转换操作符引入了替换性,但是在代码里会引发问题。你在表明:可以使用一个类来替换你创建的另一个类,在所有这些情况下,你需要替代性。当被替换的对象可以被访问时,你会使得客户在临时对象或者内部字段上工作,而不是你创建的类对象本身。你修改临时对象并丢弃结果,这些微小的bug很难被发现,因为编译器会生成代码来对这些对象进行转换。因此,避免转换操作符。