一:背景
1. 講故事
前段時間將公司的一個專案從 4.5 升級到了 framework 4.8 ,編碼的時候發現 Enumerable 中多了三個擴充套件方法: Append, Prepend, ToHashSet
,想必玩過jquery的朋友一眼就能看出這三個方法的用途,這篇就和大家一起來聊聊這三個方法的底層原始碼實現,看有沒有什麼新東西可以挖出來。
二:Enumerable 下的新擴充套件方法
1. Append
看到這個我的第一印象就是 Add
方法, 可惜在 Enumerable 中並沒有類似的方法,可能後來程式設計師在這塊的呼聲越來越高,C#開發團隊就彌補了這個遺憾。
<1> 單條資料的追加
接下來我寫一個小例子往集合的尾部追加一條資料,如下程式碼所示:
static void Main(string[] args)
{
var arr = new int[2] { 1, 2 };
var result = Enumerable.Append(arr, 3);
foreach (var item in result)
{
Console.WriteLine(item);
}
}
邏輯還是非常清晰的,再來看看底層原始碼是怎麼實現的。
public static IEnumerable<TSource> Append<TSource>(this IEnumerable<TSource> source, TSource element)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
AppendPrependIterator<TSource> appendPrependIterator = source as AppendPrependIterator<TSource>;
if (appendPrependIterator != null)
{
return appendPrependIterator.Append(element);
}
return new AppendPrepend1Iterator<TSource>(source, element, appending: true);
}
private class AppendPrepend1Iterator<TSource> : AppendPrependIterator<TSource>
{
public AppendPrepend1Iterator(IEnumerable<TSource> source, TSource item, bool appending) : base(source)
{
_item = item;
_appending = appending;
}
public override bool MoveNext()
{
switch (state)
{
case 1:
state = 2;
if (!_appending)
{
current = _item;
return true;
}
goto case 2;
case 2:
GetSourceEnumerator();
state = 3;
goto case 3;
case 3:
if (LoadFromEnumerator())
{
return true;
}
if (_appending)
{
current = _item;
return true;
}
break;
}
Dispose();
return false;
}
}
從上面的原始碼來看,這玩意做的還是挺複雜的,繼承關係依次是: AppendPrepend1Iterator<TSource> -> AppendPrependIterator<TSource> -> Iterator<TSource>
, 這裡大家要著重看一下 MoveNext()
裡面的兩個方法 GetSourceEnumerator() 和 LoadFromEnumerator(),如下程式碼所示:
可以看到,第一個方法用於獲取 Array 這個資料來源,下面這個方法用於遍歷這個 Array,當 foreach 遍歷完之後,執行 case 3 語句,也就是下面的 if 語句,將你追加的 3 迭代一下,如下圖:
<2> 批量資料的追加
我們知道集合的新增除了 Add 還有 AddRange,很遺憾,Enumerable下並沒有找到類似的 AppendRange 方法,那如果要實現 AppendRange 操作該怎麼處理呢? 哈哈,只能自己 foreach 迭代啦,如下程式碼:
static void Main(string[] args)
{
var arr = new int[2] { 1, 2 };
var arr2 = new int[3] { 3, 4, 5 };
IEnumerable<int> collection = arr;
foreach (var item in arr2)
{
collection = collection.Append(item);
}
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
結果也是非常簡單的,因為 IEnumerable 是非破壞性的操作,所以你需要在 Append 之後用型別給接住,接下來找一下底層原始碼。
public static IEnumerable<TSource> Append<TSource>(this IEnumerable<TSource> source, TSource element)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
AppendPrependIterator<TSource> appendPrependIterator = source as AppendPrependIterator<TSource>;
if (appendPrependIterator != null)
{
return appendPrependIterator.Append(element);
}
return new AppendPrepend1Iterator<TSource>(source, element, appending: true);
}
private class AppendPrepend1Iterator<TSource> : AppendPrependIterator<TSource>
{
public override AppendPrependIterator<TSource> Append(TSource item)
{
if (_appending)
{
return new AppendPrependN<TSource>(_source, null, new SingleLinkedNode<TSource>(_item).Add(item), 0, 2);
}
return new AppendPrependN<TSource>(_source, new SingleLinkedNode<TSource>(_item), new SingleLinkedNode<TSource>(item), 1, 1);
}
}
private class AppendPrependN<TSource> : AppendPrependIterator<TSource>
{
public override AppendPrependIterator<TSource> Append(TSource item)
{
SingleLinkedNode<TSource> appended = (_appended != null) ? _appended.Add(item) : new SingleLinkedNode<TSource>(item);
return new AppendPrependN<TSource>(_source, _prepended, appended, _prependCount, _appendCount + 1);
}
}
從上面的程式碼可以看出,當你 Append 多次的時候,本質上就是多次呼叫 AppendPrependN<TSource>.Append()
,而且在呼叫的過程中,一直將你後續新增的元素追加到 SingleLinkedNode
單連結串列中,這裡要注意的是 Add 採用的是 頭插法,所以最後插入的元素會在佇列頭部,如下圖:
如果你不信的話,我可以在 vs 除錯中給您展示出來。
貌似說的有點囉嗦,最後大家觀察一下 AppendPrependN<TSource>.MoveNext
的實現就可以了。
說了這麼多,我想你應該明白了哈。
2. Prepend
本質上來說 Prepend 和 Append 是一對的,一個是在前面插入,一個是在後面插入,不要想歪了,如果你細心的話,你會發現 Prepend 也是用了這三個類: AppendPrepend1Iterator<TSource>,AppendPrependIterator<TSource>,AppendPrependN<TSource>
以及 單連結串列 SingleLinkedNode<TSource>
,這個就留給大家自己研究了哈。
3. ToHashSet
我以前在全記憶體開發中會頻繁的用到 HashSet,畢竟它的時間複雜度是 O(1)
,而且在 Enumerable 中早就有了 ToList 和 ToDictionary,憑啥沒有 ToHashSet,在以前只能將 source 塞到 HashSet 的建構函式中,如: new HashSet<int>(source)
,想想也是夠奇葩的哈,而且我還想吐糟一下的是居然到現在還沒有 AddRange 批量新增方法,氣人哈,接下來用 ILSpy 看一下這個擴充套件方法是如何實現的。
三: 總結
總體來說這三個方法還是很實用的,我相信在後續的版本中 Enumerable 下的擴充套件方法還會越來越多,越來越人性化,人生苦短, 我用C#。