Runtime: System.Linq - Add FirstOrElse()

Created on 10 Mar 2020  路  21Comments  路  Source: dotnet/runtime

While doing some development, I ran into a problem. Take this code sample as an example.

var list = new List<int>();
// conditionally add some stuff to list 
var first = list.FirstOrDefault(); // 0

Is first zero because the first value was zero or because FirstOrDefault() took the default(int) path? This leaves me having to add in extra checks to form something like this:

int first;
if (list.Length > 0)
    first = list.First();
else
    first = -1;

// Or as a one-liner
int first = list.Length > 0 ? list.First() : -1;

This is functional, but less than ideal. We're forced to manually recreate the functionality FirstOrDefault() is supposed to provide.

So, here's my recommendation. I'm not entirely certain on the name as of yet, so if anyone has any better ideas I'm all ears.

public static class First
{
    public static TSource FirstOrElse<TSource>(this IEnumerable<TSource>, TSource defaultValue);
    public static TSource FirstOrElse<TSource>(this IEnumerable<TSource>, Func<TSource, bool> predicate, TSource defaultValue);

// Others, as suggested by Clockwork-Muse
    public static TSource ElementAtOrElse<TSource>(this IEnumerable<TSource>, int index, TSource defaultValue);

    public static TSource LastOrElse<TSource>(this IEnumerable<TSource>, TSource defaultValue);
    public static TSource LastOrElse<TSource>(this IEnumerable<TSource>, Func<TSource, bool> predicate, TSource defaultValue);
}

// Min/Max and Single throw if the collection is empty rather than returning `default`.

These methods would function identically to their original counterparts, however instead of returning default(T) when the IEnumerable is empty, it returns the provided value.

This would allow me to use a single line as so:

var first = list.FirstOrElse(-1); // 0

Some names I've thought of or heard from others:

  • FirstOrElse();
  • FirstOrProvided();
  • FirstOrDefault(); -- As an overload
  • FirstOr();

Edit: Added some additional interfaces at @Clockwork-Muse's recommendation
Edit 2: Removed duplicate signatures. Oops.

api-needs-work area-System.Linq

Most helpful comment

You can do this using list.DefaultIfEmpty(-1).First();.

All 21 comments

It is possible to do this today using list.Cast<int?>().FirstOrDefault() ?? -1; not ideal either, but it is what I tend to use. There isn't a big use case for FirstOrDefault on structs in the first place, I only use it with T?

....OrElse has precedent in other languages - eg: Rust's Option::map_or_else.

We would also need to look at similar methods:

  • ElementAt
  • Last
  • Max/Min
  • Single

....OrElse has precedent in other languages - eg: Rust's Option::map_or_else.

We would also need to look at similar methods:

* `ElementAt`

* `Last`

* `Max`/`Min`

* `Single`

True. Let me amend my suggestion to include a note for this.

I would argue that instead of revising all of LINQ, a simple list.Count > 0 ? is acceptable for the cases where a List<struct> is used AND the possibility of a Count of zero exsists AND where default(T) is a sensible value that could occure in that list.

Even when revising all those methods, I think it would make more sense to just return T?.

@HurricanKai - That presupposes that either you have a non-reference type, or that your list will not contain a null reference element.

Note that the general case of "find-or-add" has to be handled by the collection, if the attempt is something like cache.

You can do this using list.DefaultIfEmpty(-1).First();.

@Clockwork-Muse I can't think of another case then numbers where default(T) is a value that does not just represent a null / where null does mean anything other then no value present

Even in cases where null is a value used (which I can't think of), I'd think that's a missuse of null and should not be supported anywhere.

@jakobbotsch - That assumes that you don't mind if the default value is created regardless of the existence of other elements.

@HurricanKai - There are no default constructors for structs, they start zeroed. That may not be a valid object. Additionally, if this were an array or other indexed constructor, it's actually somewhat common to maintain object positionality as a sparse map (although I agree it's something that should likely be frowned upon).

@jakobbotsch - That assumes that you don't mind if the default value is created regardless of the existence of other elements.

I don't follow, how does the proposal prevent this from happening when the default is always an eagerly passed value?

@jakobbotsch - That assumes that you don't mind if the default value is created regardless of the existence of other elements.

I don't follow, how does the proposal prevent this from happening when the default is always an eagerly passed value?

