Как null проверить кортеж c# 7 в запросе LINQ?

дано:

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

В приведенном выше примере, ошибка компилятора возникает в строке if (result == null).

Cs0019 оператор '= = ' не может быть применен к операндам типа '(int a, int b, int c)' и "

как бы я проверил, что кортеж найден, прежде чем продолжить в моей "найденной" логике?

до использования новых кортежей c# 7 у меня было бы это:

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

, который работал хорошо. Мне нравится больше легко интерпретируемое намерение нового синтаксиса, но я не уверен, как его проверить до того, как действовать на то, что было найдено (или нет).

7 ответов


кортежи значений-это типы значений. Они не могут быть null, поэтому компилятор жалуется. Старый тип Кортежа был ссылочным типом

результат FirstOrDefault() в этом случае будет экземпляром по умолчанию ValueTuple<int,int,int> - всем полям будет присвоено значение по умолчанию 0.

если вы хотите проверить значение по умолчанию, вы можете сравнить результат со значением по умолчанию ValueTuple<int,int,int>, например:

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

ПРЕДУПРЕЖДЕНИЕ

в метод называется FirstOrDefault, а не TryFirst. Он не предназначен для проверки того, существует ли значение или нет, хотя мы все (ab)используем его таким образом.

создание такого метода расширения в C# не так сложно. Классический вариант-использовать параметр out:

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

вызов этого можно упростить в C# 7 как:

if (myList.TryFirst(w => w.a == 4 && w.b == 1,out var result))
{
    Console.WriteLine(result);
}

разработчики F# могут похвастаться, что у них есть Seq.tryPick, что вернет None если совпадение не найдено.

C# не имеет типов опций или типа Maybe (пока), но, возможно (каламбур), мы можем создать свой собственный:

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

что позволяет написать следующий вызов Go-style:

var (found,value) =myList.TryPick(w => w.a == 4 && w.b == 1);

В дополнение к более традиционным :

var result=myList.TryPick(w => w.a == 4 && w.b == 1);
if (result.HasValue) {...}

просто добавить еще одну альтернативу для работы с типами значений и FirstOrDefault используйте Where и приведите результат к типу nullable:

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

вы даже можете сделать метод расширения из него:

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

тогда ваш исходный код будет компилироваться (при условии, что вы замените FirstOrDefault С StructFirstOrDefault).


как написано Panagiotis, вы не можете сделать это напрямую... Вы могли бы "обмануть" немного:

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

вы берете до одного элемента с Where и поместить результат в массив длины 0-1.

в качестве альтернативы вы можете повторить сравнение:

var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4);

if (result.a == 4 && result.b == 4)
    Console.WriteLine("Not found");

этот второй вариант не будет работать, если вы искали

var result = Map.FirstOrDefault(w => w.a == 0 && w.b == 0);

в этом случае значение "default", возвращаемое FirstOrDefault() и a == 0 и b == 0.

или вы можете просто создать "специальный"FirstOrDefault() что есть out bool success (например, различные 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);
    }
}

использовать его как:

bool success;
var result = Map.FirstOrDefault(w => w.a == 4 && w.b == 4, out success);

другой возможный метод расширения, ToNullable<>()

static class EnumerableEx
{
    public static IEnumerable<T?> ToNullable<T>(this IEnumerable<T> source) where T : struct
    {
        return source.Cast<T?>();
    }
}

использовать его как:

var result = Map.Where(w => w.a == 4 && w.b == 4).ToNullable().FirstOrDefault();

if (result == null)

отметим, что result это T?, так что вам нужно будет делать result.Value использовать его значение.


если вы уверены, что ваш набор данных не включают (0, 0, 0), то, как говорили другие, вы можете проверить по умолчанию:

if (result.Equals(default(ValueTuple<int,int,int>))) ...

если это значение может произойти, то вы можете использовать First и поймать исключение, когда нет совпадений:

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

кроме того, вы можете использовать библиотеку,например, моя собственная библиотека Succinc!--13-->, которые обеспечивают TryFirst метод, который возвращает тип "возможно"none если никакой спички, или деталь если совпадение:

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

ваш чек может быть следующим:

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-базовый тип, используемый для кортежей C#7. Они не могут быть null, поскольку они являются типами значений. Вы можете проверить их по умолчанию, но это может быть допустимым значением.

кроме того, оператор равенства не определен в ValueTuple, поэтому вы должны использовать 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");
}

большинство ответов выше подразумевают, что ваш результирующий элемент не может быть default(T), где T-ваш класс/кортеж.

простой способ обойти это-использовать подход, как показано ниже:

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

в этом примере используются подразумеваемые имена кортежей C# 7.1 (и пакет ValueTuple для C# 7), но при необходимости можно явно указать имя элементов кортежа или использовать простой Tuple<T1,T2> вместо.