最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

第 72 講:C# 2 之泛型(六):常見泛型數(shù)據(jù)類型的使用

2021-12-14 19:30 作者:SunnieShine  | 我要投稿

前面我們介紹了眾多的泛型的相關語法和特性。那么泛型基本上就算是告一段落了。下面我們針對于 C# 2 提供的泛型語法,對 .NET 提供的一些 API 做一個簡要的介紹和使用。

Part 1 泛型類和泛型結構

下面我們來學習一下 .NET 里都有哪些好玩的泛型的 API。

本節(jié)內(nèi)容需要你至少知道或對數(shù)據(jù)結構有一定的了解。如果你對數(shù)據(jù)結構了解不多,或者是完全沒有學過的話,這段內(nèi)容可能會不知所云。

另外,本節(jié)由于篇幅的關系,也只對基本操作做一個介紹。對于復雜的操作,你……還是等著看我的視頻吧。

1-1 List<> 順序表

第一個要說的是 List<> 集合。在之前我們學習了 ArrayList 集合,它類似于一個可變長的序列,因此我們也經(jīng)常稱它為順序表??蛇@個順序表的缺陷在于,它的元素類型是 object 類型接收的,這樣會產(chǎn)生和造成裝箱行為,導致性能的損失。雖然我們也確實知道,早期 C# 沒有泛型機制,所以也不得不這么去處理。但是……自從 C# 2 誕生后,就立馬有了 List<> 集合。該集合依然是順序表,但每一個元素均為同一種數(shù)據(jù)類型,也就是泛型參數(shù)所規(guī)定的、給出的類型了。要說這個集合的好處,自然是用到的泛型機制巧妙地避開了裝箱行為。換句話說,現(xiàn)在有了 List<> 后,ArrayList 基本上可以說是基本完全可以拋棄了,因為 List<> 集合可以完成 ArrayList 所有的基本功能,而且還能做得更好、更棒。

那么,這個集合的基本用法呢?不瞞你說,這個集合的用法和 ArrayList 可以說是基本完全一樣。仍然使用 Add 方法來添加追加元素,Remove 則是刪除元素,RemoveAt 則是刪除指定索引上的元素,也可以使用索引器運算符 [] 來獲取指定索引上的元素。

當然,Count 屬性確實沒有說過,不過也不復雜:它表示該集合的總元素數(shù)量。

1-2 LinkedList<> 鏈表和 LinkedListNode<> 鏈表節(jié)點

是的,學了數(shù)據(jù)結構的人,最開始就會接觸鏈表。鏈表是另外一種線性表的實現(xiàn)類型,它采用指針的形式關聯(lián)相鄰的數(shù)據(jù),而順序表則直接用的是數(shù)組。對于不關心內(nèi)存位置的存儲情況,我們可以使用鏈表來完成元素的存儲和取值,但是相對地,鏈表不如順序表搜索元素來得快:順序表用數(shù)組存儲,那么自然元素的索引取值的時間復雜度就是 %5Ctext%7BO%7D(1),但在鏈表里,時間復雜度則是 %5Ctext%7BO%7D(n)。

使用 AddLast 方法為集合的末尾追加一個節(jié)點。與此同時,鏈表還支持在開頭插入節(jié)點(AddFirst 方法),在指定節(jié)點的前面插入節(jié)點(AddBefore 方法)以及在指定節(jié)點的后面插入節(jié)點(AddAfter 方法)。而我們使用 First 屬性獲取鏈表的頭節(jié)點,而 Last 則是尾節(jié)點。注意,這兩個屬性返回的數(shù)據(jù)類型是 LinkedListNode<> 而不是數(shù)據(jù)本身,因此需要再次對該節(jié)點實例使用 Value 屬性才可以得到最終內(nèi)的數(shù)值。

順帶一說。.NET 里的 LinkedList<> 是雙向循環(huán)鏈表,不是單向鏈表,也不是雙向非循環(huán)鏈表。

1-3 Stack<>

如果你想要完成數(shù)據(jù)結構里的棧,那么很高興的是,.NET 提供了該數(shù)據(jù)結構的泛型版本:Stack<>。

使用 PushPop 方法可以入棧(壓棧)元素和出棧(彈棧)元素,而使用 Peek 方法則可以在不彈棧的情況下獲取棧頂?shù)脑亍?/span>

1-4 Queue<> 隊列

同理,既然都有棧了,自然少不了隊列。.NET 里也有隊列集合 Queue<>。

我們使用 Enqueue 入隊元素,使用 Dequeue 出隊元素。

注意隊列和棧的存儲機制的不同。隊列是先入先出表(FIFO),因此先進去的元素會先被讀取出來;那么出隊的操作和入隊操作并不在同一個方向,因此 Peek 方法的結果是 3,因為 Dequque 方法出隊的是第一個入隊元素 1,因此該集合現(xiàn)在只剩下 3、6、10 三個元素。

