Лучшие практики для работы с операторами LINQ, которые приводят к пустым последовательностям и тому подобное? - PullRequest
23 голосов
/ 11 сентября 2010

... Я немного сбит с толку или не уверен, что делать с ошибками, возникающими из операторов LINQ. Мне просто нравится, когда я могу извлечь один или несколько элементов из коллекции, основываясь на некоторых критериях ... с помощью строки кода single . Это довольно круто.

Но я разрываюсь с обработкой ошибок или проверкой граничных условий. Если я хочу получить элемент, используя First (), и , но элемент не удовлетворяет моему запросу, генерируется исключение. Это немного обидно, потому что теперь я должен обернуть каждый оператор LINQ отдельным блоком try / catch. Для меня код начинает выглядеть немного неаккуратно со всем этим, тем более что мне приходится объявлять переменные вне блока try / catch, чтобы позже я мог использовать их (нулевые) значения (которые были установлены в null в блок захвата).

Кто-нибудь здесь понимает мое затруднительное положение? Если мне придется обернуть каждый оператор LINQ в блоки try / catch, я сделаю это, потому что это все равно намного лучше, чем писать всевозможные циклы for для выполнения одной и той же задачи. Но должен быть лучше, верно? :) Я хотел бы услышать, что все остальные здесь делают в этой ситуации.

** ОБНОВЛЕНИЕ **

Спасибо за ответы, ребята, они очень помогли. Еще одна вещь, которую я также собирался затронуть, наряду с «однобедренностью» LINQ, - это то, что если я хочу получить. Сначала я должен сделать это из самого простого запроса LINQ, затем я могу запросить свойство результата, которое я ищу. Я думаю, что нет никакого способа обойти это?

Ответы [ 5 ]

55 голосов
/ 11 сентября 2010

Используйте Сначала, когда вы знаете , что в коллекции есть один или несколько предметов. Используйте Single, если вы знаете , что в коллекции ровно один элемент. Если вы не знаете этих вещей, то не используйте эти методы . Используйте методы, которые делают что-то еще, например FirstOrDefault (), SingleOrDefault () и т. Д.

Вы могли бы, например, сказать:

int? first = sequence.Any() ? (int?) sequence.First() : (int?) null;

, который намного менее груб, чем

int? first = null;
try { first = sequence.First(); } catch { }

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

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

public static int? FirstOrNull(this IEnumerable<int> sequence)
{
    foreach(int item in sequence)
        return item;
    return null;
}

или даже лучше:

public static T? FirstOrNull<T>(this IEnumerable<T> sequence) where T : struct
{
    foreach(T item in sequence)
        return item;
    return null;
}

или это:

struct Maybe<T>
{
    public T Item { get; private set; }
    public bool Valid { get; private set; }
    public Maybe(T item) : this() 
    { this.Item = item; this.Valid = true; }
}

public static Maybe<T> MyFirst<T>(this IEnumerable<T> sequence) 
{
    foreach(T item in sequence)
        return new Maybe(item);
    return default(Maybe<T>);
}
...
var first = sequence.MyFirst();
if (first.Valid) Console.WriteLine(first.Item);

Но что бы вы ни делали, не обрабатывайте те исключения , которые вы упомянули. Эти исключения не предназначены для обработки, они должны сообщить вам, что в вашем коде есть ошибки . Вы не должны обращаться с ними, вы должны исправлять ошибки. Помещение ловушек вокруг них - это сокрытие ошибок , а не исправление ошибок .

UPDATE:

Дейв спрашивает, как создать FirstOrNull, который принимает предикат. Достаточно просто. Вы можете сделать это так:

public static T? FirstOrNull<T>(this IEnumerable<T> sequence, Func<T, bool> predicate) where T : struct
{
    foreach(T item in sequence)
        if (predicate(item)) return item;
    return null;
}

Или вот так

public static T? FirstOrNull<T>(this IEnumerable<T> sequence, Func<T, bool> predicate) where T : struct
{
    foreach(T item in sequence.Where(predicate))
        return item;
    return null;
}

Или даже не беспокойтесь:

