Effective C# 避免強制轉換操作符

來源:互聯網
上載者:User

轉換操作符為類之間引入了一層“替換性(substitutability)”。“替換”意味著一個類的執行個體可以被替換為另一個類的執行個體。這對我們來說可以是一種好處:一個衍生類別的對象可以被當做一個基類對象來使用。

例如在經典的Shape類層次中,我們可以建立一個Shape(形狀)基類,並派生出許多子類:Rectangle(長方形)、Ellipse(橢圓)、Circle(圓)等。在任何需要Shape的地方,我們都可以使用一個Circle子類來替換。替換得以實現是因為多態發揮的作用,因為Circle是一個更為具體的Shape類型。當我們建立一個類時,某些轉換會自動奏效。例如,任何對象都可以被當作System.Object執行個體來使用,因為System.Object是整個.NET類層次中的根類。類似地,任何類的對象都可以被隱式地當作它所實現的一個介面或者它的基類來使用。另外,C#語言還支援許多數值轉換。

在為我們的類型定義了轉換操作符之後,我們實際上是在告訴編譯器這些類型可以被當作目標類型來使用。這樣的替換經常會導致一些很詭異的bug,因為我們的類型可能並不是目標類型的完美替換品。例如,更改目標類型狀態的結果可能並不會反應到我們的類型上。更糟糕的是,如果我們的轉換操作符返回一個臨時對象,更改的效應將僅限於臨時對象,而且隨後就會被丟棄變成垃圾對象。最後,轉換操作符的調用規則是基於對象的編譯時間類型,而非運行時類型。為此,類型的使用者可能要執行多次強制轉型來調用轉換操作符,這樣的做法會導致難以維護的代碼。

如果希望將一個類型轉換為另一個類型,我們應該使用構造器。這種做法更清楚地反映了建立新對象的行為。而轉換操作符會為代碼引入很難發現的問題。假設我們獲得了一個3-1所示的類庫的代碼。其中 Circle類和Ellipse類都派生自Shape類。我們決定保留這個類層次不動,因為雖然Circle和Ellipse有相關性,但是我們並不希望在類層次中出現非抽象的葉子類;並且當試圖讓Circle類派生自Ellipse類時,會遇到一些實現方面的問題。然而,我們總還是會認為每一個 Circle都可以是一個Ellipse。而且,某些Ellipse也可以被當作Circle來使用。

 

這會導致我們添加兩個轉換操作符。由於每一個 Circle都是一個Ellipse,因此我們需要添加隱式轉換操作符將一個Circle轉換為一個Ellipse。當一個類型需要被轉換成另一個類型才能正常工作時,隱式轉換操作符就會被調用。與此相反,當我們在原始碼中使用強制轉型操作符時,顯式轉換操作符便會被調用。看下面的代碼:

 

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 );

}

}

 

有了隱式轉換操作符之後,我們就可以在任何需要Ellipse的地方使用Circle對象。而且,這樣的轉換會自動發生:

public double ComputeArea( Ellipse e )

{

// 返回橢圓的面積。

}

// 調用:

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

ComputeArea( c );

 

上面的例子展示了所謂的“替換”:在本來要求Ellipse的地方使用了Circle。替換後,ComputeArea()函數工作得很好,這很幸運。但是,對於如下的函數:

 

public void Flatten( Ellipse e )

{

e.R1 /= 2;

e.R2 *= 2;

}

// 使用Circle來調用:

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

Flatten( c );

 

就有問題了。Flatten()方法接受一個Ellipse作為參數。編譯器必須將Circle轉換為Ellipse。這便是我們上面建立的隱式轉換操作符的工作。

隱式轉換操作符調用後會建立一個臨時的 Ellipse對象,然後傳遞給Flatten()函數作為參數。這個臨時的Ellipse對象會被Flatten()函數修改,然後就變成垃圾對象。 Flatten()函數只是在臨時的Ellipse對象上顯現了一些副作用。結果是真正的Circle對象c什麼都沒有發生。

將隱式轉換操作符更改為顯式轉換操作符只會強制使用者添加一個轉型動作:

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

Flatten( ( Ellipse ) c );

原來的問題並沒有解決。強制使用者添加轉型動作會導致同樣的問題——仍然會建立臨時對象,將臨時對象變平(flatten),然後將其丟棄,而Circle對象c根本沒有得到任何更改。然而,如果我們通過建立一個構造器來將Circle轉換為Ellipse,那麼下面代碼的行為就很清晰了:

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

Flatten ( new Ellipse( c ));

絕大多數程式員看到上面兩行代碼,立刻就會明白Flattern()中對Ellipse的任何更改都會丟失。這樣自然就會對Ellipse對象保持一個追蹤:

 

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

// 處理Circle。

// ……

// 轉換為一個Ellipse。

Ellipse e = new Ellipse( c );

Flatten( e );

 

變數e中儲存著變平的Ellipse。通過使用構造器來代替轉換操作符,我們不但沒有丟失任何功能,反而使得新對象的建立工作變得更加清晰。(對於那些C++老手來說,需要注意C#不會將構造器用做隱式或者顯式的轉換。只有在顯式使用new操作符時,C#才會建立新對象,沒有任何例外。因此C#的構造器上不需要使用explicit關鍵字。)

在轉換操作符中返回對象內部的欄位,雖然不再出現上述行為,但是它們會帶來其他問題——會給類的封裝性帶來嚴重的漏洞。因為如果將我們的類型強制轉換為其他類型,類的客戶程式就可以訪問類的內部變數。不管有什麼理由,都應該竭力避免這種做法。

綜上所述,轉換操作符所獲得的“替換性”會為代碼帶來一些問題。提供轉換操作符的意思是在向類的使用者表明,在本該使用這個類的地方使用者可以用其他的類來代替。當訪問被替換的對象時,和客戶程式打交道的實際上是一些臨時對象或者內部欄位。這些臨時對象被修改之後,結果就會被丟棄。這樣詭異的bug是很難發現的,因為進行類型轉換的代碼是由編譯器產生的。因此我們應該避免使用轉換操作符。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.