1-5 Dictionary<,> 哈希表

哈希表是一個極為復雜的數(shù)據(jù)結構。在學習數(shù)據(jù)結構的時候很多人就會因為這個數(shù)據(jù)結構的理論復雜性而被勸退。還記得哈希碼嗎?是的,哈希碼就是標識同一種數(shù)據(jù)類型的實例的唯一編碼。它好比身份證編號,以保證實例的不一致。

Equals 方法的思路一致,它們都是用來比較相等性的,可 Equals 是嚴格判斷,這導致有些時候速度會很慢。哈希碼可以通過某個稍微簡單一些的公式得到結果。那么同一個數(shù)據(jù)類型的實例自然就會使用同一套計算規(guī)則得到兩個哈希碼數(shù)值。如果哈希碼數(shù)值一致,我們就可以認為兩個實例的數(shù)值是相同的了。

雖然,有些時候哈希碼是自己實現(xiàn)的,也避免不了極端情況(哈希碼相同但實例包含了不同的數(shù)值),但按照規(guī)范去實現(xiàn),往往這樣的情況遇到的概率極低,是人類可以接受的情況。因此,哈希碼機制就保留下來了。

下面要說的哈希表,其實就是利用 GetHashCode 方法來計算對象的數(shù)值,來達到例如對象去重之類的、對對象同一性非常敏感的操作的一種數(shù)據(jù)類型。

要說 Dictionary<,> 的使用方式的話,它其實跟 List<> 也差不太多,也是使用 Add 追加元素,Remove 刪除元素。但請注意的是,哈希表是存儲的一對一對的數(shù)據(jù),而不是一個一個的數(shù)據(jù),這也是為什么,這個數(shù)據(jù)類型的英語單詞用的是 dictionary——因為我們需要按照自定義的索引規(guī)則去獲取里面存儲的元素,這個搜索的過程和查字典的操作相當類似,根據(jù)筆畫、讀音等信息找尋到對應的漢字。

比如這樣的例子。請注意,Dictionary<,> 數(shù)據(jù)類型是兩個泛型參數(shù)的數(shù)據(jù)類型,就跟你根據(jù)拼音查找漢字一樣,拼音是一個數(shù)據(jù)類型,漢字是一個數(shù)據(jù)類型,因此對應到編程的概念里可以類比成 Dictionary<拼音, 漢字> 的感覺:第一個泛型參數(shù)表示索引項目到底是什么類型,而第二個泛型參數(shù)才是對應的取值信息。另外,我們把 Add 方法的第一個參數(shù)(它的類型對應到的是第一個泛型參數(shù))我們稱為(Key),而第二個參數(shù)(它的類型則對應第二個泛型參數(shù))我們則稱為(Value)。也就是說,Add 方法在調(diào)用的時候,'c'、'a''A' 我們都稱為鍵,而 99、9765 我們則稱為值。

不過請你注意一點。字典集合的 Add 操作,鍵是不能重復的。你想想,你查字典的時候,不論是按筆畫還是按拼音去查找,至少筆畫和拼音是不相同的吧,雖然每一個筆畫項或拼音項對應了多個漢字。是的,Dictionary<,> 的鍵也是必須保證全局唯一性:Add 方法調(diào)用多次的時候,必須保證每一次調(diào)用傳入的第一個參數(shù)都是互不相同的數(shù)據(jù)才行。那么,如果我故意或無意傳入相同的鍵呢?當然是拋異常啦。恭喜你 100% 獲得 InvalidOperationException 異常實例一枚。

而索引器,也是針對于鍵來取值的,因此索引器運算符 [] 里寫的是鍵的信息。如果找不到這個鍵的話,就好比你查字典沒有對應的拼音和筆畫一樣,那么自然就會拋異常了。是的,你將 100% 獲得 KeyNotFoundException 異常實例一枚。

1-6 ArraySegment<> 數(shù)組片段

如果你想要獲取數(shù)組集合的一小段數(shù)據(jù)的話,你可以使用 ArraySegment<> 集合來完成。操作很簡單,new 一下就完事了。

我們使用 new ArraySegment<int>(數(shù)組, 從哪個索引開始, 取多少元素) 來完成。比如 arr, 2, 6 三個參數(shù)分別對應從 arr 獲取 6 個元素出來,從第三個元素開始(注意傳參的 2 是表示索引位置是 2,索引是從 0 開始的,所以是第三個元素)。

