Effective C# 原則28:避免轉換操作(譯)

來源:互聯網
上載者:User

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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.