I could, in theory, add an overload that accepts a Func<TSource> which could be evaluated only if it hits that branch. But I'm not sure that'd be too incredibly helpful. No other method in this namespace (unless I'm forgetting something) does anything like that.

What about an overload to GetValueOrDefault which accepts a default value e.g.

public static TSource FirstOrDefault (this System.Collections.Generic.IEnumerable source, Func predicate, TSource defaultValue);

What about an overload to GetValueOrDefault which accepts a default value e.g.

public static TSource FirstOrDefault (this System.Collections.Generic.IEnumerable source, Func predicate, TSource defaultValue);

I had first proposed an overload for FirstOrDefault() which allows you to specify the "default" value it returns, however @tannergooding suggested it might be better to create an entirely new method on the grounds that the signatures may be too similar. I decided on the OrElse suffix because it has a history in other languages. This gives it the advantage of being well-known. It should pair rather nicely with its existing OrDefault counterpart, on the grounds of them sharing similar yet distinct functions.

@tannergooding suggested it might be better to create an entirely new method on the grounds that the signatures may be too similar.

Specifically if this were ever to get an overload where the user provided fallback value comes from a factory method then I'd be worried about the following two overloads:

public static TSource FirstOrElse<TSource>(this IEnumerable<TSource>, Func<TSource, bool> predicate);
public static TSource FirstOrElse<TSource>(this IEnumerable<TSource>, Func<TSource> defaultValue);

The same issue would exist with any method overload and is just something to consider. The other reasoning is that Default may have a particular meaning to some people and I think FirstOrElse or FirstOrProvided make it more clear that the fallback isn't default(TSource)

@jakobbotsch - Ah, you're right, I misread the proposed signatures due to the Else.

I was assuming the signature was

public static TSource FirstOrElse<TSource>(this IEnumerable<TSource>, Func<TSource> defaultValue);

like @tannergooding listed.

Should I add overloads for Func<TSource>? Currently they only accept a TSource argument.

@tannergooding, I was just thinking shouldn't it maybe resemble the existing Nullable API's GetValueOrDefault already has an overload where the default can be specified.

Since this is on Linq I can both agree and disagree to it being a separate method but I feel having similarity is a good thing.

Since I forgot to mention this earlier, I'd be willing to implement this change once we work out the details and it's approved.

Looks like a useful addition. I think MaxOrElse and MinOrElse and SingleOrElse should also be added.

@Foxtrek64, could you perhaps update your OP with a complete API proposal? I can then mark this ready for review. You can follow the guidelines documented here. Thanks. cc @adamsitnik

Looks like a useful addition. I think MaxOrElse and MinOrElse and SingleOrElse should also be added.

As noted in my original post, Max(), Min(), and Single() all throw rather than returning default, so I had considered them to be outside of the scope of this suggestion. They do not suffer the same issue when working with a struct enumerable and most developers learn pretty quickly to surround these in a try/catch or only use them in situations where they already know the collection isn't empty. I can add them to the formal proposal, but they might be better suited to go into their own proposal.

I'll get the original post updated asap.

As noted in my original post, Max(), Min(), and Single() all throw rather than returning default

There's also SingleOrDefault(), which returns default on empty enumerables. It seems very compatible with what FirstOrElse is trying to achieve.

They do not suffer the same issue when working with a struct enumerable and most developers learn pretty quickly to surround these in a try/catch or only use them in situations where they already know the collection isn't empty.

I wouldn't necessarily agree, it's very often the case that you need to find the max of a potentially empty collection. Exception handling is a poor way of working around this. You're right to point out that these methods don't suffer from the default struct problem, however I still think that fundamentally these extensions solve the same problem and as such they should be reviewed together.

Hi @Foxtrek64

Thank you for your proposal. We already have an existing issue that describes the same problem: #20064

Since the other issue is older and was recently marked as ready for review, I am going to close this one and ask you to follow #20064 for updates (it should go through the official API review within the next few months, if it gets a green light we are going to implement it for .NET 6).

Thanks,
Adam

Was this page helpful?
0 / 5 - 0 ratings

Related issues

matty-hall picture matty-hall  路  3Comments

chunseoklee picture chunseoklee  路  3Comments

jkotas picture jkotas  路  3Comments

omariom picture omariom  路  3Comments

aggieben picture aggieben  路  3Comments