然后,我們使用 Count 屬性獲取取出來的數(shù)據(jù)片段的元素總數(shù)(6 個),然后遍歷序列。使用索引器來獲取取出來的元素。注意,這里的 ArraySegment<> 類型里的索引器,索引又從 0 開始了,即使你并非從 0 索引開始截取的數(shù)組片段。所以,這個代碼里的 i 從 0 到 5 就恰好對應了原數(shù)組的 arr[2]arr[6] 這 6 個元素。

另外,ArraySegment<> 數(shù)組片段一般用于一維數(shù)組。也就是說,如果是二維數(shù)組甚至更高維度的數(shù)組,就不要用這個類型了。

1-7 PriorityQueue<,> 優(yōu)先級隊列

.NET 6 里誕生了一個新的數(shù)據(jù)類型,叫 PriorityQueue<,>。該數(shù)據(jù)類型和隊列基本上是差不多的,因為它的底層實現(xiàn)就是一個隊列。不過,PriorityQueue<,> 用到了兩個泛型參數(shù)。其中第一個泛型參數(shù)表示隊列里的每一個元素的類型,而第二個泛型參數(shù)則表示的是優(yōu)先級。

優(yōu)先級隊列用于一些復雜的處理過程里,它將一系列的操作存儲到優(yōu)先級隊列之中,并按照指定的優(yōu)先級(第二個泛型參數(shù)給出的這個類型)來排序,并抉擇到底什么內(nèi)容應該優(yōu)先執(zhí)行。那么,優(yōu)先選擇出來的(優(yōu)先級高的)會被提出來先出隊。這個是優(yōu)先級隊列的用法。

不過,對于 C# 語法層面來說很少用到,這里就做一個科普吧,提一嘴。它一般用在很多復雜的多線程處理過程里,比如消息隊列等等。

Part 2 泛型接口

下面我們來說說泛型接口。泛型的接口一般多從普通的接口類型上進行拓展,因此多數(shù)都是我們前文講解過的知識點,只是它變?yōu)榉盒土T了。

2-1 IEquatable<> 接口

還記得之前說的東西嗎?

IEquatable 接口是不存在的。因為按照設計規(guī)則,IEquatable 非泛型接口應當包含一個這樣的方法:

但可以從這樣的設計上看出問題:這個 Equals 方法在最終基類型 object 里就已經(jīng)自帶。所以你設計的所有數(shù)據(jù)類型均全部從 object 類型里派生。不論你是不是重寫了 Equals 方法,該類型都具備 Equals 方法。因此,該接口類型存在與否都沒有任何區(qū)別,所以,.NET 體系里是沒有 IEquatable 接口的。

但是,泛型接口 IEquatable<> 是否可有可無呢?按照這樣的設計規(guī)則,我們應該是這樣的:

可以從這里看到,因為參數(shù)類型從 object 改成了 T,而 T 類型顯然就不是 object,因此是構成重載的。重載意味著兩個方法即使方法名相同,也可以共存。正是因為如此,這個接口就有意義了:因為你 override 掉的是 Equals(object) 這樣簽名的方法,而 Equals(T) 并不存在于你的代碼里,因此這樣的接口是具有約束性的。實際上,.NET 也確實是這樣設計 API 的。

因此,要想強約束一個類型必須包含強類型的 Equals 方法,那么就速速實現(xiàn) IEquatable<> 接口吧!

如代碼所示,這樣是一種實現(xiàn)的大概的代碼寫法。既然你都從 IEquatable<> 接口派生了,那么就相當于說明了類型肯定是具備相等判斷規(guī)則的類型。那么既然如此,你就沒有理由不一起重寫掉 Equals(object) 這個不起眼的比較方法。然后,順帶重載掉運算符 ==!=,以后寫代碼的時候就方便多了。

是的,雖然看似無用的代碼,但是這么寫有一個好處在于,兩種情況的 Equals 都有可能被調(diào)用到,一種是模糊類型校驗,一種是具體類型校驗,它們各有各的好處。

2-2 IComparable<> 接口

之前學習了 IComparable 接口,那么對應的泛型版本自然就是 IComparable<> 接口了。不過,因為原始的接口類型傳入的參數(shù)類型是 object,因此相當不方便?,F(xiàn)在我們有了泛型接口 IComparable<> 后,就可以完美代替掉原來的這個接口類型了。

IEquatable<> 接口不同,由于 IEquatable<> 接口不存在非泛型版本,因此設計上完全不影響;但 IComparable 非泛型接口包含的方法 CompareTo(object) 并非 object 自帶的方法,因此這個非泛型接口類型設計起來是有必要的。emmm……起碼,在非泛型的時代,是有必要的。而在后期,泛型時代來臨,這些非泛型版本就得拋棄了。.NET 在設計 IComparable<> 接口的時候,是這么設計的:

是的,原本 object 參數(shù)類型改成了 T。于是,我們在實現(xiàn)代碼的時候,是這么做的:

