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命令,但最终哪一个接近得更快?

C# .First() vs [0]

您可以自己检查:

    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.都不一样

尝试通过选择其中一种方法来优化代码显然是过早的优化。