『飞鸽』彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
徹底學通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
自從使用.net以來就一直都在使用string.Format方法,一直沒有空或者其他原因都沒有深入去了解,主要還是因為項目上似乎沒有這么高的要求,也沒必要去深入了解,就算碰到了自定義的格式化內容也是寫幾個通用的方法而已。今天空下來仔細去理解了一下,在這里和大家分享一下,也希望大家一起交流。
?
string.Format方法是string類提供的靜態方法,一般最多使用的是其兩個參數的重載,例如:
?
var name = "Zhezhe";
var msg = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.", name, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg);
?
后面一個參數是.net語法簡寫的可變參數,在.net內部實際是數組而已,實質還是兩個參數的方法重載。
你也可以不使用這種方法,將字符串相加即可
var msg1 = "Hello Cnblogs, I am " + name + ",Today is " + DateTime.Now.ToString("yyyy-MM-dd") + " " + DateTime.Now.DayOfWeek + ".";
上面兩種方法的結果是一樣的。
?之前普遍使用第一種方法的原因是相比string的多個加號相加在性能上有一定優勢,因為其內部是使用StringBuilder類的,還有一個原因是代碼的可讀性比起+這樣的方式更好一些。
?
分析一下第一種方法的實現原理:
?1.Format方法的內部解析方式和原理
Format方法在取到第一個參數"Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}."之后便將其分解成多個部分
① "Hello Cnblogs, I am "?? ② "{0}"? ③",Today is " ④"{1:yyyy-MM-dd}"⑤ "? " ⑥ "{2}"⑦ "."
分解的原則是按照{}配對的數量進行的,{}是微軟定義好的標記而已,你自己也可以去實現個用 []表示都無所謂。既然{}已經被定義為了特殊的標記,所以如果是自己需要在字符串中包含大括號的話就必須進行轉義,這個轉義也和我們平時使用的"/"轉義表示法不同,需要使用兩個大括號進行轉義如 {{ 或者 }}。 如:
?var msg2 = string.Format("Hello {{}},I am {0}", name);
將{}分解出來之后根據中間的序號來對應第二個參數,如果第二個參數的實際個數小于需要的數量,則會出現運行錯誤(編譯時不會報錯), 如果參數個數大于序號的數量,則其后的忽略不計。
?參數個數小于序號的實際數量,錯誤
var msg4 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.", name, DateTime.Now);
參數個數大于序號的實際數量,多出的參數忽略不計
?var msg4 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd}.", name, DateTime.Now,DateTime.Now.DayOfWeek);
序號的順序不一定必須是0,1,2,3,4可以任意排列,但是序號永遠和第二個參數(實質是數組)的索引一致。
var msg4 = string.Format("Hello Cnblogs, I am {2},Today is {0:yyyy-MM-dd} {1}.", DateTime.Now, DateTime.Now.DayOfWeek, name);
序號還能跳躍,但是中間跳躍過的序號參數里必須有
var msg5 = string.Format("Hello Cnblogs, I am {0},Today is {2:yyyy-MM-dd} {3}.", name, "test", DateTime.Now, DateTime.Now.DayOfWeek);
?上面講了一下用法,接下來繼續
分解完畢之后使用 StringBuilder的Append方法將各個部分添加進去,最后再用ToString方法轉成string,其實現原理非常類似于下面的代碼
var s = new StringBuilder();
??????????? s.Append("Hello Cnblogs, I am ");
??????????? s.Append(name);
??????????? s.Append(",Today is ");
??????????? s.Append(DateTime.Now.ToString("yyyy-MM-dd"));
??????????? s.Append(" ");
??????????? s.Append(DateTime.Now.DayOfWeek);
??????????? s.Append(".");
??????????? var msg3 = s.ToString();
順便解釋一下string和StringBuilder:string雖然也是引用類型,但是該類型.net內部進行了特殊處理,讓其表現出和值類型相似的特征,特別是在每次變動之后就會重新分配內存空間,而StringBuilder就不會,所以如果有很多個字符串相加拼接,則string性能較低。
在用 Append方法進行添加的時候會有兩種情況:
一種是{0},{1}這樣的不帶有特殊格式化的則直接會調用該對象的ToString方法,比如上面的? s.Append(DateTime.Now.DayOfWeek);其實就是 s.Append(DateTime.Now.DayOfWeek.ToString());在.net中,如果是自己定義的類,并且沒有重寫ToString方法,則會輸出類的全名,下面會詳細討論。
另一種是{0:yyyy-MM-dd}帶有特殊格式化的則繼續分解,將冒號后面的內容分解出來,并且在調用ToString時作為參數傳入,上面的s.Append(DateTime.Now.ToString("yyyy-MM-dd"));就體現了這一點。所以這些其實都沒什么奧妙可言,冒號也是一個預定義好的標記而已,如果微軟讓你去實現這個,你也可以用其他符號。
?
2.ToString方法的深入理解
通過第一步的分析如果純粹從分析Format這個方法來說已經足夠了,大括號的特殊標記作用以及和后面參數的對應關系也已經解釋清楚了。但是這里還是需要深入了解一下ToString方法。
上面1中提到如果一個自己定義的類不去重寫ToString方法的話則會 輸出類的全名,例如
public class Person
??? {
??????? public string Name { get; set; }
??? }
如果寫如下代碼
var msg6 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.",
???????????????????????????????????? new Person() {Name = "Zhezhe"}, DateTime.Now, DateTime.Now.DayOfWeek);
??????????? Console.WriteLine(msg6);
則會輸出:
?這里再次強調一下,如果某個對象需要轉換成ToString,并且沒有手動調用該方法,程序會自動調用該方法,上面的new Person() {Name = "Zhezhe"}沒有手工調用,程序會自動調用方法(new Person() {Name = "Zhezhe"}).ToString(); 這個是微軟讓你少些代碼而已,好的習慣是始終寫上 .ToString();
.net中的任何對象都具有該方法,因為該方法在object對象中定義,任何類或者結構都會繼承object,所以不用擔心一個對象沒有ToString方法。
接下來定義帶有ToString重載方法的類
public class PersonWithToString
??? {
??????? public string Name { get; set; }
??????? public override string ToString()
??????? {
??????????? return Name;
??????? }
??? }
編寫如下代碼:
?//使用自己定義類,但是重寫了ToString方法
var msg7 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.", new PersonWithToString(){ Name = "Zhezhe" }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg7);
輸入結果為 輸出就正常了,自己重寫的方法起作用了。
總結:對自己定義的類始終重寫 ToString方法。 這樣在 string.Format 中或者其他需要程序自動轉換成string類型時不會出現 輸出類全名的情況。
?
3.ToString帶有自定義格式化參數的理解
上面講到的ToString都是不帶格式化參數的,像? {1:yyyy-MM-dd} 這樣的情況是沒法處理的,也許有人會說像 DateTime.Now.ToString("yyyy-MM-dd") 這樣的情況自己去重載一個ToString方法就可以了,果真如此嗎? 下面就測試一下
public class PersonWithToString
??? {
??????? public string Name { get; set; }
??????? public override string ToString()
??????? {
??????????? return Name;
??????? }
??????? public string ToString(string format)
??????? {
??????????? switch (format)
??????????? {
??????????????? case "UPP":
??????????????????? return Name.ToUpper();
??????????????? case "LOW":
??????????????????? return Name.ToLower();
??????????????? default:
??????????????????? return Name;
??????????? }
??????? }
??? }
?
var msg9 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.",
????????????????????????????????? new PersonWithToString() { Name = "Zhezhe" }.ToString("UPP"), DateTime.Now, DateTime.Now.DayOfWeek);
??????????? Console.WriteLine(msg9);
msg9的實際輸出為 Hello Cnblogs, I am ZHEZHE,Today is 2010-07-30 Friday.? 這個正是我們需要的,當然,這個肯定是對的,要不然就是.net的bug了
接下來再看看下面的
var msg8 = string.Format("Hello Cnblogs, I am {0:UPP},Today is {1:yyyy-MM-dd} {2}.",
????????????????????????????????? new PersonWithToString() { Name = "Zhezhe" }, DateTime.Now, DateTime.Now.DayOfWeek);
實際輸出是: Hello Cnblogs, I am Zhezhe,Today is 2010-07-30 Friday.? 并不是我們所期望的。實際上上面的代碼是調用了PersonWithToString類的不帶參數的ToString()方法。言外之意就是?? {0:UPP}這樣的格式實際上內部處理的是和? {0}
一樣的效果了。在1中提到了分解的原理用了類似兩個字,實際情況并不是這么簡單。
?? {0:UPP} 真正調用的方法簽名是??? string ToString(string format,IFormatProvider formatProvider)
而且也不是直接調用該對象的此方法。而是通過 IFormattable 接口實現的方法
?現在定義實現了該接口的 Person2類
?
Person2
public class Person2 : IFormattable
??? {
??????? public string Name { get; set; }
??????? public override string ToString()
??????? {
??????????? return Name;
??????? }
??????? #region IFormattable Members
??????? public string ToString(string format, IFormatProvider formatProvider)
??????? {
??????????? if (string.IsNullOrEmpty(format))
??????????????? return ToString();
??????????? switch (format)
??????????? {
??????????????? case "UPP":
??????????????????? return Name.ToUpper();
??????????????? case "LOW":
??????????????????? return Name.ToLower();
??????????????? default:
??????????????????? return Name;
??????????? }
??????? }
??????? #endregion
??? }
?運行一下代碼得到預期的結果
? //使用實現了IFormattable接口的Person2對象
var msg10 = string.Format("Hello Cnblogs, I am {0:UPP},Today is {1:yyyy-MM-dd} {2}.",
new Person2() { Name = "ZhezheToUpper" }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg10);
?ZhezheToUpper已經輸出成全部大寫形式了。
?
既然{0:UPP}會調用接口定義的ToString方法,那么{0}呢? 如果該類沒有實現IFormattable接口,上面已經說了,會調用重載的或者是基類的ToString()方法。但是如果該類已經實現了IFormattable接口,那么{0}也不會去調用重載的或者是基類的ToString()方法了,它始終是去調用 接口定義的 ToString方法。下面具體印證一下
?
Person3
?public class Person3 : IFormattable
??? {
??????? public string Name { get; set; }
??????? public override string ToString()
??????? {
??????????? return Name;
??????? }
??????? #region IFormattable Members
??????? public string ToString(string format, IFormatProvider formatProvider)
??????? {
??????????? if (string.IsNullOrEmpty(format))
??????????????? return Name + " IFormattable Method";
??????????? switch (format)
??????????? {
??????????????? case "UPP":
??????????????????? return Name.ToUpper();
??????????????? case "LOW":
??????????????????? return Name.ToLower();
??????????????? default:
??????????????????? return Name + " IFormattable Method";
??????????? }
??????? }
??????? #endregion
??? }
?
運行下面的測試代碼
var msg11 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.",
????????????????????????????????? new Person3() { Name = "ZhezheToUpper" }, DateTime.Now, DateTime.Now.DayOfWeek);
??????????? Console.WriteLine(msg11);
?
輸出為: Hello Cnblogs, I am ZhezheToUpper IFormattable Method,Today is 2010-07-30 Friday.
證明了確實是調用了接口定義的方法,而不是重載的ToString方法,否則是輸出ZhezheToUpper
再來看一下Person2中實現的ToString方法,
?if (string.IsNullOrEmpty(format))
??????????????? return ToString();
如果是剛才的{0}不帶格式化參數的調用,則format參數傳過來的是null值,這里需要自己判斷,如果是null值,一般情況下是手工去調用重載的ToString()方法。
所以Person2的做法是好的,而Person3中的做法是不好的,Person3只是為了測試分辨出調用的是哪個方法才這么設計的。
?
總結:一.對于實現IFormattable 接口時,如果format參數為null(即不帶格式化參數的情況,如{0})則應該調用重載的 ToString()方法,而不應該自己去另外寫代碼。
二.如果找不到相應的格式化參數,例如{0:AAA},在Person2的switch中并無匹配的AAA,這種情況一般也應該去調用重載的 ToString()方法。
否則就會出現
?//以下兩個輸出結果不一樣,是不合理的
var msg12 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.",new Person3() { Name = "ZhezheToUpper" }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg12);
var msg13 = string.Format("Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.",new Person3() { Name = "ZhezheToUpper" }.ToString(), DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg13);
?
不同的結果的情況
上面的輸出結果不同:
?這是不好的設計
?
?4.繼續了解 IFormatProvider 和 ICustomFormatter 接口
到這里為止,應該說靈活應用string.Format()已經沒什么多大的問題了,但是也還是存在一些問題,比如我們必須得為每個類單獨去實現IFormattable接口才能實現自定義的格式化參數。在一些場后還是覺得不太方便或者說代碼冗余。
.net的string.Format靜態方法還提供了重載方法,具體簽名如下:public static string Format(IFormatProvider provider,string format,params Object[] args)
?
這個方法比起原來使用的方法最前面增加了 IFormatProvider類型參數。使用此方法的優點是不需要為后面的參數對象實現 IFormattable? 接口就可以使用自定義的格式化參數。既然這樣的話也就解決了第4部分開頭提到的問題了。
?
還是用例子說話吧
下面是正方形類
Square類
public class Square
??? {
??????? public string Name { get; set; }
??????? /// <summary>
??????? /// 邊長
??????? /// </summary>
??????? public double Side { get; set; }
??????? public override string ToString()
??????? {
??????????? return string.Format("{0}(Side:{1})",Name, Side);
??????? }
??? }
?
下面是長方形類
Rectangle類
?public class Rectangle
??? {
??????? public string Name { get; set; }
??????? /// <summary>
??????? /// 寬
??????? /// </summary>
??????? public double Width { get; set; }
??????? /// <summary>
??????? /// 高
??????? /// </summary>
??????? public double Height { get; set; }
??????? public override string ToString()
??????? {
??????????? return string.Format("{0}(Width:{1},Height:{2})",Name, Width, Height);
??????? }
??? }
?
?兩個類都重寫了ToString方法
定義MyHelloFormatProvider類,該類從名稱上就可以看出是格式化的提供者
?
?public class MyHelloFormatProvider : IFormatProvider
??? {
??????? #region IFormatProvider Members
??????? public object GetFormat(Type formatType)
??????? {
??????????? return new MyHelloFormatter();
??????? }
??????? #endregion
??? }
?
該類實現了 IFormatProvider 接口,接口只有一個唯一的方法需要實現,GetFormat返回的是真正進行格式化操作的類,這里很像是工廠模式。
返回 MyHelloFormatter 對象之后,在MyHelloFormatter 中具體進行格式化操作。
?public class MyHelloFormatter : ICustomFormatter
??? {
??????? #region ICustomFormatter Members
??????? public string Format(string format, object arg, IFormatProvider formatProvider)
??????? {
??????????? var t = "Hello ";
??????????? switch (format)
??????????? {
??????????????? case "UPP":
??????????????????? t = t.ToUpper();
??????????????????? break;
??????????????? case "LOW":
??????????????????? t = t.ToLower();
??????????????????? break;
??????????????? default:
?????????????????? break;
??????????? }
??????????? return t + arg.ToString();
??????? }
??????? #endregion
??? }
?
?
MyHelloFormatter 實現了ICustomFormatter接口,該接口也只有一個唯一的方法,即實際執行格式化的方法
如果不使用格式化參數或者格式化參數不匹配,情況會怎么樣?
?
代碼
var msg15 = string.Format(new MyHelloFormatProvider(), "{0}? {1}", new Rectangle() { Name = "MyRectangle", Width = 14.3, Height = 10 }, new Square() { Name = "MySquare", Side = 24.2 });
??????????? Console.WriteLine(msg15);
??????????? var msg16 = string.Format(new MyHelloFormatProvider(), "{0}? {1}", new Rectangle() { Name = "MyRectangle", Width = 14.3, Height = 10 }.ToString(), new Square() { Name = "MySquare", Side = 24.2 }.ToString());
??????????? Console.WriteLine(msg16);
??????????? var msg17 = string.Format(new MyHelloFormatProvider(), "{0:AAA}? {1:BBB}", new Rectangle() { Name = "MyRectangle", Width = 14.3, Height = 10 }, new Square() { Name = "MySquare", Side = 24.2 });
??????????? Console.WriteLine(msg17);
?
以上輸出都是一樣的: Hello MyRectangle(Width:14.3,Height:10)? Hello MySquare(Side:24.2)
上面的運行結果表明,如果提供了new MyHelloFormatProvider() ,那么執行過程過是: 根據MyHelloFormatProvider 對象得到 MyHelloFormatter 對象,利用MyHelloFormatter 對象的Format方法進行格式化
這里還有一個問題,如果 MyHelloFormatProvider 的 GetFormat返回的不是一個實現了 ICustomFormatter 接口的對象又會是什么情況呢?
答案是會報異常。 那么如果返回的是 null 呢? 答案是直接調用了對象的ToString()方法了。如果返回null,則運行結果如下:
MyRectangle(Width:14.3,Height:10)? MySquare(Side:24.2)
?
帶上格式化參數的運行結果
?
var msg18 = string.Format(new MyHelloFormatProvider(), "{0:UPP}? {1:LOW}", new Rectangle() { Name = "MyRectangle", Width = 14.3, Height = 10 }, new Square() { Name = "MySquare", Side = 24.2 });
??????????? Console.WriteLine(msg18);
?
?HELLO MyRectangle(Width:14.3,Height:10)? hello MySquare(Side:24.2)
?通過上面的例子我們知道如果我們需要定義一種通用的格式化方式的話,不需要讓類實現 IFormattable 接口,可以通過定義實現 IFormatProvider,ICustomFormatter接口的類去做,上面的無論是正方形還是長方形類都需要在前面加上 Hello 進行格式化,可以是普通的,小寫的,大寫的等等,不需要兩個類單獨去實現了,就選以后增加了圓形,三角形等等,也都能用我們已經定義好的 MyHelloFormatProvider 和 MyHelloFormatter? 去進行格式化。
?
?
使用這種方式還能解決另外一個問題,假如我們已經為圓形類實現了 IFormattable? 接口,并且已經實現了{0:UPP}格式化參數,但是實現的方法中沒有加{0:LOW}格式化參數,而且這個類我們又不能更改(可能是.net自帶的類,可能是第三方dll提供的類等等),那該怎么辦呢? 顯然已經不可能靠IFormattable? 接口來解決了
使用這節講的方法就可以實現我們要求了。以下是具體實現
圓形類
public class Circle : IFormattable
??? {
??????? public string Name { get; set; }
??????? /// <summary>
??????? /// 半徑
??????? /// </summary>
??????? public double Radius { get; set; }
??????? public override string ToString()
??????? {
??????????? return string.Format("{0}(Radius:{1})", Name, Radius);
??????? }
??????? #region IFormattable Members
??????? public string ToString(string format, IFormatProvider formatProvider)
??????? {
??????????? if (string.IsNullOrEmpty(format))
??????????????? return ToString();
??????????? var t = "Hello ";
??????????? switch (format)
??????????? {
??????????????? case "UPP":
??????????????????? t = t.ToUpper();
??????????????????? break;
??????????????? default:
??????????????????? break;
??????????? }
??????????? return t + Name;
??????? }
??????? #endregion
??? }
?
該類可以實現UPP格式化參數的格式化。
?var msg19 = string.Format("Test: {0}", new Circle() { Name = "MyCircle", Radius = 10 });
??????????? Console.WriteLine(msg19);
??????????? var msg20 = string.Format("Test: {0:UPP}", new Circle() {Name = "MyCircle", Radius = 10});
??????????? Console.WriteLine(msg20);
?
運行上面的代碼得到:
Test: MyCircle(Radius:10)
Test: HELLO MyCircle
第一個無格式化參數,實際調用ToString()方法得到,由代碼 if (string.IsNullOrEmpty(format))決定
第二個帶UPP格式化參數,也得到了預期的結果。
?
現在需要實現LOW的格式化參數
var msg21 = string.Format(new MyHelloFormatProvider(),"Test: {0:LOW}", new Circle() { Name = "MyCircle", Radius = 10 });
??????????? Console.WriteLine(msg21);
?
在不修改Circle類并且不重新定義其他類的情況下就可以達到我們的要求了
顯示結果如下: Test: hello MyCircle(Radius:10) hello已經是全部小寫了。
?
?
寫了這么多,感覺有些亂七八糟了,發現還有很多沒有提到,很多都講重復了。本人也難得寫博客,文字水平表達能力欠佳,還望閱者理解。
以下是全部代碼
飛鴿傳書:http://www.freeeim.com/ 徹底學通string.Format以及IFormattable,IFormatProvider,ICustomFormatter。
總結
以上是生活随笔為你收集整理的『飞鸽』彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 整个互联网真的是呈现出一种勃勃的生机
- 下一篇: 163相册密码破解秘诀