How to null check c# 7 tuple in LINQ query?

C#LinqC# 7.0

C# Problem Overview


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).

C# Solutions


Solution 1 - C#

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) {...}

Solution 2 - C#

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).

Solution 3 - C#

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.

Solution 4 - C#

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");
    }
}

Solution 5 - C#

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");
}

Solution 6 - C#

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");
}

Solution 7 - C#

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");
}

Solution 8 - C#

You need:

if (result.Equals(default)) Console.WriteLine(...

(c# > 7.1)

Solution 9 - C#

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;

Solution 10 - C#

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKritnerView Question on Stackoverflow
Solution 1 - C#Panagiotis KanavosView Answer on Stackoverflow
Solution 2 - C#EvkView Answer on Stackoverflow
Solution 3 - C#xanatosView Answer on Stackoverflow
Solution 4 - C#David ArnoView Answer on Stackoverflow
Solution 5 - C#Kevin SijbersView Answer on Stackoverflow
Solution 6 - C#DesmondView Answer on Stackoverflow
Solution 7 - C#Patrik TengströmView Answer on Stackoverflow
Solution 8 - C#kofifusView Answer on Stackoverflow
Solution 9 - C#tom noblemanView Answer on Stackoverflow
Solution 10 - C#Jay HaybatovView Answer on Stackoverflow