var first = sequence.Where(x=>whatever).FirstOrNull();

Нет причин, по которым предикат должен идти в FirstOrNull. Мы предоставляем First (), который принимает предикат для удобства, чтобы вам не приходилось вводить лишнее «Где».

ОБНОВЛЕНИЕ: Дейв задает еще один дополнительный вопрос, который, я думаю, может быть «что, если я хочу сказать sequence.FirstOrNull (). Frob (). Blah (). Независимо от (), но любой из тех, кто находится на этой линии, может вернуть ноль? "

Мы рассмотрели вопрос о добавлении в C # оператора доступа к элементу с нулевым распространением, условно обозначенного как ?. - то есть можно сказать

x = a? .B? .C? .D;

и если a, b или c выдают ноль, то результатом будет присвоение нолю x.

Очевидно, что мы на самом деле не реализовали это для C # 4.0. Это возможный рабочий элемент для гипотетических будущих версий языка ... ОБНОВЛЕНИЕ "Оператор Элвиса" был добавлен в C # 6.0, ууу!

Обратите внимание, что в C # есть есть оператор объединения нулей:

(sequence.FirstOrNull () ?? GetDefault ()). Frob (). Blah (). Независимо от ()

означает «Если FirstOrNull возвращает ненулевое значение, используйте его в качестве получателя Frob, в противном случае вызовите GetDefault и используйте его в качестве получателя». Альтернативным подходом было бы снова написать свой собственный:

public static T FirstOrLazy<T>(this IEnumerable<T> sequence, Func<T> lazy) 
{
    foreach(T item in sequence)
        return item;
    return lazy();
}

sequence.FirstOrLazy(()=>GetDefault()).Frob().Blah().Whatever();

Теперь вы получаете первый элемент, если он есть, или результат вызова GetDefault (), если его нет.

22 голосов
/ 11 сентября 2010

Используйте FirstOrDefault, а затем проверьте null.

4 голосов
/ 11 сентября 2010

Кто-нибудь здесь понимает мое затруднительное положение?

Не совсем, если вы замените First() на FirstOrDefault(), ваши блоки try / catch можно заменить на операторы if(...) или стратегически используемые операторы && или ||.

0 голосов
/ 07 мая 2015

В дополнение к реализациям Эрика Липперта FirstOrNull, здесь есть версия SingleOrNull для типов значений.

    /*
     * This SingleOrNull implementation is heavily based on the standard
     * Single/SingleOrDefault methods, retrieved from the reference
     * source codebase on Thu May 7, 2015.
     * http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs
     *
     * In case it isn't clear, the first part is merely an opportunistic
     * optimization for sources that are actually lists, and which thus
     * expose a precomputed count.  Using a count is faster since we
     * only have to read 0-1 elements.  In contrast, the fallback must
     * read 1-2 elements.
     */
    public static TSource? SingleOrNull<TSource>(
        this IEnumerable<TSource> source)
        where TSource : struct
    {
        if (source == null) throw new ArgumentNullException("source");
        var list = source as IList<TSource>;
        if (list != null)
        {
            switch (list.Count)
            {
                case 0: return null;
                case 1: return list[0];
            }
        }
        else
        {
            using (var e = source.GetEnumerator())
            {
                if (!e.MoveNext()) return null;
                var result = e.Current;
                if (!e.MoveNext()) return result;
            }
        }
        return null;
    }

И вот несколько тестов , которые были добавлены навсегдаизмерение.

0 голосов
/ 12 сентября 2010

Операторы FirstOrDefault и SingleOrDefault решают вашу проблему.

С аналогичной проблемой, с которой я столкнулся, является случай, когда коллекция содержит коллекцию; вложенный список. В этом случае я часто использую оператор объединения нулей, чтобы разрешить поиск по одной строке через вложенный список. Самый тривиальный случай выглядит так:

var nestedList = new List<List<int>>();
int? first = (nestedList.FirstOrDefault() ?? new List<int>).FirstOrDefault();

Таким образом, если внешний список пуст, возвращается новый пустой список, который просто позволяет окончательному FirstOrDefault вернуть null.

...