如代碼所示,我們多寫一個 CompareTo 方法,替換掉 object 參數(shù)類型的版本即可。然后重載掉運算符 >=、<=><,這樣使用起來更方便一些。

2-3 IEqualityComparer<> 接口

還記得我們以前怎么實現(xiàn)的嗎?我們之前用的是一個方法,傳入的是 object[] 來搞定的。然后再對每一個元素比較,使用非泛型版本的那個接口類型。既然我們有了泛型版本了,那么代碼就可以改了。

是的,所有 object 都可以替代成 T 了。然后,我們就可以開始調(diào)用了。倘若還是用以前的 Student 類型。

然后,創(chuàng)建類型實現(xiàn) IEqualityComparer<> 接口。

是的,清爽多了。這次有了具體類型,我們就不必做那些類型判斷了。

最后,我們開始調(diào)用那個方法。

是的,這次執(zhí)行結果是完全一致的,不過這次要效率高一些,因為沒有冗余的類型判斷,沒有那些莫名其妙的類型轉換機制和操作。

2-4 IComparer<> 接口

是的,這個和非泛型版本也形成了對比。因此我們還是來講一下實現(xiàn)即可。

然后,執(zhí)行排序。

是的,多簡單。

2-5 ICollection<>IReadOnlyCollection<> 接口

要說接口的作用,那么……還記得接口的作用嗎?接口的作用是為了起到成員實現(xiàn)的限制作用,為了能夠讓你能實現(xiàn)這些成員,接口就把你需要實現(xiàn)的成員以名字的形式列舉出來,然后你要想加上 : 接口 的語法,就必須實現(xiàn) 接口 類型里的所有成員。從另外一個角度來說,既然你已經(jīng)實現(xiàn)了該接口,那么基本上就可以認定你能夠做到接口本應該抽象體現(xiàn)出來的事物的基本功能和作用了。

那么,要想了解 ICollection<> 接口,那么必須要看懂這個單詞。collection 這個單詞在編程里是“集合”的意思。所謂的“集合”,就是說明一個數(shù)據(jù)類型,它專門實現(xiàn)出來存儲元素的數(shù)據(jù)信息。而且存儲的數(shù)據(jù)信息還得是一系列的數(shù)據(jù),而不是一個單獨的數(shù)據(jù)。我們看一下 ICollection<> 集合的接口內(nèi)成員都有哪些。

一共 7 個成員,兩個屬性、5 個方法。它們的含義分別是這樣的:

  • Count 屬性:表示集合多少元素;

  • IsReadOnly 屬性:表示集合是不是只讀的(就是說,是不是集合在初始化之后就永不改變里面的數(shù)值,只用來讀取了);

  • Add 方法:往集合追加一個元素進去;

  • Clear 方法:表示將集合的所有已經(jīng)存儲進去的元素全部清除掉;

  • CopyTo 方法:表示將這個集合里的每一個元素往參數(shù) array 里拷貝,就是復制一份副本到參數(shù)這個數(shù)組里去。arrayIndex 表示從第幾個元素開始拷貝;

  • Remove 方法:表示刪除、移除集合里指定數(shù)值的元素。

可以看出,它們都跟增刪改查相關。雖然查找集合序列在這里沒有提及,但 Remove 方法傳入的參數(shù)要一定能從集合里刪除,自然肯定要求底層實現(xiàn)得比較數(shù)據(jù)是不是一樣。那么必然會調(diào)用一些方法,例如 Equals 方法等成員來判別數(shù)據(jù)是否一致,那么自然就相當于是在查找元素了。那么,增刪改查都有了:增加元素、刪除元素、改變數(shù)據(jù)(Clear 清零)、查找數(shù)據(jù)。這就是 ICollection<> 泛型接口的基本用法。

同理,IReadOnlyCollection<> 接口的名字里帶有 read only 一詞的,因此它和 ICollection<> 接口的使用場景的不同在于是不是表示集合只讀。

再次查看 IReadOnlyCollection<> 接口的內(nèi)容,可以發(fā)現(xiàn)它只有一個成員。

是的,這里只有一個 Count 屬性了。如果一個集合包含簽名一致的 Count 屬性,那么就相當于是實現(xiàn)了該接口了。

稍微回憶一下,接口的多態(tài)。如果集合 MyCollection 類型實現(xiàn)了 ICollection<int>IReadOnlyCollection<int> 接口,那么你就可以寫這樣的代碼了:

一旦你使用上了 IReadOnlyCollection<> 接口,那么就說明你該類型只讀了;當然這個是字面意思。如果你在使用的時候,因為多態(tài)性導致你該類型的接收方是用的接口來接收的,說明該集合現(xiàn)在只讀了,因此你僅能使用里面的 Count 屬性,以及 foreach 循環(huán)(foreach 循環(huán)綁定上的是 IReadOnlyCollection<> 接口的基接口 IEnumerable<>IEnumerable 的行為,這個我們稍后說明)。

