Каков наилучший способ проверить и получить первый элемент коллекции? - PullRequest
8 голосов
/ 02 марта 2011

Я понимаю, что это несколько тривиально, но ...

Какой лучший способ получить ссылку на первый элемент коллекции, если таковой существует? Предположим, что коллекция содержит элементы ссылочного типа.

Пример кода 1:

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

Приведенный выше пример имеет два отдельных вызова коллекции, начиная итерацию, которая завершается, как только обнаруживается первая.

Пример кода 2:

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

Приведенный выше пример имеет только один вызов для коллекции, но вводит переменную, которая неоправданно расширяется.

Есть ли передовые практики, связанные с этим сценарием? Есть ли лучшее решение?

Ответы [ 7 ]

6 голосов
/ 02 марта 2011

Я предпочитаю второй пример, потому что он более эффективен в общем случае. Возможно, что эта коллекция представляет собой комбинацию множества различных LINQ-запросов, оцениваемых по задержке, так что даже получение первого элемента требует нетривиального объема работы.

Представьте, например, что эта коллекция построена из следующего запроса LINQ

var collection = originalList.OrderBy(someComparingFunc);

Чтобы получить только первый элемент из collection, требуется полная сортировка содержимого originalList. Эта полная сортировка будет происходить каждый раз, когда оцениваются элементы collection.

В первом примере потенциально дорогая коллекция оценивается дважды: с помощью методов Any и First. Второй пример оценивает коллекцию только один раз, и поэтому я бы выбрал ее поверх первого.

3 голосов
/ 02 марта 2011

Вы можете создать метод расширения следующим образом:

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 голосов
/ 02 марта 2011

Второе не работает для типов значений, не допускающих значения NULL ( Редактировать: , как вы и предполагали - пропустили это в первый раз) и на самом деле не имеет альтернативы, кроме первого, в котором есть раса-состояние.Есть две альтернативы, которые подходят обе - выбор одной или другой зависит от того, как часто вы получите пустую последовательность.

Если это обычный или ожидаемый случай, когда вы получаете пустое перечисление, используя 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
    }
}
1 голос
/ 02 марта 2011

Или, как расширение к решению от Гейба, заставьте его использовать лямбду, чтобы вы могли сбросить 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));
1 голос
/ 02 марта 2011

Иногда я использую этот шаблон:

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

Он запускает только одну итерацию (так что лучше, чем в примере кода 1), а область действия переменной firstItem ограничена внутри скобок (поэтому лучшечем образец кода 2).

0 голосов
/ 02 марта 2011

Только что сделал простой тест на примитивном типе, и похоже, что ваш пример кода №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 секунд

ForLoop
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 секунд

0 голосов
/ 02 марта 2011

Поскольку все общие Collections (то есть: типа System.Collections.ObjectModel ) имеют член Count, мой предпочтительный способ сделать это следующим образом:

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

Это безопасно, поскольку все коллекции будут иметь свойства Count и Item.Это также очень просто и легко для любых других программистов, читающих ваш код, чтобы понять, каково ваше намерение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...