System.Object.ToString()恐怕是.NET中最常用的方法了。應該為我們的類的所有客戶代碼提供一個合理的版本,否則這些代碼就只能使用我們的類的一些屬性來自己定製可讀的表示了。類型的字串表示非常有用,可以在很多地方向使用者顯示對象的有關資訊,例如在Windows Forms上、Web Forms上、控制台輸出視窗中,以及調試環境中。為此,我們建立的每一個類型都應該重寫Object類的ToString()方法。如果建立的是更複雜的類型,則應該實現Iformattable.ToString()方法。如果我們沒有重寫該方法,或者寫得不夠好,那麼使用它們的客戶代碼就要自己想辦法修補了。
System.Object預設提供的ToString()方法會傳回型別的名稱。這樣的資訊一般沒有什麼用處,像"Rect"、"Point"、"Size"這樣的字串大多都不是我們希望顯示給使用者的。但如果我們不重寫Object的ToString()方法,使用者看到的就將是這些。我們只需要寫一次,但是客戶將享用無數次。一點點付出,就可以讓很多人(包括我們自己)受益。
讓我們來看看重寫System.Object.ToString()這個最簡單的需求。該方法主要的功能就是為類型提供一個最常用的文本表示。例如,考慮下面這個具有三個欄位的Customer類:
public class Customer
{
private string _name;
private decimal _revenue;
private string _contactPhone;
}
public class Customer
{
private string _name;
private decimal _revenue;
private string _contactPhone;
}
如果不提供重寫的版本,Customer將繼承Object類的ToString()方法,也就是返回一個"Customer"字串。這個字串實在沒有什麼用處。即使ToString()方法只應用於調試的目的,它也應該輸出一個更有意義的字串。我們重寫的時候應該盡量考慮客戶所希望的表示。就Customer類來說,返回_name是一個不錯的選擇:
public override string ToString()
{
return _name;
}
public override string ToString()
{
return _name;
}
即使大家不遵循本條款中的其他建議,也要遵循這裡所展示的實踐。它可以節省很多人的時間。在我們為Customer類重寫了ToString()方法之後,該類的對象將可以更容易地添加到Windows Forms控制項、Web Forms控制項或者控制台上。.NET FCL在將對象顯示到各個控制項(如Combo Box、List Box、Text Box等)上時,使用的就是Object.ToString()的重寫版本。如果我們在Windows Forms或者Web Forms上建立了一個Customer對象的列表,其文本顯示將為Customer的名稱(_name)。System.Console.WriteLine()方法、System.String.Format()方法等內部也都調用到了ToString()方法。只要當.NET FCL需要擷取Customer的字串表示時,我們的Customer類型都將以其名稱(_name)來響應。僅僅提供一個具有三行代碼的方法,就可以處理所有這些基本的需求。
雖然簡單的ToString()方法很多時候已經可以滿足我們的需求,但有時候,我們還需要功能更強的方法。上述Customer類型有三個欄位:_name、_revenue和_contactPhone,而我們僅使用了_name一個欄位。我們可以通過實現IFormattable介面來解決這個問題。IFormattable介面包含了一個重載的ToString()方法,它允許我們為類型指定某種格式資訊。當我們需要為類型建立不同形式的字串輸出時,這個介面非常有用。Customer類型就是一個例子。比如,有些使用者可能希望建立一個報表,在其中以表格的形式包含客戶的名稱和上一年的收入。IFormattable.ToString()方法允許使用者為我們的類型指定某種格式的字串輸出。其簽名如下:
view plaincopy to clipboardprint?
string System.IFormattable.ToString(string format, IFormatProvider formatProvider);
string System.IFormattable.ToString(string format, IFormatProvider formatProvider);
我們可以使用格式字串來為我們的類型指定自己的格式。比如,使用特定的字元來表示某種格式資訊。在Customer類型的例子中,我們可以使用n來表示name,使用r來表示revenue,使用p來表示phone。另外,還可以指定這些字元的組合形式。下面的代碼展示了一種可能的做法:
#region IFormattable Members
// 所支援的格式:
// 用n 表示name。
// 用r 表示revenue。
// 用p 表示contact phone。
// 同時支援組合格式: nr、np、npr等。
// "G" 表示通用格式。
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;
}
}
#endregion
#region IFormattable Members
// 所支援的格式:
// 用n 表示name。
// 用r 表示revenue。
// 用p 表示contact phone。
// 同時支援組合格式: nr、np、npr等。
// "G" 表示通用格式。
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;
}
}
#endregion
添加該函數使得Customer類型的客戶可以定製Customer類型的表示:
view plaincopy to clipboardprint?
IFormattable c1 = new Customer();
Console.WriteLine("Customer record: {0}", c1.ToString("nrp", null));
IFormattable c1 = new Customer();
Console.WriteLine("Customer record: {0}", c1.ToString("nrp", null));
IFormattable.ToString()的實現一般來說是依類型而異的,但有些工作是每一個類型中我們都需要處理的。首先,我們必須支援表示“通用格式”的"G"。其次,我們必須支援兩種形式的“空格式”,即""和null。這三種格式返回的字串必須與Object.ToString()的重寫版本返回的字串相同。.NET FCL對每一個實現了IFormattable介面的類型,會調用IFormattable.ToString(),而非Object.ToString()。.NET FCL通常會用一個null的格式字串來調用IFormattable.ToString(),只是在一小部分場合會使用"G"來表示通用格式。如果我們的類型支援IFormattable介面,但又不支援這些標準格式,那麼我們就打破了FCL中的自動字串轉換規則。
IFormattable.ToString()方法的第二個參數為一個實現了IFormatProvider介面的對象。該對象允許客戶程式提供一些我們不能預料的格式化選項。如果看前面IFormattable.ToString()的實現,總會有一些我們期望、但實際上卻沒有提供的格式化選項。如果我們希望提供的輸出容易為人所讀懂,這種情況便不可避免。不管我們支援多少種格式化選項,使用者總有一天會期望某種我們無法預料的格式。這就是上面的程式碼範例中最開始的幾行所做的工作:尋找實現了IFormatProvider介面的對象,然後將格式化任務交給其中的ICustomFormatter來完成。
下面,將我們的視角從類的作者轉到類的使用者上來。假設我們期望的某種格式沒有獲得支援,例如某些customer的name字元數要大於20,這時候我們希望提供字元數為50的name。這就是IFormatProvider介面的用武之地了。我們需要建立兩個類:一個實現IFormatProvider介面,另一個實現ICustomFormatter介面——該類用於建立自訂的輸出格式。IFormatProvider介面中定義有一個方法:GetFormat(),該方法會返回一個實現了ICustomFormatter介面的對象。ICustomFormatter介面中包含了實際執行格式化的方法。下面的代碼實現了提供字元數為50的name輸出:
// IFormatProvider樣本:
public class CustomFormatter : IFormatProvider
{
#region IFormatProvider Members
// IFormatProvider 僅包含一個方法。
// 該方法返回一個使用指定介面格式的對象。
// 一般情況下,只有ICustomFormatter被實現。
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return new CustomerFormatProvider();
return null;
}
#endregion
// 一個嵌套類,為Customer類提供定製格式。
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
}
}
// IFormatProvider樣本:
public class CustomFormatter : IFormatProvider
{
#region IFormatProvider Members
// IFormatProvider 僅包含一個方法。
// 該方法返回一個使用指定介面格式的對象。
// 一般情況下,只有ICustomFormatter被實現。
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return new CustomerFormatProvider();
return null;
}
#endregion
// 一個嵌套類,為Customer類提供定製格式。
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
}
}
上面的GetFormat()方法建立了一個實現了ICustomFormatter介面的對象。ICustomFormatter.Format()方法則按指定的方式執行實際的格式化輸出工作,將對象轉換為一個字串格式。我們可以為ICustomFormatter.Format()方法定義format參數,以便指定多種格式化選項。參數formatProvider則是用於調用GetFormat()方法的一個IFormatProvider對象。
要指定我們自己定製的格式,需要顯式調用string.Format()方法,並傳遞一個IFormatProvider對象:
Console.WriteLine(string.Format(new CustomFormatter(), "", c1 ));
Console.WriteLine(string.Format(new CustomFormatter(), "", c1 ));
不管一個類是否實現了IFormattable介面,我們都可以為其建立IformatProvider和ICustomFormatter的實作類別。因此即使一個類的原作者沒有提供合理的ToString()行為,我們仍然可以為其提供格式化支援。當然,作為一個類的外部存取者,我們只能通過訪問其中的公有屬性和資料成員來構造字串。雖然編寫兩個類(分別實現IFormatProvider和ICustomFormatter)需要很多工作,且其目的僅僅是為了得到一個字串。但是,一旦使用了這種方式來實現我們自己定義的字串輸出,它們將在.NET架構的各個地方得到支援。
現在,再讓我們回到類作者這一角色上來。重寫Object.ToString()是為類提供字串表示的最簡單方式。每當我們建立一個類型時,都要提供該方法。它應該是我們的類型最明顯、最常用的一種表示。只有在一些比較少的情況下,當我們期望為類型提供更複雜的輸出格式時,才應該實現IFormattable介面。它為“類型的使用者定製類型的字串輸出”提供了一種標準的方式。如果我們沒有做這些工作,使用者就要自己來實現自訂格式化器。那樣的做法需要更多的代碼,因為使用者處於類外,無法訪問對象的內部狀態。
人們總有擷取類型資訊的需求,而字串對於人來說是最容易理解的。我們應該積極地去做這件事,而重寫所有類型中的ToString()方法可能是所有做法中用最簡單的。