另外稍微需要你注意的是,ICollection<>IReadOnlyCollection<> 接口是不共通的,因為它們各自派生的關系上,并沒有用到“其中一個接口是另外一個接口的基接口”的情況。因此,它們倆是不共通的。也就是說,你不能把一個 ICollection<> 接口類型的對象賦值給 IReadOnlyCollection<> 接口類型作為接收類型;反之亦然。

可以看到,這兩個接口類型,全部都從 IEnumerable<>IEnumerable 接口派生,但這兩個接口都是什么呢?下面我們就來討論一下。

2-6 IEnumerable<>IEnumerator<> 接口

實際上,這個接口也沒什么好講的。因為在前面基本上也都說過了。在之前講解接口良構類型的時候就說過該接口的用法,并且提到過這樣的內(nèi)容:

“如果一個數(shù)據(jù)類型實現(xiàn)了該接口里面的成員信息的話,我們就可以認為這個接口是可以使用 foreach 循環(huán)的?!?/span>

不過問題在于,foreach 的迭代變量的數(shù)據(jù)類型上。由于早期的接口 IEnumerable 是不帶有泛型的,因此它迭代的每一個元素都會自動被關聯(lián)為 object。也就是說,它基本上在用的過程都等效于這樣的語法:

然后,我們簡單還提過一句。object element 可以替換掉 object,改成你的具體的類型。因此基本上也算是方便了,因為我們期望迭代元素也就只需要讓它能夠寫代碼看起來更加“優(yōu)雅”,而允許 object 自動換成具體的類型這一點來說,就算是比較方便了。

可是,明顯的問題就是裝箱和拆箱操作。如果一個集合我們迭代的每一個元素都是 int 的,結果我又不想去自己實現(xiàn)一個“鴨子類型”來完成成員的具體類型的迭代過程(畢竟,太復雜了),那么我們只能接受裝箱拆箱的操作。還記得鴨子類型吧。鴨子類型說的是一個數(shù)據(jù)類型,一旦滿足一定的條件,即使它不實現(xiàn)接口,也能做一些接口才能做的事情,因為它已經(jīng)被當成能做這個事情的類型了。

C# 2 帶來了接口,就引入了 IEnumerable<> 泛型版本的該接口。于是,它實現(xiàn)起來就比起原本的 IEnumerable 要更好,因為它是泛型的,也就意味著頻繁裝箱拆箱的時代結束了。是的,它的用法基本上和 IEnumerable 沒有任何區(qū)別,唯一的、也是最方便的好處就是它避免了原來類型的 Current 屬性是 object 類型而會導致隱式的裝箱拆箱行為。同時地,在 IEnumerator 接口里,Current 屬性原本是 object 類型的,那么我們想要改成自己的一個具體類型的話,只需要加上泛型參數(shù)的實際類型,就可以了。這樣省得你自己實現(xiàn)具體類型來避免復雜的迭代內(nèi)部機制的實現(xiàn)。

2-7 IList<>IReadOnlyList<> 接口

要想說清楚這個接口類型,我們必須回去看看 List<> 泛型列表類型。這個 List<> 類型包含了眾多的成員,比如 Add 方法啊、Remove 方法啊、Count 屬性之類的。但是,這些方法在實現(xiàn)期間,其實背后是有一個接口約束的。是的,這個接口就是 IList<>。是的,list 單詞的意思是“列表”,因此實現(xiàn)了接口就等同于表示這個自定義的數(shù)據(jù)類型可以做到一個列表該做的基本功能(增刪改查什么的)。

我們來看看 IList<> 接口的基本定義吧。

是的,它里面帶有四個成員:索引器,IndexOf 方法、Insert 方法和 RemoveAt 方法。而它從 ICollection<>、IEnumerable<>IEnumerable 接口派生。其中 ICollection<> 接口有點“大”,因為它里面的成員非常多,一共有 7 個(前面介紹了),而該 IList<> 接口又從 ICollection<> 派生,就意味著你在實現(xiàn)一個集合,從 IList<> 的時候要順帶也把 ICollection<> 里的成員都給實現(xiàn)了。

同理,既然 ICollection<> 都有只讀版的接口 IReadOnlyCollection<> 接口,那么 IList<> 也有對應的只讀版本的接口類型:IReadOnlyList<>。不過,這個接口長這樣:

是的,它走 IReadOnlyCollection<> 接口派生,然后里面包含的是索引器。這意味著,如果你使用的是 IReadOnlyList<> 作為接收類型的話,那么這個類型可以用的三個操作自然就是 foreach 循環(huán)、Count 屬性以及索引器了。

