型別自定義格式字串
型別自定義格式字串
引言
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);
}
此時,我們再次執行程式碼:
Friend f = new Friend();
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"作為引數,這樣就解決了上面的問題:
Friend f = new Friend();
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()就可以達到期望的效果,這樣會更省事一些,比如:
Friend f = new Friend();
label1.Text = String.Format("{0}{1}", 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介面。此時有兩種策略:
建立一個類,比如叫 FriendFormatter,這個類實現 IFormatProvider 和 ICustomFormatter 介面。
建立兩個類,比如叫 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: {0}{1}" ,friend.FamilyName, friend.FirstName);
case "B":
return String.Format("Friend: {0}{1}", 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()方法,返回了預期的效果。
NOTE:注意上面註釋掉的部分,可能是參考了MSDN的緣故吧,有些人在實現ICustomFormatt的時候,會加上那部分語句。實際上MSND範例中使用的一個Long型別,並且使用的是String.Format()的過載方法來進行自定義格式化,與這裡不盡相同。當你遮蔽掉上面的註釋時,很顯然會形成一個無限迴圈。
我們現在來對上面的程式碼進行一下測試:
Friend f = new Friend();
FriendFormatter formatter = new FriendFormatter();
Console.WriteLine(f.ToString("b", formatter)); // 輸出:Friend: 張子陽
接下來我們看下第二種方式,將 IFormatProvider 和 ICustomFormatter 交由不同的類來實現:
public class ObjectFormatProvider : IFormatProvider {
// 實現 IFormatProvider 介面,由 Friend類的 ToString() 方法呼叫
public object GetFormat(Type formatType) {
if (formatType == typeof(Friend))
return new FriendFormatter();//返回一個實現了ICustomFormatter的型別例項
else
return null;
}
}
// 實現ICustomFormatter介面,總是為一個特定型別(比如Friend)提供格式化服務
public class FriendFormatter : ICustomFormatter {
// 實現 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: {0}{1}", friend.FamilyName, friend.FirstName);
case "B":
return String.Format("Friend: {0}{1}", friend.FamilyName, friend.FirstName);
default:
return arg.ToString();
}
}
}
看上去和上面的方法幾乎一樣,區別不過是將一個類拆成了兩個。實際上,拆分成兩個類會更加的靈活:使用一個類實現兩個介面的方式時,FriendFormatter 只能用來格式化 Friend型別。如果再有一個Book類,類似地,需要再建立一個 BookFormatter。
而將它拆分成兩個類,只需要再建立一個類實現一遍 ICustomFormatter 介面,然後對ObjectFormatProvider做些許修改就可以了。此時Provider類可以視為一個通用類,可以為多種型別提供格式化服務。現在假設我們有一個Book型別,我們只需要這樣修改一下 ObjectFormatProvider類就可以了:
public class ObjectFormatProvider : IFormatProvider {
// 實現 IFormatProvider 介面,由 Friend類的 ToString() 方法呼叫
public object GetFormat(Type formatType) {
if (formatType == typeof(Friend))
return new FriendFormatter();
if (formatType == typeof(Book))
return new BookFormatter(); // 返回一個BookFormatter物件
else
return null;
}
}
// 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
{
// 實現 IFormatProvider 介面,由 Friend類的 ToString() 方法呼叫
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
// 實現 ICustomFormatter 略
}
我們再次對程式碼進行一下測試:
Friend f = new Friend();
OutFriendFormatter formatter = new OutFriendFormatter();
string output = String.Format(formatter, "{0:i}", f);
Console.WriteLine(output); // Friend: 張子陽
.Net 中實現IFormatProvider的一個例子
.Net 中使用 IFormatProvider 最常見的一個例子就是 CultureInfo 類了。很多時候,我們需要對金額進行格式化,此時我們通常都會這樣:
int money = 100;
Console.WriteLine(String.Format("{0:c}", money));
我們期望這個輸出的結果是 ¥100.00。然而情況並非總是如此,當你將這個程式執行於中文作業系統下時,的確會如你所願得到 ¥100.00;而在英文作業系統下,你恐怕會得到一個 $100.00。這是因為在對數字以金額方式進行顯示的時候,會依據當前系統的語言環境做出判斷,如果你沒有顯示地指定語言環境,那麼就會按照預設的語言環境來進行相應的顯示。在.Net中,將語言環境進行封裝的類是 CultureInfo,並且它實現了IFormatProvider,當我們需要明確指定金額的顯示方式時,可以藉助這個類來完成:
int money = 100;
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()靜態類來實現這一過程?這樣設計提供了怎樣的靈活性?從這篇文章中,我期望你收穫更多的不是作為一個框架使用者如何去使用這些型別,而是作為一個框架設計者來設計出這樣的型別結構。
感謝閱讀,希望這篇文章能帶給你幫助!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2236/viewspace-2811701/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 自定義型別型別
- 自定義函式實現字串分割,返回集合型別函式字串型別
- 自定義數字格式字串輸出示例字串
- 自定義資料型別資料型別
- Pl/SQL 自定義型別SQL型別
- ORACLE 自定義型別[轉]Oracle型別
- DM自定義資料型別資料型別
- UnrealEngine建立自定義資產型別Unreal型別
- Linq to sql 自定義型別SQL型別
- ros|自定義訊息型別ROS型別
- 建立自定義塊 - 型別檢查型別
- C# 泛型集合的自定義型別排序C#泛型型別排序
- JumpList中Recent類別和自定義型別薦型別
- 自定義分頁格式
- Laravelapi 自定義 response 格式LaravelAPI
- Android 自定義構建型別 BuildTypeAndroid型別UI
- MyBatis使用自定義TypeHandler轉換型別MyBatis型別
- C語言筆記——自定義型別C語言筆記型別
- 兄弟連go教程(7)自定義型別Go型別
- EF:自定義Oracle的對映型別Oracle型別
- SQL Server 中自定義資料型別SQLServer資料型別
- PostgreSQL自定義自動型別轉換(CAST)SQL型別AST
- Artisan 自定義輸出格式
- 自定義Nginx日誌格式Nginx
- C++ 之預定義型別 IO 格式控制C++型別
- 《Haskell趣學指南》筆記之自定義型別Haskell筆記型別
- Mybatis使用小技巧-自定義型別轉換器MyBatis型別
- ROS2/C++ 自定義訊息型別ROSC++型別
- Hadoop-MapReduce之自定義資料型別Hadoop資料型別
- XSD中自定義型別的三種方式型別
- 多型關聯自定義的型別欄位的處理多型型別
- 自主資料型別:在TVM中啟用自定義資料型別探索資料型別
- 第11章 使用類——型別轉換(二)將自定義型別轉換為內建型別型別
- TypeScript 字串型別TypeScript字串型別
- SCSS 字串 型別CSS字串型別
- 理解VC++裡字串型別的真正含義 (轉)C++字串型別
- 還可以這麼玩?超實用 Typescript 內建型別與自定義型別TypeScript型別
- 一文說透WordPress的自定義文章型別型別