一:背景
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 Append(this IEnumerable source, TSource element)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
AppendPrependIterator appendPrependIterator = source as AppendPrependIterator;
if (appendPrependIterator != null)
{
return appendPrependIterator.Append(element);
}
return new AppendPrepend1Iterator(source, element, appending: true);
}
private class AppendPrepend1Iterator : AppendPrependIterator
{
public AppendPrepend1Iterator(IEnumerable 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
, 这里大家要着重看一下 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 collection = arr;
foreach (var item in arr2)
{
collection = collection.Append(item);
}
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
结果也是非常简单的,因为 IEnumerable 是非破坏性的操作,所以你需要在 Append 之后用类型给接住,接下来找一下底层源码。
public static IEnumerable Append(this IEnumerable source, TSource element)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
AppendPrependIterator appendPrependIterator = source as AppendPrependIterator;
if (appendPrependIterator != null)
{
return appendPrependIterator.Append(element);
}
return new AppendPrepend1Iterator(source, element, appending: true);
}
private class AppendPrepend1Iterator : AppendPrependIterator
{
public override AppendPrependIterator Append(TSource item)
{
if (_appending)
{
return new AppendPrependN(_source, null, new SingleLinkedNode(_item).Add(item), 0, 2);
}
return new AppendPrependN(_source, new SingleLinkedNode(_item), new SingleLinkedNode(item), 1, 1);
}
}
private class AppendPrependN : AppendPrependIterator
{
public override AppendPrependIterator Append(TSource item)
{
SingleLinkedNode appended = (_appended != null) ? _appended.Add(item) : new SingleLinkedNode(item);
return new AppendPrependN(_source, _prepended, appended, _prependCount, _appendCount + 1);
}
}
从上面的代码可以看出,当你 Append 多次的时候,本质上就是多次调用 AppendPrependN
,而且在调用的过程中,一直将你后续添加的元素追加到 SingleLinkedNode
单链表中,这里要注意的是 Add 采用的是 头插法,所以最后插入的元素会在队列头部,如下图:
如果你不信的话,我可以在 vs 调试中给您展示出来。
貌似说的有点啰嗦,最后大家观察一下 AppendPrependN
的实现就可以了。
说了这么多,我想你应该明白了哈。
2. Prepend
本质上来说 Prepend 和 Append 是一对的,一个是在前面插入,一个是在后面插入,不要想歪了,如果你细心的话,你会发现 Prepend 也是用了这三个类: AppendPrepend1Iterator
以及 单链表 SingleLinkedNode
,这个就留给大家自己研究了哈。
3. ToHashSet
我以前在全内存开发中会频繁的用到 HashSet,毕竟它的时间复杂度是 O(1)
,而且在 Enumerable 中早就有了 ToList 和 ToDictionary,凭啥没有 ToHashSet,在以前只能将 source 塞到 HashSet 的构造函数中,如: new HashSet
,想想也是够奇葩的哈,而且我还想吐糟一下的是居然到现在还没有 AddRange 批量添加方法,气人哈,接下来用 ILSpy 看一下这个扩展方法是如何实现的。
三: 总结
总体来说这三个方法还是很实用的,我相信在后续的版本中 Enumerable 下的扩展方法还会越来越多,越来越人性化,人生苦短, 我用C#。