# 原則5:總是提供 ToString()
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明[出處](/blog/1979985):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
在 .NET 環境中 System.Object.ToString() 是最常使用方法之一。你應該為客戶端代碼的所有類寫一個合理的版本。否則,你要強迫每個使用你代碼的使用者使用你創建可讀的說明的屬性。字符串說明很容易被用來向使用者說明對象的信息:在 WPF (Windows Presentation Foundation)框架中, Silverlight 框架中, Web Forms 或控制臺輸出。字符串說明也能被用來調試。你創建的每個類型都應該重寫(override)這個方法。當你創建更復雜的類型,你應該實現更復雜的 IFormattable.ToString() 。必須承認:如果你沒有重寫這個方法,或者寫了得很糟糕,你的客戶會強制為你修復這個問題。
System.Object 的版本會返回類型的全限定名字。這個信息很少有用:“ System.Drawing.Rect ”,“ MyNamespace.Point ”,“SomeSample.Size ” 不是我們要向客戶展示的信息。這就是你沒有重寫 ToString() 獲得的信息。你一個類只寫一次,但是你的客戶使用很多次。當你的寫類多付出一點工作,會從你使用者那里得到回報的。
我們考慮最簡單的需求:重寫 System.Object.ToString() 。你創建的類你需要重寫 ToString 為了提供最常用的文本說明。下面的自定義的類,提供三個 public 屬性:
```
public class Customer
{
public string Name
{
get;
set;
}
public decimal Revenue
{
get;
set;
}
public string ContactPhone
{
get;
set;
}
public override string ToString()
{
return Name;
}
}
```
繼承的 Object.ToString() 版本會發揮 “ Custormer ”。這個信息對任何都沒有作用。即使你使用 ToString() 只是為了調試,它應該比那更復雜些。
你應該重寫 ToString() 方法,使之返回最有可能是這個類的文本說明。在 Customer 的例子里,就是 name:
```
public override string ToString()
{
retrun Name;
}
```
如果你不遵從這個原則的其他建議,按照上面的方法為你的類重寫該方法。這會直接節省每個人的時間。當你提供了 Object.ToString() 方法的合理的實現,這個類的對象很容易添加到 WPF 控件, Siverlight 控件, Web Form 控件,或打印輸出。 .NET BCL 使用在每個控件中使用重寫的 Object.ToString() 來說明對象: combo boxes , list boxes , text boxes , 以及其他控件。在 WPF 或 Web Form 中,如果你創建 Customer 的對象列表,你可以通過 System.Console.WriteLine() 來輸出System.String.Format() 或 ToString() 的 name 文本。
任何時候 .NET BCL 想要獲取一個 Customer 的字符串說明,你的 Customer 類型要提供屬性 Name 。上面簡單的三行代碼處理所有基本的需求。
C# 3.0 編譯為匿名類提供了默認的 ToString() 。默認生成的 ToString() 會輸出每個元素的屬性值。屬性的說明的序列是 LINQ 查詢結果,會顯示他們的類型信息而不是他們的值。看下面這段代碼:
```
int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var test = new { Name = "Me",
Numbers = from l in list select l };
Console.WriteLine(test);
```
將會顯示:
```
{ Name = Me, Numbers = System.Linq.Enumerable+WhereSelectArrayIterator`2 [System.Int32,System.Int32] }
```
甚至,編譯器為匿名類產生的輸出會比你自定義的類型更好,除非你重寫了 ToString() 方法。你應該為使用者提供比編譯器在方法作用域中為暫時類型做的更好支持。
之前定義的 Customer 類型有三個屬性: Name , Revenue 和 Phone。 重寫的 System.ToString() 只使用了 Name 。你可以通過實現 IFormattalbe 接口來解決這個缺陷。 IFormattable 包含了一個重載的 ToString() 方法讓你指定類的格式化輸出信息。當你需要創建不同格式的字符串輸出是,你就需要這個接口。定義的類型是這個接口的對象。使用可以創建一個報表,以表格形式輸出包含 Customer 的 Name 和去年的收入 Revenue 。IFormattalbe.ToString 就是提供這個作用,讓你能格式化類的輸出字符串。 IFormattable.ToString() 方法聲明(signature)包含了一個要格式化的字符串和格式化提供器:
```
string System.IFormattable.ToString(string format, IFormatProvider formatProvider)
```
你可以使用格式化字符串來指定類型自己的格式。你可以指定格式化字符的關鍵字符。在這個 Customer 的例子里,你可以指定 n 表示 Name , r 表示 Revenue , p 表示 Phone 。同時還可以向下面 IFormattable.ToString() 版本一樣,指定組合關鍵字符:
```
// supported formats:
// substitute n for name.
// substitute r for revenue
// substitute p for contact phone.
// Combos are supported: nr, np, npr, etc
// "G" is general.
string System.IFormattable.ToString(string format, IFormatProvider formatProvider)
{
if (formatProvider != null)
{
ICustomFormatter fmt = formatProvider.GetFormat(
this.GetType()) as ICustomFormatter;
if (fmt != null)
return fmt.Format(format, this, formatProvider);
}
switch (format)
{
case "r":
return Revenue.ToString();
case "p":
return ContactPhone;
case "nr":
return string.Format("{0,20}, {1,10:C}",
Name, Revenue);
case "np":
return string.Format("{0,20}, {1,15}",
Name, ContactPhone);
case "pr":
return string.Format("{0,15}, {1,10:C}",
ContactPhone, Revenue);
case "pn":
return string.Format("{0,15}, {1,20}",
ContactPhone, Name);
case "rn":
return string.Format("{0,10:C}, {1,20}",
Revenue, Name);
case "rp":
return string.Format("{0,10:C}, {1,20}",
Revenue, ContactPhone);
case "nrp":
return string.Format("{0,20}, {1,10:C}, {2,15}",
Name, Revenue, ContactPhone);
case "npr":
return string.Format("{0,20}, {1,15}, {2,10:C}",
Name, ContactPhone, Revenue);
case "pnr":
return string.Format("{0,15}, {1,20}, {2,10:C}",
ContactPhone, Name, Revenue);
case "prn":
return string.Format("{0,15}, {1,10:C}, {2,15}",
ContactPhone, Revenue, Name);
case "rpn":
return string.Format("{0,10:C}, {1,15}, {2,20}",
Revenue, ContactPhone, Name);
case "rnp":
return string.Format("{0,10:C}, {1,20}, {2,15}",
Revenue, Name, ContactPhone);
case "n":
case "G":
default:
return Name;
}
}
```
添加下面函數,讓你客戶代碼能夠指定 Customer 數據的顯示:
```
IFormattable c1 = new Customer();
Console.WriteLine("Customer record: {0}",c1.ToString("nrp", null));
```
針對不同的類,IFomrattable.ToString() 的實現特定的,但當你實現 IFormattable 接口,你必須實現某些情況。首先,你必須支持一般格式 “G”。第二,你要支持空格式:"" 和 null 。你重寫的 Ojbect.ToString() 方法必須返回為這三個格式返回相同的字符串。 .NET 記錄類庫(BCL)為每個實現了 IFormattable 的類調用 IFormattable.ToString() 而不是 Object.ToString() 。.NET BCL 經常使用格式字符串 null 來條用 IFormattable.ToString() ,但很少使用格式字符 G 來格式化字符串。如果添加了 IFormattable 接口去沒有支持這個三個標準格式,你會打破 BCL 的字符串自動轉換的規范。你會發現這個的 IFormattable 很快會失控。你不可能預料到你的類型需要支持所有可能的格式。大多時候,選擇少數常見的格式。客戶端代碼應該彌補所有邊界情況。
IFormattable.ToString() 的第二個參數需要是實現 IFormatProvider 接口的對象。這個對象可以提供你沒有預料的到格式的支持。如果你瀏覽了前面 IFormattable.ToString() 的實現,毫無疑問你會發現數字格式的選項是沒有提供的。提供數字的輸出是理所當然德爾。無論你支持了多少種格式字符,使用者總有一天會發現一些格式是沒有預料到的。那就是為什么方法中頭幾行去查看實現 IFormatProvider 的對象并委托操作給它定義的 ICustomFormatter 。
把角色從類的定義轉移到類的使用者上去。你會發現你需要的格式沒有被支持。舉個例子,你的 Customer 的 Name的字符長度大于20,你需要修改了格式為 Customer Name提供50個字長度的格式輸出。
那就是為什么IFormatProvider 接口的產生。你創建的類可以通過實現 IFormatProvider 和一個實現 ICustomerProvider 的組合類來實現自定義的格式輸出。 IFormatProvider 接口定義了一個方法: GetFormat() 。 GetFormat() 方法返回了實現了 ICustomFormatter 接口的對象。ICustomFormatter 接口指定了實際的輸出格式的方法。下面兩個類的創建,修改使用50列來輸出 Customer Name 的輸出:
```
// Example IFormatProvider:
public class CustomFormatter : IFormatProvider
{
#region IFormatProvider Members
// IFormatProvider contains one method.
// This method returns an object that
// formats using the requested interface.
// Typically, only the ICustomFormatter
// is implemented
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatte))
return new CustomerFormatProvider();
return null;
}
#endregion
// Nested class to provide the
// custom formatting for the Customer class.
private class CustomerFormatProvider :ICustomFormatter
{
#region ICustomFormatter Members
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Customer c = arg as Customer;
if (c == null)
return arg.ToString();
return string.Format("{0,50}, {1,15}, {2,10:C}",c.Name, c.ContactPhone, c.Revenue);
}
#endregion
}
}
```
GetForm 方法返回創建實現 ICustomFormatter 接口的對象。在所請求的方式中,ICustomFormatter.Format() 做的實際格式輸出工作。你可以使用 ICustomFormatter.Format() 定義格式字符串,滿足在一個歷程中指定多個格式的需求。 格式提供器是從 GetFormat 方法返回的 ICustomFormatter 。
為了指定你定義的格式,你需要顯示調用參數為 IFormatProvider 對象的 string.Format():
```
Console.WriteLine(string.Format(new CustomFormatter(), "", c1));
```
現在把角色切回到類的定義者。重寫 Object.ToString() 是一個非常簡單的提供類的字符串說明的方式。當你定義一個類的時候,都應該重寫這個方法。它是最明顯和常用的類的說明。它不會太冗余。它會出現在控件,HTML 頁,或其他人可讀的地方。當罕見的類需要更復雜的輸出信息的時候,你就可以充分利用 IFormattable 接口的實現。這可以提供標準方式去定義類的文本輸出。如果你對此置若罔聞,你就需要自己實現的格式(formatter)。 這個解決方案需要更多的代碼,因為使用者不會檢查對象的內部狀態。同樣,實現這不可能預料所有潛在的格式。
最后,使用者會猜測你的類型的信息。會理解文本輸出,所以你需要盡可能提供最簡單的信息:所有的類都重寫 ToString() 。使得 ToString() 輸出的簡短而合理。
小結:
這幾天工作比較忙,就每天騰出體力翻譯了。這篇跟上篇比較類似,記住一個標題就可以了,更多是是一種編程思維,自從工作了,就發現自己太弱了, 以前讀書,覺得父母賺錢難,現在自己工作了,這是多么痛的領悟呀…… ,要加油,不要放棄,我也會有《放牛班的春天》的。竟然上了臺,就要相信自己可以做到——《激戰》。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1979985](/blog/1979985)
更多精彩請關注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:使用動態接收匿名類型參數