现在的位置: 首页 > 综合 > 正文

Effective C# 原则28:避免转换操作(译)

2011年08月17日 ⁄ 综合 ⁄ 共 8691字 ⁄ 字号 评论关闭

Effective C# 原则28:避免转换操作

Item 28: Avoid Conversion Operators

转换操作是一种等代类型(Substitutability)间操作转换操作。等代类型就是指一个类可以取代另一个类。这可能是件好事:一个派生类的对象可以被它基类的一个对象取代,一个经典的例子就是形状继承。先有一个形状类,然后派生出很多其它的类型:长方形,椭圆形,圆形以及其它。你可以在任何地方用图形状来取代圆形,这就是多态的等代类型。这是正确的,因为圆形就是一个特殊的形状。当你创建一个类时,明确的类型转化是可以自动完成的。正如.Net中类的继承,因为System.Object是所有类型的基类,所以任何类型都可以用System.Obejct来取代。同样的情况,你所创建的任何类型,也应该可以用它所实现的接口来取代,或者用它的基类接口来取代,或者就用基类来取代。不仅如此,C#语言还支持很多其它的转换。

当你为某个类型添加转换操作时,就等于是告诉编译器:你的类型可以被目标类所取代。这可能会引发一些潜在的错误,因为你的类型很可能并不能被目标类型所取代(译注:这里并不是指继承关系上的类型转换,而是C#语言许可我们的另一种转换,请看后文)。它所的副作用就是修改了目标类型的状态后可能对原类型根本无效。更糟糕的是,如果你的转换产生了临时对象,那么副作用就是你直接修改了临时对象,而且它会永久丢失在垃圾回收器。总之,使用转换操作应该基于编译时的类型对象,而不是运行时的类型对象。用户可能须要对类型进行多样化的强制转换操作,这样的实际操作可能产生不维护的代码。

你可以使用转换操作把一个未知类型转化为你的类型,这会更加清楚的表现创建新对象的操作(译注:这样的转换是要创建新对象的)。转换操作会在代码中产生难于发现的问题。假设有这样一种情况,你创建了如图3.1那样的类库结构。椭圆和圆都是从形状类继承下来的,尽管你相信椭圆和圆是相关的,但还是决定保留这样的继承关系。这是因为你不想在继承关系中使用非抽象叶子类,这会在从椭圆类上继承圆类时,有一些不好实现的难题存在。然而,你又意识到每一个圆形应该是一个椭圆,另外某些椭圆也可能是圆形。
(图3.1)

(译注:这一原则中作者所给出的例子不是很恰当,而且作者也在前面假设了原因,因此请读者不要对这个例子太钻牛角尖,理解作者所在表达的思想就行了,相信在你的C#开发中可能也会遇到类似的转换问题,只是不太可能从圆形转椭圆。)

这将导致你要添加两个转换操作。因为每一个圆形都是一个椭圆,所以要添加隐式转换从一个圆形转换到新的椭圆。隐式转换会在一个类要求转化为另一个类时被调用。对应的,显示转化就是程序员在代码中使用了强制转换操作符。

public class Circle : Shape
{
  private PointF _center;
  private float _radius;

  public Circle() :
    this ( PointF.Empty, 0 )
  {
  }

  public Circle( PointF c, float r )
  {
    _center = c;
    _radius = r;
  }

  public override void Draw()
  {
    //...
  }

  static public implicit operator Ellipse( Circle c )
  {
    return new Ellipse( c._center, c._center,
      c._radius, c._radius );
  }
}

现在你就已经实现了隐式的转换操作,你可以在任何要求椭圆的地方使用圆形。而且这个转换是自动完成的:

public double ComputeArea( Ellipse e )
{
  // return the area of the ellipse.
}

// call it:
Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f );
ComputeArea( c );

我只是想用这个例子表达可替代类型:一个圆形已经可以代替一个可椭圆了。ComputeArea函数可以在替代类型上工作。你很幸运,但看下面这个例子:

public void Flatten( Ellipse e )
{
  e.R1 /= 2;
  e.R2 *= 2;
}

// call it using a circle:
Circle c = new Circle( new PointF ( 3.0f, 0 ), 5.0f );
Flatten( c );

这是无效的,Flatten()方法要求一个椭圆做为参数,编译器必须以某种方式把圆形转化为椭圆。确实,也已经实现了一个隐式的转换。而且你转换也被调用了,Flatten()方法得到的参数是从你的转换操作中创建的新的椭圆对象。这个临时对象被Flatten()函数修改,而且它很快成为垃圾对象。正是因为这个临时对象,Flatten()函数产生了副作用。最后的结果就是这个圆形对象,c,根本就没有发生任何改变。从隐式转换修改成显示转换也只是强迫用户调用强制转换而以:

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

原先的问题还是存在。你想让用户调用强制转换为解决这个问题,但实际上还是产生了临时对象,把临时对象进行变平(flatten)操作后就丢掉了。原来的圆,c,还是根本没有被修改过。取而代之的是,如果你创建一个构造函数把圆形转换成椭圆,那么操作就很明确了:

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

相信很多程序员一眼就看的出来,在前面的两行代码中传给Flatten()的椭圆在修改后就丢失了。他们可能会通过跟踪对象来解决这个问题:

Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f );
// Work with the circle.
// ...

