C# .First() vs [0]
本文关键字:vs First | 更新日期: 2023-09-27 18:20:36
感兴趣的是,方法是否有任何差异
所以,我创建了两个片段。
Snippet A
List<int> a = new List<int>();
a.Add(4);
a.Add(6);
int b = a.First();
和
Snippet B
List<int> a = new List<int>();
a.Add(4);
a.Add(6);
int b = a[0];
在IL我们信任,所以
Snippet A IL
IL_0000: nop
IL_0001: newobj System.Collections.Generic.List<System.Int32>..ctor
IL_0006: stloc.0 // a
IL_0007: ldloc.0 // a
IL_0008: ldc.i4.4
IL_0009: callvirt System.Collections.Generic.List<System.Int32>.Add
IL_000E: nop
IL_000F: ldloc.0 // a
IL_0010: ldc.i4.6
IL_0011: callvirt System.Collections.Generic.List<System.Int32>.Add
IL_0016: nop
IL_0017: ldloc.0 // a
IL_0018: call System.Linq.Enumerable.First
IL_001D: stloc.1 // b
IL_001E: ret
和
Snippet B IL
IL_0000: nop
IL_0001: newobj System.Collections.Generic.List<System.Int32>..ctor
IL_0006: stloc.0 // a
IL_0007: ldloc.0 // a
IL_0008: ldc.i4.4
IL_0009: callvirt System.Collections.Generic.List<System.Int32>.Add
IL_000E: nop
IL_000F: ldloc.0 // a
IL_0010: ldc.i4.6
IL_0011: callvirt System.Collections.Generic.List<System.Int32>.Add
IL_0016: nop
IL_0017: ldloc.0 // a
IL_0018: ldc.i4.0
IL_0019: callvirt System.Collections.Generic.List<System.Int32>.get_Item
IL_001E: stloc.1 // b
IL_001F: ret
Snippet B多出了一个IL命令,但最终哪一个接近得更快?
您可以自己检查:
static void Main()
{
List<long> resultsFirst = new List<long>();
List<long> resultsIndex = new List<long>();
Stopwatch s = new Stopwatch();
for (int z = 0; z < 100; z++)
{
List<int>[] lists = new List<int>[10000];
int temp = 0;
for (int i = 0; i < lists.Length; i++)
lists[i] = new List<int>() { 4, 6 };
s.Restart();
for (int i = 0; i < lists.Length; i++)
temp = lists[i].First();
s.Stop();
resultsFirst.Add(s.ElapsedTicks);
s.Restart();
for (int i = 0; i < lists.Length; i++)
temp = lists[i][0];
s.Stop();
resultsIndex.Add(s.ElapsedTicks);
}
Console.WriteLine("LINQ First() : " + resultsFirst.Average());
Console.WriteLine(Environment.NewLine);
Console.WriteLine("By index : " + resultsIndex.Average());
Console.ReadKey();
}
释放模式下的输出:
LINQ First():367
按索引:84
调试模式下的输出:
LINQ First():401
按索引:177
p.S.
方法First的源代码是:
public static TSource First<TSource>(this IEnumerable<TSource> source)
{
IList<TSource> list = source as IList<TSource>;
if (list != null)
{
if (list.Count > 0)
{
return list[0];
}
}
else
{
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
{
return enumerator.Current;
}
}
}
}
强制转换操作source as IList<TSource>
或创建Enumerator对象很可能是First()
相当慢的原因。
p.S.
考虑到这一点,我不建议总是使用索引器,因为它有时可能会产生可读性较差的代码。通常可读性比微观优化更重要。
例如:
var lastEmployee = employees[employees.Count - 1]; // and even
var lastEmployee = employees[^1]; // C#8
可读性低于:
var lastEmployee = employees.Last();
Enumerable.First
方法定义为
public static TSource First<TSource>(this IEnumerable<TSource> source)
{
if (source == null) throw Error.ArgumentNull("source");
IList<TSource> list = source as IList<TSource>;
if (list != null) {
if (list.Count > 0) return list[0];
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (e.MoveNext()) return e.Current;
}
}
throw Error.NoElements();
}
因此,对于List<T>
,它最终会在进行null检查和强制转换后使用索引器。似乎不多,但当我测试性能时,First
比索引器慢10倍(for
循环,10000 000次迭代,发布版本:First-100 ms,indexer-10 ms)。
通常,具体的类/接口方法应该比通用的型的,数据结构应该考虑其细节。例如,链表不应该提供索引器,因为它不能有效地实现。理想情况下,当每个数据结构能够提供更好的实现时,它将定义一个自己的方法,该方法具有与相应的通用扩展方法相同的签名,编译器将正确处理该方法。这可以被视为专业化,不幸的是,它不像C++模板那样得到很好的支持。Enumerable.First
的实现是"变通方法"而非解决方案的一个很好的例子——它对特定的BCL接口进行了优化,但不能处理自定义数据结构(如链表),该结构可以比使用通用实现更好地提供相同的信息。而Enumerable.Last
的情况更糟。
要继续,如果您针对特定的类/接口进行编程,请尽可能使用它们的方法。如果您是针对标准通用接口进行编程的,那么无论如何,您都没有其他选择(除了定义覆盖标准接口的扩展方法,但这通常会导致冲突)。
在LINQPad 5中测试,代码如下:
var sw = Stopwatch.StartNew();
for(int i = 0; i < 1000000000; i++)
{
List<int> a = new List<int>();
a.Add(i);
a.Add(i+2);
int b = a.First();//[0] for B
}
sw.Stop();
Console.WriteLine(sw.ElapsedTicks);
CCD_ 9经优化分别为01:04.021和0:45.794。[0]
给出了0:44.288,0:27.968的优化和更好的代码,正如我所认为的。
实际上,对我来说,[0]
比.First()
更可读,通常我不需要他提供的支票。所以,在大多数情况下,我会选择[0]
。谢谢
如果你问渐近复杂性,两种方法都是O(1)
,请使用其中的任何一种
如果你问的是实际速度,没有答案,因为它可能因版本而异,因机器而异。您生成的IL与任何其他版本的.NET.都不一样
尝试通过选择其中一种方法来优化代码显然是过早的优化。