C# Linq-FirstOrDefault VS Array.Find

引言

近日笔者撸代码时候遇到一个如下图的代码建议,Call 'Find' instead of 'FirstOrDefault'。应用建议后,代码变成了这样var s = Array.Find(arr, i => i == "10000000");。于是好奇心上来:建议的代码并没有比笔者手撸的代码更优雅,那么此建议就是处于性能的考虑了。Array.Find的性能真的优于Linq-FirstOrDefault吗?Why?
image

PS:本文中所有代码均为示例代码,他们并不代表什么实际意义,请忽略这一点。

一、谁说的

这建议是谁给的?部分读者可能并没有见到过上面的代码建议,我需要说明一下,代码里的arr是一个数组对象。笔者使用的IDE是Visual Studio Code,给出上述建议的是 Roslynator 扩展。对于此扩展,本文不做介绍。上述建议的依据是RCS1119。从规则中我们可以得到的信息:

  • 此项规则被标记了deprecated。可能已经过时,但是我们先忽略这一点,继续研究
  • Performance:规则分类为性能,印证了笔者的猜想
  • Info:严重级别为Info,会给出代码建议
    image

二、溯源

回到问题的核心,Array.FindLinq-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遍历集合来匹配目标项,返回命中的第一个,或者defaultforeach是基于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");

image

至此我们已经可以回答本文开头的问题了。对于一个Array实例来说,Array.Find性能会略好于FirstOrDefault,但性能提升不明显。
还记得我们在上文中忽略的一个点吗?RoslynatorRCS1119规则标记了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");

image
看到了吗?使用数组下标遍历数组使用foreach遍历集合耗时缩短了将近二分之一。

四、结论

  • 使用数组下标遍历数组实例的性能明显优于使用foreach遍历IEnumerable实例
  • 同样是遍历数组实例使用下标使用foreach差别不大

PS:IEnumerable的优势在于和ORM框架联合使用时候的延迟加载

posted @ 2021-09-09 17:44  Theo·Chan  阅读(363)  评论(0编辑  收藏  举报