开发者

Best practices for dealing with LINQ statements that result in empty sequences and the like?

开发者 https://www.devze.com 2023-01-15 11:05 出处:网络
...I\'m a little confused, or unsure about how to deal with errors that arise from LINQ statements.I just love being able to pull one or more items from a collection, based on some criteria... with a

...I'm a little confused, or unsure about how to deal with errors that arise from LINQ statements. I just love being able to pull one or more items from a collection, based on some criteria... with a single line of code. That's pretty awesome.

But where I'm torn is with the error handling, or the boundary condition checking. If I want to retrieve an item using First(), and no item satisfies my query, an exception gets thrown. That's a bit of a bummer because now I have to wrap every LINQ statement with a separate try/catch block. To me, the code starts to look a little me开发者_如何学Pythonssy with all of this, especially since I end up having to declare variables outside of the try/catch block so I can use their (null) values later (which were set to null in the catch block).

Does anyone here understand my predicament? If I have to wrap every LINQ statement in try/catch blocks, I will, because it's still a hell of a lot better than writing all sorts of for loops to accomplish the same thing. But there must be a better way, right? :) I'd like to hear what everyone else here does in this situation.

** UPDATE **

Thanks for the answers, guys, they have been very helpful. One other thing I also was going to bring up, along the "one-lineness" of LINQ, is that if I want to get some .some_value.some_value.some_other_value, if I adopt an approach where I have to check for a Nullable, I have to do it from the most basic LINQ query first, then I can query for the result's property that I'm looking for. I guess there's no way around this?


Use First when you know that there is one or more items in the collection. Use Single when you know that there is exactly one item in the collection. If you don't know those things, then don't use those methods. Use methods that do something else, like FirstOrDefault(), SingleOrDefault() and so on.

You could, for example, say:

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

which is far less gross than

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

But still not great because it iterates the first item of the sequence twice. In this case I would say if there aren't sequence operators that do what you want then write your own.

Continuing with our example, suppose you have a sequence of integers and want to get the first item, or, if there isn't one, return null. There isn't a built-in sequence operator that does that, but it's easy to write it:

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

or even better:

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

or this:

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);

But whatever you do, do not handle those exceptions you mentioned. Those exceptions are not meant to be handled, they are meant to tell you that you have bugs in your code. You shouldn't be handling them, you should be fixing the bugs. Putting try-catches around them is hiding bugs, not fixing bugs.

UPDATE:

Dave asks how to make a FirstOrNull that takes a predicate. Easy enough. You could do it like this:

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;
}

Or like this

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;
}

Or, don't even bother:

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

No reason why the predicate has to go on FirstOrNull. We provide a First() that takes a predicate as a convenience so that you don't have to type the extra "Where".

UPDATE: Dave asks another follow-up question which I think might be "what if I want to say sequence.FirstOrNull().Frob().Blah().Whatever() but any one of those along the line could return null?"

We have considered adding a null-propagating member-access operator to C#, tentatively notated as ?. -- that is, you could say

x = a?.b?.c?.d;

and if a, b, or c produced null, then the result would be to assign null to x.

Obviously we did not actually implement it for C# 4.0. It is a possible work item for hypothetical future versions of the language... UPDATE the "Elvis operator" has been added to C# 6.0, yay!

Note that C# does have a null coalescing operator:

(sequence.FirstOrNull() ?? GetDefault()).Frob().Blah().Whatever()

means "If FirstOrNull returns non-null use it as the receiver of Frob, otherwise call GetDefault and use that as the receiver". An alternative approach would be to again, write your own:

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();

Now you get the first item if there is one, or the result of a call to GetDefault() if there is not.


Use FirstOrDefault and then check for null.


Does anyone here understand my predicament?

Not really, if you replace First() with FirstOrDefault() your try/catch blocks can be replace with if(...) statements or strategically used && or || operators.


The FirstOrDefault and SingleOrDefault operators solve your problem.

A similar problem I've encountered is when a collection contains a collection; a nested list. In that case I often use the null coalescing operator to allow a single line retrieval through the nested list. The most trivial case looks like this:

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

So if the outer list is empty, a new empty list is returned which simply allows the final FirstOrDefault to return a null.


In addition to Eric Lippert's FirstOrNull implementations, here is a version of SingleOrNull for value types.

    /*
     * 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;
    }

And here are a few tests thrown in for good measure.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号