Cómo comprobar null tupla c # 7 en consulta LINQ?


Dado:

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

En el ejemplo anterior, se encuentra un error del compilador en la línea if (result == null).

El operador CS0019 '= = 'no se puede aplicar a los operandos de tipo' (int a, int b, int c) ' y "

¿Cómo voy a comprobar que la tupla se encuentra antes de proceder en mi lógica "encontrado"?

Antes de usar las nuevas tuplas de c # 7, tendría esto:

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

Que funcionó bien. Me gusta la intención más fácil de interpretar de la nueva sintaxis, pero no estoy seguro de cómo comprobar null antes de actuar sobre lo que se encontró (o no).

Author: Kritner, 2017-06-01

7 answers

Las tuplas de valor son tipos de valor. No pueden ser nulos, por lo que el compilador se queja. El antiguo tipo de tupla era un tipo de referencia

El resultado de FirstOrDefault() en este caso será una instancia predeterminada de un ValueTuple<int,int,int> - todos los campos se establecerán en su valor predeterminado, 0.

Si desea verificar un valor predeterminado, puede comparar el resultado con el valor predeterminado de ValueTuple<int,int,int>, por ejemplo:

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

PALABRA DE ADVERTENCIA

El método se llama FirstOrDefault, no TryFirst. No significa comprobar si un valor existe o no, aunque todos (ab)lo usamos de esta manera.

Crear un método de extensión en C# no es tan difícil. La opción clásica es usar un parámetro 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;
}

Llamar a esto se puede simplificar en C # 7 como:

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

F# los desarrolladores pueden presumir de que tienen un Seq.tryPick que devolverá None si no se encuentra ninguna coincidencia.

C# no tiene tipos de opciones o el tipo Maybe (todavía), pero maybe (juego de palabras) puede construir la nuestra:

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

Que permite escribir la siguiente llamada al estilo Go:

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

Además De los más tradicionales :

var result=myList.TryPick(w => w.a == 4 && w.b == 1);
if (result.HasValue) {...}
 36
Author: Panagiotis Kanavos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 15:12:15

Solo para agregar una alternativa más para tratar con los tipos de valor y FirstOrDefault: use Where y lance el resultado al tipo 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");

Incluso puede hacer un método de extensión de la misma:

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

Entonces tu código original se compilará (suponiendo que reemplaces FirstOrDefault con StructFirstOrDefault).

 11
Author: Evk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-03 15:38:09

Como lo escribió Panagiotis, no puedes hacerlo directamente... Usted podría "engañar" un poco:

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

Se toma un elemento con el Where y se pone el resultado en una matriz de longitud 0-1.

Alternativamente puedes repetir la comparación:

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

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

Esta segunda opción no funcionará si estabas buscando

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

En este caso el valor" predeterminado " devuelto por FirstOrDefault() ha a == 0 y b == 0.

O simplemente podría crear un "especial" FirstOrDefault() que tiene un out bool success (como los varios 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);
    }
}

Úsalo como:

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

Otro posible método de extensión, ToNullable<>()

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

Úsalo como:

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

if (result == null)

Tenga en cuenta que result es un T?, por lo que tendrá que hacer result.Value para usar su valor.

 6
Author: xanatos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 13:20:09

Si está seguro de que su conjunto de datos no incluirá (0, 0, 0), entonces como otros han dicho, puede verificar el valor predeterminado:

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

Si ese valor puede ocurrir, entonces puedes usar First y atrapar la excepción cuando no hay coincidencia:

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

Alternativamente, puede usar una biblioteca, como mi propia biblioteca Succinc que proporciona un método TryFirst que devuelve un tipo "tal vez" de none si no coincide, o el elemento si coincide:

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");
    }
}
 6
Author: David Arno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 13:40:16

Su cheque podría ser el siguiente:

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");
}
 5
Author: Kevin Sijbers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 13:38:14

ValueTuple es el tipo subyacente utilizado para las tuplas de C#7. No pueden ser null ya que son tipos de valor. Sin embargo, puede probarlos por defecto, pero en realidad podría ser un valor válido.

Además, el operador de igualdad no está definido en ValueTuple, por lo que debe usar 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");
}
 4
Author: Patrik Tengström,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 12:50:00

La mayoría de las respuestas anteriores implican que el elemento resultante no puede ser predeterminado(T), donde T es su clase/tupla.

Una forma sencilla de evitar esto es usar un enfoque como el siguiente:

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

Este ejemplo usa nombres de tupla implícitos en C# 7.1 (y paquete ValueTuple para C# 7), pero puede dar el nombre a sus elementos de tupla explícitamente si es necesario, o usar un simple Tuple<T1,T2> en su lugar.

 0
Author: pennanth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-05 07:55:35