Enumerable 下又有新的擴充套件方法啦,快來一起一睹為快吧

一線碼農發表於2020-08-14

一:背景

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#。

如您有更多問題與我互動,掃描下方進來吧~

圖片名稱

相關文章