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