天天看點

類型自定義格式字元串類型自定義格式字元串

類型自定義格式字元串

引言

String可能是使用最多的類型,ToString()則應該是大家使用得最多的方法了。然而它不應該僅僅是用來輸出類型的名稱,如果使用得當,它可以友善地輸出我們對類型自定義的格式。本文将循序漸進地讨論ToString(),以及相關的IFormattable、IFormatProvider以及ICustomFormatter接口。

在類型内部提供自定義格式字元串的能力

繼承自System.Object 基類的 ToString()

String是人們直接就可以看懂的資料類型之一,很多情況下我們都會期望能夠獲得類型的一個字元串輸出。是以,Microsoft 在.Net Framework所有類型的基類System.Object中提供了一個虛拟的 ToString()方法,它的預設實作是傳回對象的類型名稱。

假設我們有這樣的一個類型,它定義了“朋友”這一對象的一些資訊:

namespace CustomToString

   public class Friend {

       private string familyName;    // 姓

       private string firstName;     // 名

       public Friend(string familyName, string firstName){

          this.familyName = familyName;

          this.firstName = firstName;

       }   

       public Friend(): this("張","子陽"){}

       public string FamilyName {

          get { return familyName; }

       }

       public string FirstName {

          get { return firstName; }

   }

}

當我們在Friend的執行個體上調用 ToString()方法時,便會傳回類型的名稱:CustomToString.Friend。

Friend f = new Friend();

Console.WriteLine(f.ToString()); // 輸出:CustomToString.Friend

覆寫 ToString() 方法

在上面的例子中,不管類型執行個體(對象)所包含的資料(字段值)是什麼,它總是會傳回相同的結果(CustomToString.Friend)。很多時候,傳回一個對象的類型名稱對我們來說沒有多大的意義,拿上面來說,我們可能更加期望能夠傳回朋友的姓名(famliyName和firstName字段的值)。這時候,我們可以簡單地覆寫System.Object基類的ToString()方法,在 Friend 類中添加如下方法:

// 覆寫System.Object基類的 ToString() 方法

