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).
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) {...}
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
).
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.
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");
}
}
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");
}
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");
}
You need:
if (result.Equals(default)) Console.WriteLine(...
(c# > 7.1)
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");
}
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;
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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.