This post is part of a series on .NET 6 and C# 10 features. Use the following links to navigate to other articles in the series and build up your .NET 6/C# 10 knowledge! While the articles are seperated into .NET 6 and C# 10 changes, these days the lines are very blurred so don’t read too much into it.
.NET 6
Minimal API Framework
DateOnly and TimeOnly Types
LINQ OrDefault Enhancements
Implicit Using Statements
IEnumerable Chunk
SOCKS Proxy Support
Priority Queue
MaxBy/MinBy
C# 10
Global Using Statements
File Scoped Namespaces
A fairly common interview question I have for any C#/.NET Developer revolves around the difference between First and FirstOrDefault LINQ statements. There’s a general flow to the questions that basically go something like :
What’s the difference between First and FirstOrDefault
When answered, I follow it up with :
So when FirstOrDefault can’t find a value, what does it return?
Commonly, I actually have people say things like it always returns null. Which is incorrect, it returns the “default” value for that type. Essentially, doing something like default(T). But if they get this part right, then I follow it up with things like
So what is the default value then? What would be the default value for a type of integer?
The correct answer to the above is 0. I’m not sure if it’s a difficult set of questions or not (Certainly many get it wrong), but it’s definitely something you will run into a lot in your career if you develop in C# for any length of time.
One of the main reasons I ask this question, is often I see code across all levels that works something like this :
var hayStack = new List { 1, 2, 2 }; var needle = 3; var foundValue = hayStack.FirstOrDefault(x => x == needle); if(foundValue == 0) { Console.WriteLine("We couldn't find the value"); }
This works of course, but what if needle actually is the number 0? You have to do a bit of a dance to work out was it truly not found, or is the value you are looking for actually the default value anyway. You have a couple of options :
- Run a LINQ Any statement beforehand to ensure that your list does indeed contain the item
- Cast the list to a nullable variant of the type if not already, so you can ensure you get null back if not found
- Use First instead of FirstOrDefault and catch the exception
You might think this is only really an issue in edge cases where you are using primitives that typically aren’t nullable, but with the introduction of Nullable Reference types in C# 8, this is actually going to become a very common scenario.
.NET 6 introduces the concept of being able to pass in what the default value should be, should the item not be found. So for example, this code is now valid in .NET 6.
var hayStack = new List<int?> { 1, 2, 2 }; var needle = 3; var foundValue = hayStack.FirstOrDefault(x => x == needle, -1); if(foundValue == -1) { Console.WriteLine("We couldn't find the value"); }
But… Hold on. All we are doing here is saying instead of returning 0, return -1. Oof!
As it turns out, the IEnumerable method still must return a valid integer. And because our type is not nullable, even with this extension method, we can’t force it to return null. This goes for FirstOrDefault, SingleOrDefault and LastOrDefault.
The reason for this article I guess is to first and foremost to introduce the new extension, but also secondly to say, it doesn’t quite solve the problem that I thought it would at first look. You still must return a valid type. It doesn’t make it as useful as I first thought, but at the very least, it may help in some very edge scenarios, for example upserting values.
For this method to have worked as you intended, C# needs to support discriminated unions. We’re not quite there yet…
public static FirstOrDefault (this System.Collections.Generic.IEnumerable source, Func predicate, TDefault defaultValue)
The example that you gave is one where you need to know whether the item was found or not, which is not the ideal scenario for those new OrDefault methods. For that kind of scenario, I think that the second option of using a nullable type is better than all the others.
For scenarios where using the new overloads of OrDefault is preferable, the most elegant option before .NET 6 IMHO would have actually been:
.Where(x => x == needle).DefaultIfEmpty(/* default value */).First();
For which those new overloads seem to be shorthands.
Use a HashSet.