Reputation: 13765
Given:
class Program
{
private static readonly List<(int a, int b, int c)> Map = new List<(int a, int b, int c)>()
{
(1, 1, 2),
(1, 2, 3),
(2, 2, 4)
};
static void Main(string[] args)
{
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);
if (result == null)
Console.WriteLine("Not found");
else
Console.WriteLine("Found");
}
}
In the above example, a compiler error is encountered at line if (result == null)
.
CS0019 Operator '==' cannot be applied to operands of type '(int a, int b, int c)' and '<null>'
How would I go about checking that the tuple is found prior to proceeding in my "found" logic?
Prior to using the new c# 7 tuples, I would have this:
class Program
{
private static readonly List<Tuple<int, int, int>> Map = new List<Tuple<int, int, int>>()
{
new Tuple<int, int, int> (1, 1, 2),
new Tuple<int, int, int> (1, 2, 3),
new Tuple<int, int, int> (2, 2, 4)
};
static void Main(string[] args)
{
var result = Map.FirstOrDefault(w => w.Item1 == 4 && w.Item2 == 4);
if (result == null)
Console.WriteLine("Not found");
else
Console.WriteLine("Found");
}
}
Which worked fine. I like the more easily interpreted intention of the new syntax, but am unsure on how to null check it prior to acting on what was found (or not).
Upvotes: 83
Views: 41678
Reputation: 613
In C# 7.3, it's very clean:
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);
if (result == default) {
Console.WriteLine("Not found");
} else {
Console.WriteLine("Found");
}
Upvotes: 23
Reputation: 366
how i did it with c# 7.3
T findme;
var tuple = list.Select((x, i) => (Item: x, Index: i)).FirstOrDefault(x => x.Item.GetHashCode() == findme.GetHashCode());
if (tuple.Equals(default))
return;
...
var index = tuple.Index;
Upvotes: 1
Reputation: 19295
You need:
if (result.Equals(default)) Console.WriteLine(...
(c# > 7.1)
Upvotes: 6
Reputation: 457
Most of the answers above imply that your resulting element cannot be default(T), where T is your class/tuple.
A simple way around that is to use an approach as below:
var result = Map
.Select(t => (t, IsResult:true))
.FirstOrDefault(w => w.t.Item1 == 4 && w.t.Item2 == 4);
Console.WriteLine(result.IsResult ? "Found" : "Not found");
This sample uses C# 7.1 implied tuple names (and ValueTuple package for C# 7), but you can give the name to your tuple elements explicitly if required, or use a simple Tuple<T1,T2>
instead.
Upvotes: 0
Reputation: 101483
Just to add one more alternative to deal with value types and FirstOrDefault
: use Where
and cast the result to nullable type:
var result = Map.Where(w => w.a == 4 && w.b == 4)
.Cast<(int a, int b, int c)?>().FirstOrDefault();
if (result == null)
Console.WriteLine("Not found");
else
Console.WriteLine("Found");
You can even make an extension method of it:
public static class Extensions {
public static T? StructFirstOrDefault<T>(this IEnumerable<T> items, Func<T, bool> predicate) where T : struct {
return items.Where(predicate).Cast<T?>().FirstOrDefault();
}
}
Then your original code will compile (assuming you replace FirstOrDefault
with StructFirstOrDefault
).
Upvotes: 21
Reputation: 131423
Value tuples are value types. They can't be null, which is why the compiler complains. The old Tuple type was a reference type
The result of FirstOrDefault()
in this case will be a default instance of an ValueTuple<int,int,int>
- all fields will be set to their default value, 0.
If you want to check for a default, you can compare the result with the default value of ValueTuple<int,int,int>
, eg:
var result=(new List<(int a, int b, int c)>()
{
(1, 1, 2),
(1, 2, 3),
(2, 2, 4)
}
).FirstOrDefault(w => w.a == 4 && w.b == 4);
if (result.Equals(default(ValueTuple<int,int,int>)))
{
Console.WriteLine("Missing!");
}
WORD OF WARNING
The method is called FirstOrDefault
, not TryFirst
. It's not meant to check whether a value exists or not, although we all (ab)use it this way.
Creating such an extension method in C# isn't that difficult. The classic option is to use an out parameter:
public static bool TryFirst<T>(this IEnumerable<T> seq,Func<T,bool> filter, out T result)
{
result=default(T);
foreach(var item in seq)
{
if (filter(item)) {
result=item;
return true;
}
}
return false;
}
Calling this can be simplified in C# 7 as :
if (myList.TryFirst(w => w.a == 4 && w.b == 1,out var result))
{
Console.WriteLine(result);
}
F# developers can brag that they have a Seq.tryPick that will return None
if no match is found.
C# doesn't have Option types or the Maybe type (yet), but maybe (pun intended) we can build our own:
class Option<T>
{
public T Value {get;private set;}
public bool HasValue {get;private set;}
public Option(T value) { Value=value; HasValue=true;}
public static readonly Option<T> Empty=new Option<T>();
private Option(){}
public void Deconstruct(out bool hasValue,out T value)
{
hasValue=HasValue;
value=Value;
}
}
public static Option<T> TryPick<T>(this IEnumerable<T> seq,Func<T,bool> filter)
{
foreach(var item in seq)
{
if (filter(item)) {
return new Option<T>(item);
}
}
return Option<T>.Empty;
}
Which allows writing the following Go-style call:
var (found,value) =myList.TryPick(w => w.a == 4 && w.b == 1);
In addition to the more traditional :
var result=myList.TryPick(w => w.a == 4 && w.b == 1);
if (result.HasValue) {...}
Upvotes: 89
Reputation: 43254
If you are sure your data set won't include (0, 0, 0)
, then as others have said, you can check for the default:
if (result.Equals(default(ValueTuple<int,int,int>))) ...
If that value may occur though, then you could use First
and catch the exception when there's no match:
class Program
{
private static readonly List<(int a, int b, int c)> Map =
new List<(int a, int b, int c)>()
{
(1, 1, 2),
(1, 2, 3),
(2, 2, 4),
(0, 0, 0)
};
static void Main(string[] args)
{
try
{
Map.First(w => w.a == 0 && w.b == 0);
Console.WriteLine("Found");
}
catch (InvalidOperationException)
{
Console.WriteLine("Not found");
}
}
}
Alternatively, you could use a library, such as my own Succinc<T> library that provide a TryFirst
method that returns a "maybe" type of none
if no match, or the item if matched:
class Program
{
private static readonly List<(int a, int b, int c)> Map =
new List<(int a, int b, int c)>()
{
(1, 1, 2),
(1, 2, 3),
(2, 2, 4),
(0, 0, 0)
};
static void Main(string[] args)
{
var result = Map.TryFirst(w => w.a == 0 && w.b == 0);
Console.WriteLine(result.HasValue ? "Found" : "Not found");
}
}
Upvotes: 8
Reputation: 824
Your check could be the following:
if (!Map.Any(w => w.a == 4 && w.b == 4))
{
Console.WriteLine("Not found");
}
else
{
var result = Map.First(w => w.a == 4 && w.b == 4);
Console.WriteLine("Found");
}
Upvotes: 6
Reputation: 111870
As written by Panagiotis you can't do it directly... You could "cheat" a little:
var result = Map.Where(w => w.a == 4 && w.b == 4).Take(1).ToArray();
if (result.Length == 0)
Console.WriteLine("Not found");
else
Console.WriteLine("Found");
You take up to one element with the Where
and put the result in an array of length 0-1.
Alternatively you could repeat the comparison:
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);
if (result.a == 4 && result.b == 4)
Console.WriteLine("Not found");
This second option won't work if you were looking for
var result = Map.FirstOrDefault(w => w.a == 0 && w.b == 0);
In this case the "default" value returned by FirstOrDefault()
has a == 0
and b == 0
.
Or you could simply create a "special" FirstOrDefault()
that has a out bool success
(like the various TryParse
):
static class EnumerableEx
{
public static T FirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate, out bool success)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}
foreach (T ele in source)
{
if (predicate(ele))
{
success = true;
return ele;
}
}
success = false;
return default(T);
}
}
use it like:
bool success;
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4, out success);
Other possible extension method, ToNullable<>()
static class EnumerableEx
{
public static IEnumerable<T?> ToNullable<T>(this IEnumerable<T> source) where T : struct
{
return source.Cast<T?>();
}
}
Use it like:
var result = Map.Where(w => w.a == 4 && w.b == 4).ToNullable().FirstOrDefault();
if (result == null)
Note that result
is a T?
, so you'll need to do result.Value
to use its value.
Upvotes: 7
Reputation: 98
ValueTuple is the underlying type used for the C#7 tuples. They cannot be null as they are value types. You can test them for default though, but that might actually be a valid value.
Also, the equality operator is not defined on ValueTuple, so you must use Equals(...).
static void Main(string[] args)
{
var result = Map.FirstOrDefault(w => w.Item1 == 4 && w.Item2 == 4);
if (result.Equals(default(ValueTuple<int, int, int>)))
Console.WriteLine("Not found");
else
Console.WriteLine("Found");
}
Upvotes: 4