C#8.0本質論第十七章–構建自定義集合
17.1更多集合接口
17.1.1IList< T >和IDictionary< TKey , TValue >
這兩個接口決定了集合類型是側重于通過位置索引來獲取值,還是側重于通過鍵來獲取值。
實現這兩個接口的類都必須提供索引器。
17.1.2ICollection< T >
IList< T >和IDictionary< TKey , TValue >都實現了ICollection< T >
17.2主要集合類
17.2.1列表集合:List< T >
List< T >類的性質和數組相似,關鍵區別就是隨著元素增多,這種類會自動擴展。此外,列表可通過顯示調用TrimToSize()或Capacity來縮小。
**IComparable< T >和IComparer< T >**的區別很細微,卻很重要。前者說“我知道如何將我自己和我的類型的另一個實例進行比較”,后者說“我知道如何比較給定類型的兩個實例”。
using System;
using System.Collections.Generic;
// ...
public class Contact
{public string FirstName { get; private set; }public string LastName { get; private set; }public Contact(string firstName, string lastName){this.FirstName = firstName;this.LastName = lastName;}
}
public class NameComparison : IComparer<Contact>
{public int Compare(Contact? x, Contact? y){if(Object.ReferenceEquals(x, y))return 0;if(x == null)return 1;if(y == null)return -1;int result = StringCompare(x.LastName, y.LastName);if(result == 0)result = StringCompare(x.FirstName, y.FirstName);return result;}private static int StringCompare(string? x, string? y){if(Object.ReferenceEquals(x, y))return 0;if(x == null)return 1;if(y == null)return -1;return x.CompareTo(y);}
}
17.2.2全序
實現IComparable< T >和IComparer< T >時必須生成一個全序(total order),必須為任何可能的數據項排列組合提供一致的排序結果。例如上面代碼中連實參是null的情況都考慮到了,不能任何一個元素為null就返回0,否則可能出現兩個非null元素等于null但不相等的情況。
17.2.3搜索List< T >
可以使用Contains(),Indexof(),LastIndexOf()和BinarySearch()方法。
BinarySearch()要求有序,如果沒有找到,會返回一個負整數。該值的取反結果是“大于被查找元素的下一個元素”的索引。沒有更大的則是元素總數。
17.2.4字典集合:Dictionary< TKey , TValue >
可利用Keys和Values屬性只處理字典類中的鍵或值。返回ICollection< T >類型,返回的是對原始字典集合中的數據的引用,而不是返回拷貝。
17.2.5已排序集合:SortedDictionary< TKey , TValue >和SortedList< T >
元素是按照鍵排序的
17.2.6棧集合:Stack< T >
17.2.7隊列集合:Queue< T >
17.2.8鏈表:LinkedList< T >
鏈表集合,允許正向和反向遍歷。(所以是雙向鏈表)
17.3提供索引器
數組,字典和列表都提供了索引器(indexer)以便根據鍵或索引來獲取/設置成員。
interface IPair<T>
{T First { get; }T Second { get; }T this[PairItem index] { get; }
}public enum PairItem
{First,Second
}public struct Pair<T> : IPair<T>
{public Pair(T first, T second){First = first;Second = second;}public T First { get; }public T Second { get; }public T this[PairItem index]{get{switch (index){case PairItem.First:return First;case PairItem.Second:return Second;default:throw new NotImplementedException($"The enum { index.ToString() } has not been implemented");}}}
}
索引器的聲明和屬性很相似,但不是使用屬性名,而是使用關鍵字this,后跟方括號中的參數列表。主題也像屬性,有get和set塊。索引可獲得多個參數,甚至可以重載。
17.4返回null或者空集合
返回數組和集合時允許返回null,更好的選擇是返回不含任何數據項的集合實例。可避免強迫調用者在便利集合前檢查null值。
17.5迭代器
本節討論如何利用迭代器(iterator)為自定義集合實現自己的IEnumerator< T >,IEnumerable< T >和對應的非泛型接口。迭代器使集合的用戶能遍歷集合的內部結構,同時不必了解結構的內部實現。
類要支持用foreach進行迭代,就必須實現枚舉數(enumerator)模式,如第15章所述,C#的foreach循環結構被編譯器擴展成while循環結構,它以從IEnumerable< T >接口獲取的IEnumerator< T >接口為基礎。
17.5.1定義迭代器
17.5.2迭代器語法
迭代器提供了迭代器接口(IEnumerable< T >和IEnumerator< T >)的一個快捷實現。
using System.Collections;
using System.Collections.Generic;public class BinaryTree<T> : IEnumerable<T>
{public BinaryTree(T value){Value = value;}#region IEnumerable<T>public IEnumerator<T> GetEnumerator(){// ...}#endregion IEnumerable<T>public T Value { get; }public Pair<BinaryTree<T>> SubItems { get; set; }
}public struct Pair<T>
{public Pair(T first, T second) : this(){First = first;Second = second;}public T First { get; }public T Second { get; }
}
要為GetEnumerator()提供一個實現。
17.5.3從迭代器生成值
迭代器類似于函數,但它不是返回(return)一個值,而是**生成(yield)**一系列值。
每次迭代器遇到yield return語句都生成一個值,之后控制立即回到請求數據項的調用者。當調用者請求下一項時,慧緊接著在上一個yield return語句之后執行。
17.5.4迭代器和狀態
GetEnumerator()在foreach語句中被首次調用時,慧創建一個迭代器對象,其狀態被初始化為特殊的“起始”狀態,表示迭代器尚未執行代碼,所以尚未生成任何值。
只要foreach繼續,迭代器就會一直持續其狀態。循環每一次請求下一個值,控制就會一直維持其狀態。循環每一次請求下一個值,控制就會進入迭代器,從上一次離開的位置繼續。該位置是根據迭代器對象中存儲的狀態信息來判斷的。foreach終止,迭代器的狀態就不再保存了。
17.5.5更多的迭代器例子
17.5.6將yield return語句放到循環中
17.5.7取消更多的迭代:yield break
可以使用yield break使MoveNext()返回false,使控制立即回到調用者并終止循環。
C#編譯器遇到一個迭代器時,會根據枚舉數模式將代碼展開成恰當的CIL,在生成的CIL代碼中,C#編譯器首先創建一個嵌套的私有類來實現IEnumerator< T >接口,以及它的Current熟悉和MoveNext()方法。Current屬性返回與迭代器的返回類型對應的一個類型。
using System;
using System.Collections;
using System.Collections.Generic;
// ...[NullableContext(1)][Nullable(0)]public struct Pair<[Nullable(2)] T> : IPair<T>, IEnumerable<T>, IEnumerable{public Pair(T first, T second){First = first;Second = second;}public T First { get; }public T Second { get; }public T this[PairItem index]{get{PairItem pairItem = index;PairItem pairItem2 = pairItem;T result;if (pairItem2 != PairItem.First){if (pairItem2 != PairItem.Second){throw new NotImplementedException(string.Format("The enum {0} has not been implemented", index.ToString()));}result = Second;}else{result = First;}return result;}}public IEnumerator<T> GetEnumerator(){yield return First;yield return Second;yield break;}IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}}
}
yield關鍵字是上下文關鍵字,不是保留關鍵字。所以可以合法地聲明名為yield的局部變量。
事實上,C#1.0之后加入的所有關鍵字都是上下文關鍵字,這是為了防止升級老程序來使用語言的新版本時出問題。
17.5.8在一個類中創建多個迭代器
17.5.9yield語句的要求
只有在返回IEnumerator< T >或者IEnumerable< T >類型的成員中,才能使用yield return語句。
主體包含yield return語句的成員不能包含簡單return語句。
yield語句只能在方法,用戶自定義操作符或者索引器/屬性的get訪問器方法中出現。成員不可獲取任何ref或者out參數。
yield語句不能在匿名方法或Lambda表達式中出現。
yield語句不能在try語句的catch和finally塊中出現。此外,yield語句在try塊中出現的前提是沒有catch塊。