检查和检索集合的第一项的最佳方法是什么?

时间:2011-03-02 00:08:59

标签: c# linq scope

我知道这有点微不足道但是......

如果存在,那么获取引用的第一项是什么的最佳方法是什么?假设集合包含引用类型的项目。

代码示例1:

if (collection.Any())
{
    var firstItem = collection.First();
    // add logic here
}

上面的示例在集合上有两个单独的调用,开始迭代,一旦检测到第一个就完成迭代。

代码示例2:

var firstItem = collection.FirstOrDefault();
if (firstItem != null)
{
    // add logic here
}

上面的示例只对集合进行了一次调用,但引入了一个不必要地在更广范围内的变量。

是否存在与此方案相关的最佳做法?有更好的解决方案吗?

7 个答案:

答案 0 :(得分:6)

我更喜欢第二个例子,因为它在一般情况下更有效。这个集合可能是许多不同的延迟评估LINQ查询的组合,因此即使获得第一个元素也需要非常重要的工作量。

想象一下,例如,此集合是从以下LINQ查询构建的

var collection = originalList.OrderBy(someComparingFunc);

只获取collection中的第一个元素需要完整的originalList内容。每次评估collection的元素时,都会发生这种完整排序。

第一个示例导致可能昂贵的集合被评估两次:通过AnyFirst方法。第二个样本只评估一次集合,因此我会在第一个样本中选择它。

答案 1 :(得分:3)

你可以创建一个像这样的扩展方法:

public static bool TryGetFirst<T>(this IEnumerable<T> seq, out T value)
{
    foreach (T elem in seq)
    {
        value = elem;
        return true;
    }
    value = default(T);
    return false;
}

然后你会像这样使用它:

int firstItem;
if (collection.TryGetFirst(out firstItem))
{
    // do something here
}

答案 2 :(得分:2)

第二个不适用于非可空值类型(编辑:,如您所假设 - 第一次错过)并且除了第一个之外没有其他选择,竞争条件。有两种选择都是合适的 - 选择一种或另一种取决于你获得空序列的频率。

如果这是一个常见或预期的情况,你得到一个空的枚举,使用foreach循环是相对整洁的:

foreach (var firstItem in collection)
{
    // add logic here
    break;
}

或者如果你真的不想在那里break(这是可以理解的):

foreach (var firstItem in collection.Take(1))
{
    // add logic here
}

如果它是空的相对不常见,那么try/catch块应该提供最佳性能(因为异常只有在它们实际被提升时才是昂贵的 - 一个非增长的例外实际上是免费的):

try
{
    var firstItem = collection.First();
    // add logic here
}
catch (InvalidOperationException) { }

第三种选择是直接使用枚举器,虽然这应该与foreach版本相同,但不太明确:

using (var e = collection.GetEnumerator())
{
    if (e.MoveNext())
    {
        var firstItem = e.Current;
        // add logic here
    }
}

答案 3 :(得分:1)

有时我使用这种模式:

foreach (var firstItem in collection) {
    // add logic here
    break;
}

它只启动一次迭代(因此它比代码示例1更好)并且变量firstItem的范围限制在括号内(因此它比代码示例2更好)。

答案 4 :(得分:1)

或者,作为Gabe解决方案的扩展,让它使用lambda,这样你就可以删除if:

public static class EnumerableExtensions
{
    public static bool TryGetFirst<T>(this IEnumerable<T> seq, Action<T> action)
    {
        foreach (T elem in seq)
        {
            if (action != null)
            {
                action(elem);
            }

            return true;
        }

        return false;
    }
}

并使用它:

     List<int> ints = new List<int> { 1, 2, 3, 4, 5 };

     ints.TryGetFirst<int>(x => Console.WriteLine(x));

答案 5 :(得分:0)

由于所有通用Collections(即:System.Collections.ObjectModel类型}具有Count成员,我的首选方式如下:

Item item = null;
if(collection.Count > 0)
{
    item = collection[0];
}

这是安全的,因为所有集合都将具有CountItem属性。对于阅读代码的任何其他程序员来说,它也非常直接且容易理解你的意图。

答案 6 :(得分:0)

刚刚对原始类型进行了简单测试,看起来您的代码示例#2在这种情况下最快(更新):