public override string ToString() {

   return String.Format("Friend: {0}{1}", familyName, firstName);

此時,我們再次運作代碼:

Console.WriteLine(f.ToString());    // 輸出:Friend: 張子陽

f = new Friend("王","濤");

Console.WriteLine(f.ToString()); // 輸出:Friend: 王濤

可以看到對于不同的對象,ToString()根據對象的字段值傳回了不同的結果,這樣對我們來說會更加有意義。

重載 ToString() 方法

有時候,我們可能需要将對象按照不同的方式進行格式化。就拿Friend類型來說:西方人是名在前,姓在後;而中國人是 姓在前,名在後。是以如果運作下面的代碼,雖然程式不會出錯,但從英語文法角度來看卻有問題:

Friend a = new Friend("Zhang", "Jimmy");

Console.WriteLine(a.ToString());    // 輸出:Friend: ZhangJimmy

而我們期望輸出的是:Jimmy Zhang。這個時候,大家可以想一想想 .Net Framework 解決這個問題采用的方法:重載ToString()。讓ToString()方法接收一個參數,根據這個參數來進行格式化。比如 int a = 123; Console.WriteLine(a.ToString("c"));指定了字元串"c"作為參數,産生貨币類型的輸出:¥123.00。我們也可以使用這種方式來改進Friend類,在Friend中重載一個 ToString() 方法,使之根據一個字元參數來定義其字元串格式化:

// 根據字元串參數來定義類型的格式化

public string ToString(string format) {

    switch (format.ToUpper()) {

       case "W":         // West: 西方

           return String.Format("Friend : {0} {1}", firstName, familyName);

       case "E":         // East: 東方

           return this.ToString();

       case "G":         // General

       default:

           return base.ToString();

    }

然後我們在使用ToString()方法時可以使用重載的版本,對于英文名,我們傳入"W"作為參數,這樣就解決了上面的問題:

Console.WriteLine(f.ToString());        // 輸出:Friend: 張子陽

f = new Friend("Zhang", "Jimmy");

Console.WriteLine(f.ToString("W")); // 輸出:Friend: Jimmy Zhang

NOTE:

這個問題更好的解決辦法并非是重載ToString(),可以簡單地使用屬性來完成,比如這樣:

public string WesternFullName{

    get{ return String.Format("{0} {1}", firstName, familyName)}

public string EasternFullName{

    get{ return String.Format("{0}{1}", familyName, firstName)}

在本文中,我在這裡僅僅是舉一個例子來說明,是以就先不去管使用屬性這種方式了,後面也是一樣。

實作 IFormattable 接口

我們站在類型設計者的角度來思考一下:我們為使用者提供了Friend類,雖然重載的 ToString() 可以應對 東方/西方 的文化差異,但是使用者的需求總是千變萬化。比如說,使用者是一名Web開發者,并且期望人名總是以加粗的方式顯示,為了避免每次操作時都取出屬性再進行格式化,他會希望隻要在類型上應用ToString()就可以達到期望的效果,這樣會更省事一些,比如:

label1.Text = String.Format("<b>{0}{1}</b>", f.familyName, f.firstName);

// 這樣會更加友善(使用者期望):

// label1.Text = f.ToString(***);

此時我們提供的格式化方法就沒有辦法實作了。對于不可預見的情況,我們希望能讓使用者自己來決定如何進行對象的字元串格式化。Microsoft顯然想到了這一問題,并為我們提供了IFormattable接口。

當你作為一名類型設計者,期望為你的使用者提供自定義的格式化ToString()時,可以實作這個接口。

我們現在來看一下這個接口的定義:

public interface IFormattable {

    string ToString(string format, IFormatProvider formatProvider);

它僅包含一個方法 ToString():參數 format 與我們上一小節重載的ToString()方法中的 format 含義相同,用于根據參數值判斷如何進行格式化;參數 formatProvider 是一個 IFormatProvider 類型,它的定義如下:

public interface IFormatProvider {

    object GetFormat(Type formatType);

其中 formatType 是目前對象的類型執行個體(還有一種可能是ICustomFormatter,後面有說明) --Type對象。在本例中,我們是對Friend這一類型進行格式化,那麼這個formatType 的值就相當于 typeof(Friend),或者 f.GetType() (f為Friend類型的執行個體)。GetFormat()方法傳回一個Object類型的對象,由這個對象進行格式化的實際操作,這個對象實作了 ICustomFormatter 接口,它隻包含一個方法,Format():

public interface ICustomFormatter{

   string Format(string format, object arg, IFormatProvider formatProvider);

其中 format 的含義與上面相同,arg 為欲進行格式化的類型執行個體,在這裡是Friend的一個執行個體,formatProvider 這裡通常不會用到。

看到這裡你可能會感覺有點混亂,實際上,你隻要記得:作為類型設計者,你隻需要實作 IFormattable 接口就可以了:先通過參數provider的 IFormatProvider.GetFormat() 方法,得到一個 ICustomFormatter 對象,再進一步調用 ICustomFormatter 對象的 Format()方法,然後傳回 Format() 方法的傳回值:

public class Friend: IFormattable{

   // 略 ...

    // 實作 IFormattable 接口

    public string ToString(string format, IFormatProvider provider) {

       if (provider != null) {

           ICustomFormatter formatter =

              provider.GetFormat(this.GetType()) as ICustomFormatter;

           if (formatter != null)

              return formatter.Format(format, this, provider);

       }

       return this.ToString(format);

上面需要注意的地方就是 IFormatProvider.GetFormat()方法将目前的Friend對象的類型資訊(通過this.GetType())傳遞了進去。

類型設計者的工作在這裡就完結了,現在讓我們看下對于這個實作了IFormattable的類型,類型的使用者該如何使用自己定義的方法對對象進行字元串格式化。作為類型的使用者,為了能夠實作對象的自定義格式字元串,需要實作 IFormatProvider 和 ICustomFormatter接口。此時有兩種政策:

  1. 建立一個類,比如叫 FriendFormatter,這個類實作 IFormatProvider 和 ICustomFormatter 接口。
  2. 建立兩個類,比如叫 ObjectFormatProvider 和 FriendFormatter,分别實作 IFormatProvider 和 ICustomFormatter 接口,并且讓 ObjectFormatProvider 的 GetFormat()方法傳回一個 FriendFormatter 的執行個體。

我們先來看看第一種政策:

public class FriendFormatter : IFormatProvider, ICustomFormatter {

    // 實作 IFormatProvider 接口,由 Friend類的 IFormattable.ToString()方法調用

    public object GetFormat(Type formatType) {

       if (formatType == typeof(Friend))

           return this;

       else

           return null;

    // 實作 ICustomFormatter 接口

    public string Format(string format, object arg, IFormatProvider formatProvider) {

       //if (arg is IFormattable)

       //  return ((IFormattable)arg).ToString(format, formatProvider);

       Friend friend = arg as Friend;

       if (friend == null)

           return arg.ToString();

       switch (format.ToUpper()) {

           case "I":

             return String.Format("Friend: <i>{0}{1}<i>" ,friend.FamilyName, friend.FirstName);

           case "B":

             return String.Format("Friend: <b>{0}{1}<b>", friend.FamilyName, friend.FirstName);

           default:

              return arg.ToString();

結合上面的 ToString()方法一起來看,這裡的流程非常清楚:

使用這種方式時,GetFormat中的判斷語句,if(formatType == typeof(Friend)) 確定 FriendFormatter 類隻能應用于 Friend類型對象的格式化。

随後,通過this關鍵字傳回了目前 FriendFormatter 對象的引用。因為FriendFormatter也實作了 ICustomFormatter接口,是以在Friend類型的 IFormattable.ToString()方法中,能夠将FriendFormater 轉換為一個ICustomFormatter類型,接着調用了ICustomFormatter.Format()方法,傳回了預期的效果。

注意上面注釋掉的部分,可能是參考了MSDN的緣故吧,有些人在實作ICustomFormatt的時候,會加上那部分語句。實際上MSND範例中使用的一個Long類型,并且使用的是String.Format()的重載方法來進行自定義格式化,與這裡不盡相同。當你屏蔽掉上面的注釋時,很顯然會形成一個無限循環。

我們現在來對上面的代碼進行一下測試:

FriendFormatter formatter = new FriendFormatter();

Console.WriteLine(f.ToString("b", formatter));    // 輸出:Friend: <b>張子陽</b>

接下來我們看下第二種方式,将 IFormatProvider 和 ICustomFormatter 交由不同的類來實作:

public class ObjectFormatProvider : IFormatProvider {

    // 實作 IFormatProvider 接口,由 Friend類的 ToString() 方法調用

           return new FriendFormatter();//傳回一個實作了ICustomFormatter的類型執行個體

// 實作ICustomFormatter接口,總是為一個特定類型(比如Friend)提供格式化服務

public class FriendFormatter : ICustomFormatter {

             return String.Format("Friend: <i>{0}{1}<i>", friend.FamilyName, friend.FirstName);

看上去和上面的方法幾乎一樣,差別不過是将一個類拆成了兩個。實際上,拆分成兩個類會更加的靈活:使用一個類實作兩個接口的方式時,FriendFormatter 隻能用來格式化 Friend類型。如果再有一個Book類,類似地,需要再建立一個 BookFormatter。

而将它拆分成兩個類,隻需要再建立一個類實作一遍 ICustomFormatter 接口,然後對ObjectFormatProvider做些許修改就可以了。此時Provider類可以視為一個通用類,可以為多種類型提供格式化服務。現在假設我們有一個Book類型,我們隻需要這樣修改一下 ObjectFormatProvider類就可以了:

           return new FriendFormatter();

       if (formatType == typeof(Book))

           return new BookFormatter();  // 傳回一個BookFormatter對象

// BookFormatter 類型省略 ...

在類型外部提供自定義格式字元串的能力

現在我們站在一個類型使用者的角度來思考一下:很多時候,類型的設計者并沒有為類型實作IFormattable接口,此時我們該如何處理呢?我們再思考一下.Net Framework中的處理方式:

int a = 123;

Console.WriteLine(a.ToString("c"));        // 輸出: ¥123.00

Console.WriteLine(String.Format("{0:c}", a));  // 輸出: ¥123.00

實際上,String.Format()還提供了一個重載方法,可以一個接收IFormatProvider對象,這個IFormatProvider由我們自己定義,來實作我們所需要的格式化效果。根據上面的對比,我們再做一個總結:為了實作類型的自定義格式字元串,我們總是需要實作IFormatProvider接口。如果類型實作了IFormattable接口,我們可以在類型上調用ToString()方法,傳遞IFormatProvider對象;如果類型沒有實作IFormattable接口,我們可以通過String.Format()靜态方法,傳遞IFormatProvider對象。

現在我們就來建立實作IFormatProvider接口的類型了,與上面的方式稍稍有些不同:通過Reflector工具(不知道的可以去百度一下)可以看到,調用 String.Format() 時内部會建立一個 StringBuilder類型的對象builder,然後調用 builder.AppendFormat(provider, format, args); 在這個方法内部,最終會調用provider的GetFormat()方法:

formatter = (ICustomFormatter) provider.GetFormat(typeof(ICustomFormatter));

可以看到,provider.GetFormat()傳遞了一個typeof(ICustomFormatter)對象。是以,如果要判斷是不是在類型外部通過String.Format()這種方式來使用 IFormatProvider,隻需要判斷 formatType是不是等于 typeof(ICustomFormatter) 就可以了:

public class OutFriendFormatter : IFormatProvider, ICustomFormatter

{

    public object GetFormat(Type formatType)

    {

       if (formatType == typeof(ICustomFormatter))  

    // 實作 ICustomFormatter 略   

我們再次對代碼進行一下測試:

OutFriendFormatter formatter = new OutFriendFormatter();

string output = String.Format(formatter, "{0:i}", f);

Console.WriteLine(output);      // Friend: <i>張子陽<i>

.Net 中實作IFormatProvider的一個例子

.Net 中使用 IFormatProvider 最常見的一個例子就是 CultureInfo 類了。很多時候,我們需要對金額進行格式化,此時我們通常都會這樣:

int money = 100;

Console.WriteLine(String.Format("{0:c}", money));

我們期望這個輸出的結果是 ¥100.00。然而情況并非總是如此,當你将這個程式運作于中文作業系統下時,的确會如你所願得到 ¥100.00;而在英文作業系統下,你恐怕會得到一個 $100.00。這是因為在對數字以金額方式進行顯示的時候,會依據目前系統的語言環境做出判斷,如果你沒有顯示地指定語言環境,那麼就會按照預設的語言環境來進行相應的顯示。在.Net中,将語言環境進行封裝的類是 CultureInfo,并且它實作了IFormatProvider,當我們需要明确指定金額的顯示方式時,可以借助這個類來完成:

IFormatProvider provider = new CultureInfo("zh-cn");

Console.WriteLine(String.Format(provider, "{0:c}", money));    // 輸出:¥100.00

provider = new CultureInfo("en-us");

Console.WriteLine(String.Format(provider, "{0:c}", money));    // 輸出:$100.00

總結

在這篇文章中,我較系統地讨論了如何對類型進行自定義格式化。我們通過各種方式達到了這個目的:覆寫ToString()、重載ToString()、實作 IFormatProvider接口。我們還讨論了實作IFormatProvider和ICustomFormatter的兩種方式:建立一個類實作它們,或者各自實作為不同的類。

我想很多人在讀這篇文章以前就會使用這些方法了,我在這裡希望大家能夠多進行一點思考,以一個.Net 架構設計者的角度來思考:為什麼會設計出三個接口配合 String.Format()靜态類來實作這一過程?這樣設計提供了怎樣的靈活性?從這篇文章中,我期望你收獲更多的不是作為一個架構使用者如何去使用這些類型,而是作為一個架構設計者來設計出這樣的類型結構。

感謝閱讀,希望這篇文章能帶給你幫助!

繼續閱讀