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:
Edit: Added some additional interfaces at @Clockwork-Muse's recommendation
Edit 2: Removed duplicate signatures. Oops.
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'sOption::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
andMinOrElse
andSingleOrElse
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
Most helpful comment
You can do this using
list.DefaultIfEmpty(-1).First();
.