[TestFixture] public class SandboxTesting {
  #region Setup/Teardown
  [SetUp] public void SetUp() {
    _iterations = 10000000;
  }
  [TearDown] public void TearDown() {}
  #endregion
  private int _iterations;
  private void SetCollectionSize(int size) {
    _collection = new Collection<int?>();
    for(int i = 0; i < size; i++)
      _collection.Add(i);
  }
  private Collection<int?> _collection;
  private void AnyFirst() {
    if(_collection.Any()) {
      int? firstItem = _collection.First();
      var x = firstItem;
    }
  }
  private void NullCheck() {
    int? firstItem = _collection.FirstOrDefault();
    if (firstItem != null) {
      var x = firstItem;
    }
  }
  private void ForLoop() {
    foreach(int firstItem in _collection) {
      var x = firstItem;
      break;
    }
  }
  private void TryGetFirst() {
    int? firstItem;
    if (_collection.TryGetFirst(out firstItem)) {
      var x = firstItem;
    }
  }    
  private TimeSpan AverageTimeMethodExecutes(Action func) {
    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    // warm up 
    func();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < _iterations; i++) {
      func();
    }
    watch.Stop();
    return new TimeSpan(watch.ElapsedTicks/_iterations);
  }
  [Test] public void TimeAnyFirstWithEmptySet() {      
    SetCollectionSize(0);

    TimeSpan averageTime = AverageTimeMethodExecutes(AnyFirst);

    Console.WriteLine("Took an avg of {0} secs on empty set", avgTime);     
  }
  [Test] public void TimeAnyFirstWithLotsOfData() {
    SetCollectionSize(1000000);

    TimeSpan avgTime = AverageTimeMethodExecutes(AnyFirst);

    Console.WriteLine("Took an avg of {0} secs on non-empty set", avgTime);      
  }
  [Test] public void TimeForLoopWithEmptySet() {
    SetCollectionSize(0);

    TimeSpan avgTime = AverageTimeMethodExecutes(ForLoop);

    Console.WriteLine("Took an avg of {0} secs on empty set", avgTime);
  }
  [Test] public void TimeForLoopWithLotsOfData() {
    SetCollectionSize(1000000);

    TimeSpan avgTime = AverageTimeMethodExecutes(ForLoop);

    Console.WriteLine("Took an avg of {0} secs on non-empty set", avgTime);
  }
  [Test] public void TimeNullCheckWithEmptySet() {
    SetCollectionSize(0);

    TimeSpan avgTime = AverageTimeMethodExecutes(NullCheck);

    Console.WriteLine("Took an avg of {0} secs on empty set", avgTime);
  }
  [Test] public void TimeNullCheckWithLotsOfData() {
    SetCollectionSize(1000000);

    TimeSpan avgTime = AverageTimeMethodExecutes(NullCheck);

    Console.WriteLine("Took an avg of {0} secs on non-empty set", avgTime);
  }
  [Test] public void TimeTryGetFirstWithEmptySet() {
    SetCollectionSize(0);

    TimeSpan avgTime = AverageTimeMethodExecutes(TryGetFirst);

    Console.WriteLine("Took an avg of {0} secs on empty set", avgTime);
  }
  [Test] public void TimeTryGetFirstWithLotsOfData() {
    SetCollectionSize(1000000);

    TimeSpan averageTime = AverageTimeMethodExecutes(TryGetFirst);

    Console.WriteLine("Took an avg of {0} secs on non-empty set", avgTime);
  }
}
public static class Extensions {
  public static bool TryGetFirst<T>(this IEnumerable<T> seq, out T value) {
    foreach(T elem in seq) {
      value = elem;
      return true;
    }
    value = default(T);
    return false;
  }
}

<强> AnyFirst
 NonEmpty:00:00:00.0000262秒
 EmptySet:00:00:00.0000174秒

<强> for循环
NonEmpty:00:00:00.0000158秒
EmptySet:00:00:00.0000151秒

<强> NullCheck
NonEmpty:00:00:00.0000088秒
EmptySet:00:00:00.0000064秒

<强> TryGetFirst
NonEmpty:00:00:00.0000177秒
EmptySet:00:00:00.0000172秒