// Convert to an ellipse.
Ellipse e = new Ellipse( c );
Flatten( e );

通过一个变量来保存修改(变平)后的椭圆,通过构造函数来替换转换操作,你不会丢失任何功能:你只是让创建新对象的操作更加清楚。(有经验的C++程序可能注意到C#的隐式转化和显示转换都没有调用构造函数。在C++中,只有明确的使用new操作符才能创建一个新的对象时,其它时候不行。而在C#的构造函数中不用明确的使用关键字。)

从类型里返回字段的转换操作并不会展示类型的行为,这会产生一些问题。你给类型的封装原则留下了几个严重的漏洞。通过把类型强制转化为其它类型,用户可以访问到类型的内部变量。这正是原则23中所讨论的所有原因中最应该避免的。

转换操作提供了一种类型可替代的形式,但这会给代码引发一些问题。你应该已经明白所有这些内容:用户希望可以合理的用某种类型来替代你的类型。当这个可替代类型被访问时,你就让用户在临时对象上工作,或者内部字段取代了你创建的类。随后你可能修改了临时对象,然后丢掉。因为这些转换代码是编译器产生的,因此这些潜在的BUG很难发现。应该尽量避免转换操作。

==========================
   

Item 28: Avoid Conversion Operators
Conversion operators introduce a kind of substitutability between classes. Substitutability means that one class can be substituted for another. This can be a benefit: An object of a derived class can be substituted for an object of its base class, as in the classic example of the shape hierarchy. You create a Shape 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's using polymorphism for substitutability. It works because a circle is a specific type of shape. When you create a class, certain conversions are allowed automatically. Any object can be substituted for an instance of System.Object, the root of the .NET class hierarchy. In the same fashion, any object of a class that you create will be substituted implicitly for an interface that it implements, any of its base interfaces, or any of its base classes. The language also supports a variety of numeric conversions.

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

If you want to convert another type into your type, use a constructor. This more clearly reflects the action of creating a new object. Conversion operators can introduce hard-to-find problems in your code. Suppose that you inherit the code for a library shown in Figure 3.1. Both the Circle class and the Ellipse class are derived from the Shape class. You decide to leave that hierarchy in place because you believe that, although the Circle and Ellipse are related, you don't want to have nonabstract leaf classes in your hierarchy, and several implementation problems occur when you try to derive the Circle class from the Ellipse class. However, you realize that every circle could be an ellipse. In addition, some ellipses could be substituted for circles.

Figure 3.1. Basic shape hierarchy.

 

 

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

public class Circle : Shape
{
  private PointF _center;
  private float _radius;

  public Circle() :
    this ( PointF.Empty, 0 )
  {
  }

  public Circle( PointF c, float r )
  {
    _center = c;
    _radius = r;
  }

  public override void Draw()
  {
    //...
  }

  static public implicit operator Ellipse( Circle c )
  {
    return new Ellipse( c._center, c._center,
      c._radius, c._radius );
  }
}

 

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

public double ComputeArea( Ellipse e )
{
  // return the area of the ellipse.
}

// call it:
Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f );
ComputeArea( c );

 

This sample shows what I mean by substitutability: A circle has been substituted for an ellipse. The ComputeArea function works even with the substitution. You got lucky. But examine this function:

public void Flatten( Ellipse e )
{
  e.R1 /= 2;
  e.R2 *= 2;
}

// call it using a circle:
Circle c = new Circle( new PointF ( 3.0f, 0 ), 5.0f );
Flatten( c );

 

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

Changing the conversion from implicit to explicit only forces users to add a cast to the call:

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

 

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

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

 

Most programmers would see the previous two lines and immediately realize that any modifications to the ellipse passed to Flatten() are lost. They would fix the problem by keeping track of the new object:

Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f );
// Work with the circle.
// ...

// Convert to an ellipse.
Ellipse e = new Ellipse( c );
Flatten( e );

 

The variable e holds the flattened ellipse. By replacing the conversion operator with a constructor, you have not lost any functionality; you've merely made it clearer when new objects are created. (Veteran C++ programmers should note that C# does not call constructors for implicit or explicit conversions. You create new objects only when you explicitly use the new operator, and at no other time. There is no need for the explicit keyword on constructors in C#.)

Conversion operators that return fields inside your objects will not exhibit this behavior. They have other problems. You've poked a serious hole in the encapsulation of your class. By casting your type to some other object, clients of your class can access an internal variable. That's best avoided for all the reasons discussed in Item 23.

Conversion operators introduce a form of substitutability that causes problems in your code. You're indicating that, in all cases, users can reasonably expect that another class can be used in place of the one you created. When this substituted object is accessed, you cause clients to work with temporary objects or internal fields in place of the class you created. You then modify temporary objects and discard the results. These subtle bugs are hard to find because the compiler generates code to convert these objects. Avoid conversion operators.

抱歉!评论已关闭.