2-8 IDictionary<,>IReadOnlyDictionary<> 接口

IList<> 以及 IReadOnlyList<> 是一樣的存在,這兩個接口是 Dictionary<,> 接口的抽象。我們來大概看看這個接口類型里都有一些什么,就可以了。

是的,很奇怪的是,這兩個接口也并非通用的,但它們里面帶有的成員卻是比較相似,比如索引器、KeysValues 屬性,以及 ContainsKeyTryGetValue 方法,是它們兩個接口都有的成員。其中:

  • 索引器:獲取指定鍵的對應值是什么;

  • Keys 屬性:獲取整個字典序列里的所有已經(jīng)存儲進去的鍵,作為一個集合返回出來;

  • Values 屬性:獲取整個字典序列里的所有已經(jīng)存儲進去的值,作為一個集合返回出來;

  • Add 方法:追加一個鍵值對的數(shù)據(jù)進去,到集合里;

  • ContainsKey 方法:查找字典里是不是包含指定的鍵;

  • Remove 方法:刪除字典里指定鍵的鍵值對信息;

  • TryGetValue 方法:嘗試去獲取指定鍵的對應數(shù)值信息。如果字典里沒有這個鍵的存儲,就返回 false;否則返回 true,并把結果從 value 參數(shù)返回出來。

這個接口也沒啥好說的,因為很少我們會自己實現(xiàn)一個集合去滿足里面的成員。所以細節(jié)上就不多說了。

Part 3 泛型委托

最后,我們來說說泛型委托的內(nèi)容。是的,委托類型也有泛型的 API 提供。而且它們用得相當廣泛。

3-1 ActionFunc 系列泛型委托

下面我們來說一下 ActionFunc 系列委托。為什么說是系列呢?是因為 ActionFunc 并不是單個委托類型,而是包含泛型類型的重載版本。

還記得之前學習的委托類型的用法嗎?委托類型定義了具體的類型和返回值后,只要簽名一樣, 就可以使用 new 委托 的方式把方法賦值過去。不過,如果定義的參數(shù)和返回值有泛型參數(shù)怎么辦呢?是的,這就是我們說的泛型委托的一種特殊用法。

舉個例子,假設一般的定義是這樣的:

這表示一個無參無返回值的委托類型。如果我們替換掉返回值類型:

可以看到,這次我們將返回值替換為了一個泛型參數(shù)。這個情況我們就稱為泛型委托類型。而 ActionFunc 就是如此的泛型委托類型。

3-1-1 Action 系列委托

先來說 Action 系列委托。Action 系列委托一共是 17 個重載版本。長相是這樣的:

數(shù)數(shù)看,是不是 17 個。一個非泛型版本,16 個帶泛型參數(shù)的版本。泛型參數(shù)因為是等效的,因此泛型參數(shù)的重載只存在個數(shù)不同的重載規(guī)則。并且請注意,這 17 個委托類型都是 void 返回值,因此它們都不接受任何返回值類型。

舉個例子,假設我有一個方法 Sort

如果我們想要使用委托類型的話,可以這么寫代碼:

可能你很少見到,int[] 當泛型參數(shù)的實際類型的。實際上,C# 允許這么做。因為它也是 Array 的派生類型,而 int[] 也只是特殊記號罷了,所以沒有道理不允許這么寫。

可以從這里看到,這種代碼寫起來相當方便了。因為委托類型就可以省去很多次的委托類型聲明的語句。比如我們經(jīng)常定義一些奇怪的委托類型?,F(xiàn)在有了 Action 的系列委托,只要你的方法的參數(shù)少于或等于 16 個,都可以直接使用這個東西來完成,就……很方便。

用法和普通的委托類型用法是一樣的:Invoke 調(diào)用即可。

3-1-2 Func 系列委托

前面的委托類型并不能解決返回值不空的情況,因此 .NET 也提供了自定義返回值類型的委托類型,叫 Func。它包含 16 個重載的版本,長這樣:

這樣就可以解決返回值類型的問題了。再舉個例子,加減乘除。

用法也是一樣的。

3-1-3 ActionFunc 系列委托的缺陷

雖然它可以替代絕大多數(shù)我們用得到的委托類型的定義,但仍然有時候無法替代。比如說,參數(shù)帶有修飾符。比如說:

由于泛型參數(shù)帶有 T 修飾符,因此我們無法替換為 ActionFunc 系列委托,因為定義里的所有帶參數(shù)的情況也都沒有 ref 修飾的情況。這種情況下,委托類型只能自己定義。

3-2 Predicate<> 謂詞

