web-dev-qa-db-fra.com

Comment faire un null check c # 7 Tuple dans une requête LINQ?

Donné:

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

Dans l'exemple ci-dessus, une erreur de compilation est rencontrée à la ligne if (result == null)

CS0019 L'opérateur '==' ne peut pas être appliqué aux opérandes de type '(int a, int b, int c)' et ''

Comment pourrais-je vérifier si le tuple est trouvé avant de continuer dans ma logique "trouvé"?

Avant d’utiliser les nouveaux tuples c # 7, j’aurais ceci:

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

Ce qui a bien fonctionné. J'aime l’intention plus facilement interprétée de la nouvelle syntaxe, mais je ne sais pas comment la vérifier avant d’agir sur ce qui a été trouvé (ou non).

33
Kritner

Les tuples de valeur sont des types de valeur. Ils ne peuvent pas être nuls, c'est pourquoi le compilateur se plaint. L'ancien type de tuple était un type de référence

Dans ce cas, le résultat de FirstOrDefault() sera une instance par défaut d'un ValueTuple<int,int,int> - tous les champs auront la valeur par défaut, 0.

Si vous souhaitez rechercher une valeur par défaut, vous pouvez comparer le résultat à la valeur par défaut de ValueTuple<int,int,int>, par exemple:

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

Mot d'AVERTISSEMENT

La méthode s'appelle FirstOrDefault, pas TryFirst. Il ne s'agit pas de vérifier si une valeur existe ou non, même si nous l'utilisons tous de cette façon. 

Créer une telle méthode d'extension en C # n'est pas si difficile. L'option classique consiste à utiliser un paramètre 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;
}

Cet appel peut être simplifié en C # 7 en tant que:

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

Les développeurs F # peuvent se vanter d'avoir un Seq.tryPick qui retournera None si aucune correspondance n'est trouvée. 

C # n'a pas (encore) de types d'option ou de type Maybe, mais peut-être (jeu de mots voulu) nous pouvons construire notre propre:

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

Ce qui permet d'écrire l'appel de style suivant:

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

En plus des plus traditionnels: 

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

Juste pour ajouter une alternative pour traiter les types de valeur et FirstOrDefault: utilisez Where et convertissez le résultat en type 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");

Vous pouvez même en faire une méthode d'extension:

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

Ensuite, votre code original sera compilé (en supposant que vous remplacez FirstOrDefault par StructFirstOrDefault).

13
Evk

Comme écrit par Panagiotis, vous ne pouvez pas le faire directement ... Vous pouvez "tricher" un peu:

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

Vous prenez un élément avec la variable Where et placez le résultat dans un tableau de longueur 0-1.

Sinon, vous pouvez répéter la comparaison:

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

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

Cette deuxième option ne fonctionnera pas si vous cherchiez 

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

Dans ce cas, la valeur "par défaut" renvoyée par FirstOrDefault() a a == 0 et b == 0.

Ou vous pouvez simplement créer une FirstOrDefault() "spéciale" qui a un out bool success (comme le divers 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);
    }
}

utilisez-le comme:

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

Autre méthode d'extension possible, ToNullable<>()

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

Utilisez-le comme:

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

if (result == null)

Notez que result est un T?, vous devrez donc utiliser result.Value pour utiliser sa valeur.

6
xanatos

Si vous êtes certain que votre ensemble de données n'inclura pas (0, 0, 0), alors, comme d'autres l'ont dit, vous pouvez vérifier la valeur par défaut:

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

Si cette valeur est susceptible de se produire, vous pouvez utiliser First et intercepter l'exception lorsqu'il n'y a pas de correspondance:

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

Vous pouvez également utiliser une bibliothèque, telle que ma propre bibliothèque Succinc <T> qui fournit une méthode TryFirst qui renvoie un type "peut-être" de none si aucune correspondance ou l'élément correspondant:

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
David Arno

Votre chèque pourrait être le suivant:

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
Kevin Sijbers

ValueTuple est le type sous-jacent utilisé pour les tuples C # 7. Ils ne peuvent pas être nuls car ce sont des types valeur. Vous pouvez les tester par défaut, mais cela pourrait être une valeur valide. 

De plus, l'opérateur d'égalité n'est pas défini sur ValueTuple, vous devez donc utiliser 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

La plupart des réponses ci-dessus impliquent que votre élément résultant ne peut pas être défaut (T), où T est votre classe/Tuple.

Une solution simple consiste à utiliser l’approche ci-dessous:

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

Cet exemple utilise des noms de tuple implicites en C # 7.1 (et le paquet ValueTuple pour C # 7), mais vous pouvez donner le nom à vos éléments de tuple explicitement si nécessaire ou utiliser plutôt un simple Tuple<T1,T2>.

0
Jay Haybatov

Vous avez besoin:

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

(c #> 7.1)

0
kofifus