第 52 講:類型良構規(guī)范(二):`IFormattable` 接口
在說完前文給定的這些基本內容后,我們應該對面向對象有了一個全新而又陌生的認識。這些內容對于我們來說不得不去接受,即使它比較多。以后我們會在程序項目里,或者在你自己的程序里使用到它們。為了以后寫出來的代碼更具有可讀性,我們才有了這個篇章的內容。
本篇章的內容還有非常多其它的東西,除了基本規(guī)范外,我們還要對一些 C# 的庫里自帶的數據類型(特別是接口)作出一定程度的介紹,比如
IFormattable
和IFormatProvider
IEquatable
、IComparable
和IEqualityComparer
IDisposable
IEnumerable
和IEnumerator
在這些接口內容全部介紹了之后,我們還會給大家介紹 C# 相關的 SOLID 實現原則,以及設計模式,這對我們以后寫代碼都會有相當大的幫助。
Part 1 自定義格式化處理
考慮使用 Console.WriteLine
和 string.Format
方法的時候,我們會在最開始的第一個參數里傳入帶有一定數量占位符的模式字符串。后面的參數都是在補充說明占位符的輸出效果,以及填充位、格式等信息。比如說之前說到的:{0:f,10}
這樣的字符串,表達的是第 1 個占位符,占 10 個字符空間的顯示長度,并以 "f"
作為格式化字符串來格式化處理數據。
如果我們定義了一個自己的數據類型的話,我們必然會需要自己對它實現格式化輸出的效果,但它和整數這些數據類型不同,它不是系統自帶的數據類型,因此我們不能直接在網上查資料就可以學會。因此,這里我們要帶給大家的是一個和格式化輸出字符串有關系的接口:IFormattable
接口。
假設現在我們設計了一個數據類型叫做“溫度”。這個類型可以以一個數值的形式表示一個溫度的數值,并根據溫度的單位呈現不同的溫度結果,比如攝氏度、開爾文熱力學溫度(開氏度)和華氏度。
假設,我們允許用戶輸入一個攝氏度的溫度數值,然后存儲進去。然后我們可以通過調用 Fahrenheit
屬性獲取華氏度,或者調用 Kelvin
屬性獲取開氏度。
思考一點。假設我想要通過 ToString
獲取字符串結果,我們目前能夠得到的只能是攝氏度的溫度結果。但是,我想通過輸出字符串的方式,不同的單位指示,可以有不同的字符串結果顯示和輸出。最簡單的辦法是使用枚舉類型。
ToString
方法,并多傳入一個 TemperatureUnit
比如上面這樣的代碼,這種感覺。不過,這樣的代碼不夠靈活,因為我們指定的枚舉類型本身其實指代的是溫度的單位,在這個例子貌似是奏效的;但是換一個例子的話,單獨使用枚舉來表達格式的話,就顯得差點意思。所以,最實在的其實是字符串,這樣也可以省略定義枚舉類型的時間。
那么字符串我們可以這么做。
確實我們也不需要刻意去改變哪里。這樣我們就可以通過這個方法來獲取信息了:
Part 2 更進一步
這樣可以解決很大一部分的問題,不過……按道理來說,既然有了這樣的處理機制后,這個類型應該是有辦法自己處理格式化字符串了,不過試試這個代碼:
注意這里我們使用的是 string.Format
方法,傳入的模式字符串是 "{0:F}"
,其中的 "F"
就是我們在 ToString
重載方法里給出的這個處理了的格式化字符串(即 case "F"
里的這個 "F"
)。按道理,因為我們自己實現了這個方法,應該是有辦法處理格式化字符串了,但……實際上你在程序運行后看到的結果仍然是 23,而不是華氏度的結果 73.4(至于怎么得到的 73.4,最開始的那個類型設計里,Fahrenheit
屬性是給出了公式的,這個就自己去算了)。
問題出在哪里呢?出在它并沒有真正處理格式化字符串,而是從我們自身的角度出發(fā)的、得到的計算公式。因為我們知道調用這個重載方法就可以得到對應結果了,但機器本身是不知道的。
這可怎么辦呢?別著急,C# 提供了一個手段可以解決這個問題,那就是 IFormattable
接口。如果任何一種數據類型能夠實現這個接口,那么這個類型就可以這么使用代碼,去得到正確結果。
不過,你會發(fā)現,IFormattable
接口要求你實現一個方法,也叫 ToString
,但帶有兩個參數,一個還是 string
類型的格式化字符串(參數名是 format
),而另外一個參數,卻是一個新的接口類型的對象(參數名是 formarProvider
)。這個第二個參數的類型是 IFormatProvider
??催@個名字好像一點用都沒有,我們也沒有接觸過這個接口類型。實際上,規(guī)范化的設計里,這個類型是用來表示一個專門的類型,這個類型用來專門提供和生成格式化字符串,并提供給別的類型使用的。
舉個例子,假設我有一個 Temperature
類型表示溫度,因為它的格式化字符串不同可以輸出不同的結果,于是我們可能會考慮使用重載來搞定。不過,規(guī)范化的設計里我們是需要再單獨給 Temperature
類型創(chuàng)建一個叫 TemperatureFormatProvider
的類型,這個類型專門用來生成和產生格式化字符串,以便和避免用戶因為不懂格式化字符串而導致無法選擇,進而產生調用的異常(格式化字符串錯誤之類的)。但是,從這個說法上我們可以看出,實際上 Temperature
類型完全不需要這個所謂的 TemperatureFormatProvider
類型,因為格式化字符串就只有 "F
"、"K"
和 "C"
三種,就沒有別的了。因此,用戶自己去記住它們就行了,完全沒有必要單獨設計一個新的類型來幫助用戶得到格式化字符串。
正是因為如此,我們完全可以不必管第二個參數 IFormatProvier formatProvider
。但是,我們?yōu)榱耸褂蒙仙厦嫖覀兊哪繕斯δ?,我們可以考慮這么去實現:
是的,代碼直接從原來那個方法搬過來就行,而第二個參數我們直接不使用。
順帶一提。參數的參數名不需要和基類型或者接口里的這個方法完全一樣。一般來說這個是必須要一樣的,但是其實可以改名字的。
嗯,既然代碼是復制粘貼過來的,那么原來的單參數的 ToString
方法我們需要怎么去改變呢?現在我們有三個 ToString
方法了,那么這三個方法的代碼這樣寫比較合適:
我們直接通過使用 ToString(null, default(IFormatProvider))
或 ToString(fprmat, default(IFormatProvider))
ToString
方法就可以了。至于第二個參數,我們傳什么數值進去其實都無所謂,因為方法里壓根沒用到。這個時候我們一般寫成 null
或者 default(IFormatProvider)
。
null
呢,是所有引用類型的默認值,但是萬一我們這個參數是值類型的,習慣性地傳入 null
可能會產生編譯器錯誤,告訴你參數類型不匹配。所以得具體使用的時候要注意。
default(IFormatProvider)
呢,代碼略長,但是更嚴謹一點。反正這個參數我們也沒有用到,那干脆為了占一個參數的位置,總不能啥數據都不寫吧。這里我們就寫一個 default
表達式來表達這里參數我們是傳的默認數值進去。這表示這個參數的數據本身是沒有什么特殊意義的數據。
有了這樣的實現后,我們就可以運行程序了。可以看到程序運行結果確實是從 "23.00 °C"
變成了 "73.40 °F"
了,任務我們就算完成了。
Part 3 來看下完整的實現
下面我們來看下整個完整的實現吧。