雖然,FuncAction 系列委托類型基本上能解決大多數(shù)時候的問題,但有些時候,我們直接使用 ActionFunc 系列委托也不方便,因為寫起來有些長。于是,C# 派生出了兩個特殊的委托類型,這樣用起來方便。一個是 Predicate<>,另外一個是稍后介紹的 Comparison<>。

Predicate<T> 委托類型只帶一個泛型參數(shù) T,而它的簽名基本等價于 Func<T, bool>,即一個方法帶有一個參數(shù)和一個返回值,參數(shù)可替代為任何的類型,而返回值是 bool 類型。從這個簽名可以看出這個 bool 就表示一個條件結果。這就是為什么,這個委托類型被一些資料上稱為“謂詞”。“謂詞”這個說法來自于邏輯學,它指的是一個句子里的謂語動詞,并且整句話能作出判斷的情況。那么抽象為編程語言,不就是一個語句,執(zhí)行出來的結果是 bool 結果嗎?

用法很簡單,因為它和 Func<T, bool> 是一個意思,因此當它這么用就行。有些 API 就會用到這個委托類型,比如前文介紹的 List<> 列表類型,里面自帶了一個叫做 FindAll 方法,它用來找到整個列表里所有滿足指定條件的元素。既然要找到滿足條件的,那么自然就得把每一個元素挨個迭代一次,然后判斷條件,然后條件為 true,就記錄到結果里嗎?所以,它用 Predicate<> 充當條件部分就相當合適。事實上,.NET 也確實是這么設計的:

在使用的時候,可以這么做:

可以從第 6 行代碼看到,它需要一個參數(shù),正是這里的 Predicate<> 類型。而為什么這里沒有寫泛型參數(shù)部分呢?因為 IsOdd 方法是帶有 int 參數(shù)的方法,可以從方法本身推斷和暗示 Predicate<int> 是合適的實例化情況。因此,編譯器允許我們省略 <int> 泛型參數(shù)部分。

3-3 Comparison<> 比較器

和謂詞委托相似的,還有比較器對象。Comparison<T> 委托類型的簽名基本等價于 Func<T, T, int>,也就是傳入兩個 T 類型的參數(shù),并返回 int 結果。試想一下,什么樣的時候,會用到這個情況?

是的,CompareTo 方法的類似邏輯。要想比較兩個對象,然后比較出一個大小,是不是就得這么搞???因為部分的數(shù)據(jù)類型是不支持運算符 >= 這類重載的,因此我們并非所有時候都可以這么簡單使用比較操作。于是,我們有了比較器委托后,就可以簡略很多代碼了。

Predicate<> 委托類型一樣,它也在 .NET 的系統(tǒng)自帶 API 里就有所使用和體現(xiàn)。比如 Array 類型(所有數(shù)組的基類型)就包含一個方法,叫 Sort。它的簽名是這樣的:

是的,兩個參數(shù),沒有返回值。第一個參數(shù)肯定是數(shù)組本身了,因為 Array 類型里基本帶的都是靜態(tài)的成員(當然, Length 屬性就是實例成員,但這樣的成員很少),所以要執(zhí)行操作,比如優(yōu)先考慮把數(shù)據(jù)給傳入到方法里,那么自然就需要占用一個參數(shù)的名額來完成;而第二個參數(shù)就是我們這里所說的比較器委托類型對象了。

用法也很簡單??紤]對字符串排序。我們可以定義字符串的比較方式為比較字符串的長度(為了例子簡單一些,我們這里暫時不考慮比較 ASCII 碼等內(nèi)容)。于是我們可以這么寫代碼:

注意第 11 行代碼,我們需要的兩個參數(shù)是這么寫的。其中第二個參數(shù)我們傳入委托類型的對象,所以需要實例化;帶有泛型的時候,需要同時在實例化的時候傳入合適的泛型參數(shù)。

Part 4 其它問題

下面針對于前文沒有提到的內(nèi)容進行一個問題解答,或者補充。

4-1 沒有泛型特性

是的,你壓根就沒有看錯。我們大家都知道,整個 C# 的派生體系非常龐大,里面還包含了別的數(shù)據(jù)類型,比如特性。雖說特性寫法跟類也沒啥區(qū)別,但是,特性奇怪的點在于,它雖然是個普通的類的實現(xiàn),從 Attribute 抽象類派生,但它并不能是泛型的。

這挺奇怪的。既然是一個普通的類類型,那么為啥它不能是泛型的呢?原因在于,特性在運行期間是作為元數(shù)據(jù)存儲的。還記得元數(shù)據(jù)的基本概念吧。元數(shù)據(jù)指的是一種構建整個程序運行的基本數(shù)據(jù)信息。它們被放在一個特殊的地方,受程序初始化的時候自動初始化,并且永不可修改。

