# 原則9:在你的 API 中避免轉換操作
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明[出處](/blog/1983118):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
轉換操作引入不同類之間的替代性。替代性的意思是一個類可以被另外一個類代替。這有一個好處:一個子類可以代替基類,像下面 shape 繼承結構給出例子。你定義基類 Shape 并自定義它的子類: Rectangle , Ellipse , Circle 等。在期望是 Shape 的地方你可以用 Circle 代替它。這是使用多態的替代性。這個是因為 Circle 是 Shape 的一個特別類型。當你創建一個類,某些轉換是自動被允許的。任何對象都可以替代 .NET 類結構的基類 System.Object 的對象。相同的原理,你創建的任何類的對象都可以隱式代替它實現的接口,或者任何基類的接口,或者任何基類。語言還支持一系列的數值轉換。
當你定義類的轉換操作符,等于告訴編譯器你的類型可以代替目標類型。這些轉換經常會導致一些微妙錯誤因為你的類型可能不是能很完美替代目標類型。有一個副作用就是當你修改目標類型的狀態可能不會對你的類型產生相同的影響。更糟糕的是,如果你的轉換操作符返回一個臨時對象,這個臨時對象將會被垃圾回收而永久丟失。應用轉換操作符的規則是基于編譯時的類型,而不是運行時的對象的類型。使用你的類可能需要執行多次轉換操作符,這個實踐會導致代碼很難維護。
如果你想轉換任意類型到你的類型,使用構造器。這個更清晰的反應了創建對象的行為。轉換操作符會在代碼中引入很難發現的問題。假設你的代碼的庫的繼承結構如圖1.1一樣。 Circle 和 Ellipse 是 Shape 的子類。你打算不考慮這個結構關系,因為你認為,即使 Cirle 和 Ellipse 是相關的,但你不想要結構中非抽象的兄弟關系,當你嘗試從 Ellips 類對象得到 Circle 對象就會發現幾個實現問題。然而,你實現的每個 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 static double ComputeArea(Ellipse e)
{
// return the area of the ellipse.
return e.R1 * e.R2 * Math.PI;
}
// call it:
Circle c1 = new Circle(new PointF(3.0f, 0), 5.0f);
ComputeArea(c1);
```
這個例子就是我說的代替:一個 Circle 對象可以代替 Ellipse 對象。ComputeArea 函數甚至能在代替后工作。你獲得好運氣。但是研究下這個函數:
```
public static 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 方法需要 Ellipse 的參數。編譯器會某些情況下會將 Ellipse 轉換為 Circle 。你定義的隱式轉換就是實現這個工作的。你的轉換會被調用,而且 Flatten 函數接受的被隱式轉換創建的 Ellipse 對象。這個臨時對象唄 Flatten 函數修改,并且立即變成了垃圾。副作用預期是從你的 Flatten 函數發生的,但是僅僅是一個臨時變量。最后的結果就是 Circle c 沒有發生任何事情。
將隱式轉換該為強制轉換:
```
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
Flatten((Ellipse)c);
```
原來的問題還是存在。你只是強制你的使用添加強制轉換來引起這個問題。你還是創建臨時對象, Flatten 函數作用在這個臨時對象上,并且丟失這個對象。 Circle 根本沒有被改變。相反,如果你創建一個構造器轉換 Circle 到 Ellipse ,這個操作就更清晰了:
```
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
Flatten(new Ellipse(c));
```
大多數程序員會看到上面兩行代碼而立即發現傳給 Flatten() 的 Ellipse 的任何修改都會丟失。他們會持有一個新的對象來修復這個問題:
```
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
Flatten(c);
// Work with the circle.
// ...
// Convert to an ellipse.
Ellipse e = new Ellipse(c);
Flatten(e);
```
變量持有了被 Flatten 修改的 Ellipse 對象。通過構造函數代替轉換操作符,你還沒有失去任何功能,你只是創建新對象就使它更清晰。(老練的 C++ 程序員,應注意, C# 調用構造函數不會進行隱式或顯式轉換。您可以創建只有當你明確地使用 new 運算符的新對象,并在沒有其他時候。所以在 C# 構造器中不需要 explicit 關鍵字。)
轉換操作符你使得對象返回原來沒有的行為的域。這會其他一些問題。你隱藏了一個大漏洞在封裝的類中。當強制你的類型轉換到另一對象時,使用你這個類的可以訪問內部變量。在原則26中討論了最好避免的所有原因。
轉換操作符引入了代替的一個方式但會以前你的代碼拋出問題。你必須明確:用戶預期的類可以在你創建的這個類的對象任何地方使用。當對象的代替可用,你使得調用者用的是你創建的臨時對象或能訪問內部域。這個微妙的錯誤很難被發現因為編譯器產生了轉換對象的代碼。在你的 API 里避免轉換操作。
小結:
這個原則雖然很簡短但卻很精辟,告訴我們實際編程應該更多通過規范來約束代碼的行為,而不是將諸多情況都交給編譯器來處理,這樣總會被自己坑到的,要能在代碼中嚴格控制自己的邏輯。周末還有好多工作,加油!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1983118](/blog/1983118)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數