C# Linq-FirstOrDefault VS Array.Find
引言
近日笔者撸代码时候遇到一个如下图的代码建议,Call 'Find' instead of 'FirstOrDefault'
。应用建议后,代码变成了这样var s = Array.Find(arr, i => i == "10000000");
。于是好奇心上来:建议的代码并没有比笔者手撸的代码更优雅,那么此建议就是处于性能的考虑了。Array.Find
的性能真的优于Linq-FirstOrDefault
吗?Why?
PS:本文中所有代码均为示例代码,他们并不代表什么实际意义,请忽略这一点。
一、谁说的
这建议是谁给的?部分读者可能并没有见到过上面的代码建议,我需要说明一下,代码里的arr
是一个数组对象。笔者使用的IDE是Visual Studio Code
,给出上述建议的是 Roslynator 扩展。对于此扩展,本文不做介绍。上述建议的依据是RCS1119。从规则中我们可以得到的信息:
- 此项规则被标记了
deprecated
。可能已经过时,但是我们先忽略这一点,继续研究 - Performance:规则分类为性能,印证了笔者的猜想
- Info:严重级别为
Info
,会给出代码建议
二、溯源
回到问题的核心,Array.Find
和Linq-FirstOrDefault
谁的性能更好?这俩货都是C#的语法糖,我们先去扒它们的源码。笔者使用的是.NET 5
,很容易就能找到Array.Find
的源码(Line:755),它其实就是用数组下标遍历数组来匹配目标项,返回命中的第一个,或者default
。
public static T? Find<T>(T[] array, Predicate<T> match)
{
if (array == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
}
if (match == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
for (int i = 0; i < array.Length; i++)
{
if (match(array[i]))
{
return array[i];
}
}
return default;
}
接着看Linq-FirstOrDefault
的源码(Line:36&75),它其实就是用foreach
遍历集合来匹配目标项,返回命中的第一个,或者default
。foreach
是基于IEnumerator
的语法糖。
public static TSource? FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) =>
source.TryGetFirst(predicate, out bool _);
...
private static TSource? TryGetFirst<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, out bool found)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (predicate == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
}
foreach (TSource element in source)
{
if (predicate(element))
{
found = true;
return element;
}
}
found = false;
return default;
}
在二者的源码中,Func<TSource, bool>
和Predicate<T>
的源码定义分别是public delegate TResult Func<in T, out TResult>(T arg);
和public delegate bool Predicate<in T>(T obj);
,并且传入的值实际上是同一个对象,差别应该不大。那么我们的问题可以转化为使用foreach
遍历集合和使用数组下标遍历数组的性能差异。
三、测试
在如下的代码中,我们生成了一个包含10,00,000
项的数组,然后分别使用foreach
和数组下标
遍历100
次数组并记录各自耗费的时间。
- 使用
foreach
:耗时707ms - 使用
数组下标
:耗时669ms
对比发现二者性能差异不大,使用数组下标
以极小的优势略好于使用foreach
遍历数组。
var arr = Enumerable.Range(1, 1000000).Select(i => "1").ToArray();
Stopwatch sw = new();
Func<string, bool> predicate = s => s == "2";
sw.Restart();
for (var i = 0; i < 100; i++)
{
foreach (var s in arr)
{
predicate(s);
}
}
sw.Stop();
Console.WriteLine($"foreach loop array:: {sw.ElapsedMilliseconds}ms");
sw.Restart();
for (var i = 0; i < 100; i++)
{
for (var j = 0; j < arr.Length; j++)
{
predicate(arr[j]);
}
}
sw.Stop();
Console.WriteLine($"for loop array:: {sw.ElapsedMilliseconds}ms");
至此我们已经可以回答本文开头的问题了。对于一个Array
实例来说,Array.Find
性能会略好于FirstOrDefault
,但性能提升不明显。
还记得我们在上文中忽略的一个点吗?Roslynator
给RCS1119
规则标记了deprecated
。我推测的原因是,仅当开发者对Array
实例使用FirstOrDefault
的时候才会触发RCS1119
规则,并且此规则给出的建议对性能提升又微乎其微。
那么问题结束了吗?等等,我们的确测试了使用foreach
遍历数组,那么使用foreach
遍历集合呢?我们简单调整一下上文的测试代码,来测试使用foreach
遍历集合和使用数组下标遍历数组的性能差异:
var arr = Enumerable.Range(1, 1000000).Select(i => "1").ToArray();
Stopwatch sw = new();
Func<string, bool> predicate = s => s == "2";
sw.Restart();
for (var i = 0; i < 100; i++)
{
foreach (var l in list)
{
predicate(l);
}
}
sw.Stop();
Console.WriteLine($"foreach loop list:: {sw.ElapsedMilliseconds}ms");
sw.Restart();
for (var i = 0; i < 100; i++)
{
for (var j = 0; j < arr.Length; j++)
{
predicate(arr[j]);
}
}
sw.Stop();
Console.WriteLine($"for loop array:: {sw.ElapsedMilliseconds}ms");
看到了吗?使用数组下标遍历数组比使用foreach
遍历集合耗时缩短了将近二分之一。
四、结论
- 使用数组下标遍历数组实例的性能明显优于使用
foreach
遍历IEnumerable
实例 - 同样是遍历数组实例,使用下标和使用
foreach
差别不大
PS:IEnumerable
的优势在于和ORM框架联合使用时候的延迟加载