是的,這些數(shù)據(jù)都是實體的數(shù)據(jù)類型(什么必須是 Type 類型啊、基本的內(nèi)置類型啊、一維數(shù)組類型之類的),設定這種限制很明顯是因為,它們是可以在運行前通過編譯器自動計算到指定的數(shù)據(jù)以及存儲的內(nèi)存空間大小,并且丟進元數(shù)據(jù)的存儲內(nèi)存區(qū)域里的。正是因為它們是預先就可以被編譯器處理掉,因此泛型是不允許的:因為泛型受到運行時管理。換句話說,泛型得等到運行時期才可以確定具體的存儲機制(比如內(nèi)存占多大啊,數(shù)值是多少什么的)。所以,泛型在特性里是不允許存在的。

不過,這一點將在 C# 10 里被打破。是的,從 C# 10 開始,你就可以使用泛型特性機制來完成一些奇特的操作了,不過,這一點得等到后面去說。而且,C# 10 的對應 .NET 運行環(huán)境比較高(.NET 6),因此如果你使用的是舊版本的 .NET 框架,說不定就不可以使用這種語法機制了:因為泛型特性除了是語法要支持以外,還得運行環(huán)境自身支持才行。這種新語法就不能隨便引用到項目里,否則你會直接預先收到一條編譯器錯誤信息,告訴你,這樣的程序無法編譯,因為運行時就不支持。

4-2 數(shù)組的接口實現(xiàn)的奇怪現(xiàn)象

IList<>IReadOnlyList<>、ICollection<>IReadOnlyCollection<> 接口是用來表示一個集合的,只是細節(jié)不同。比如說 IList<>IReadOnlyList<> 是表示集合的可列舉性,而 ICollection<>IReadOnlyCollection<> 則更側重于集合的基本實現(xiàn)標準和規(guī)范。

那么,數(shù)組呢?數(shù)組難道就不是集合了?數(shù)組也是集合啊,數(shù)組也是可列舉的啊。那么自然,一個數(shù)組類型也應當實現(xiàn)這些接口類型。可問題來了。ICollection<> 接口里包含了一些比如 Add、Remove 方法的成員,用來增刪數(shù)據(jù)。可數(shù)組呢?數(shù)組是不能增刪的,數(shù)組只能改變里面的數(shù)值,以及查找數(shù)值。那么這個實現(xiàn)機制豈不是太奇怪了?

問題很好。這個現(xiàn)象我們先給出結論吧。數(shù)組實現(xiàn)了這些接口類型,也意味著你可以直接這么寫代碼:

這里的 Array.Empty<int>() 方法是一個泛型方法。它提供一個空數(shù)組。換句話說,它調(diào)用后會產(chǎn)生一個類似于 new int[0] 的數(shù)組,即沒有任何元素的數(shù)組,Length 屬性返回 0。這個方法是泛型方法,意味著你需要傳入一個泛型參數(shù)進去,比如這里的 int 傳入進去,返回的就是 new int[0] 類似的結果;如果是別的數(shù)據(jù)類型,例如表示為 T 的話,那么結果就對應了 new T[0] 里的這個 T。

這個方法比 new T[0] 直接寫要高效,因此我們永遠都建議你使用 Array.Empty<T> 泛型方法來代替掉 new T[0] 語法。

不過,請勿調(diào)用這些接口里有關增刪數(shù)據(jù)的成員,因為它們會導致程序在運行時期拋出 NotSupportedException 異常,告訴你這個集合并不支持這個方法,畢竟,數(shù)組并不支持增刪操作。是的,僅是拋異常而已。

至此,我們就把泛型給全部說完了。當然,泛型的水很深,這一點內(nèi)容還不足以說明清楚更深層次的內(nèi)容,但它們已經(jīng)不屬于教程考慮和討論的范疇了。如果有興趣的話,可以參考《CLR Via C#》之類的書籍,來學習有關泛型的底層實現(xiàn)機制。當然,我也不是一定不考慮講這些內(nèi)容。我只是說本教程不考慮這些。說不定我以后還出一些比如專講底層機制的系列教程呢?


第 72 講:C# 2 之泛型(六):常見泛型數(shù)據(jù)類型的使用的評論 (共 條)

分享到微博請遵守國家法律
微博| 周至县| 清水河县| 昂仁县| 姚安县| 潞西市| 门头沟区| 博客| 左云县| 尤溪县| 三台县| 囊谦县| 黔东| 仲巴县| 江永县| 赤水市| 潮州市| 云安县| 汨罗市| 遂川县| 嘉祥县| 锦州市| 浮山县| 同仁县| 钟祥市| 鄂伦春自治旗| 县级市| 曲阜市| 延吉市| 乐业县| 九江市| 涞源县| 遂宁市| 顺昌县| 原平市| 凌云县| 东源县| 平阴县| 黎城县| 龙海市| 黄冈市|