轉換操作是一種等代類型(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很難發現。應該盡量避免轉換操 作。
返回教程目錄