C# Language Prise en charge linguistique pour Tuples


Exemple

Les bases

Un tuple est une liste ordonnée et finie d'éléments. Les tuples sont couramment utilisés en programmation comme moyen de travailler collectivement avec une seule entité au lieu de travailler individuellement avec chacun des éléments du tuple et de représenter des lignes individuelles (par exemple des "enregistrements") dans une base de données relationnelle.

En C # 7.0, les méthodes peuvent avoir plusieurs valeurs de retour. Dans les coulisses, le compilateur utilisera la nouvelle structure ValueTuple .

public (int sum, int count) GetTallies() 
{
    return (1, 2);
}

Remarque : pour que cela fonctionne dans Visual Studio 2017, vous devez obtenir le package System.ValueTuple .

Si un résultat de méthode retournant un tuple est affecté à une seule variable, vous pouvez accéder aux membres par leurs noms définis sur la signature de la méthode:

var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2

Déconstruction de Tuple

La déconstruction des tuples sépare un tuple en ses parties.

Par exemple, l'appel de GetTallies et l'affectation de la valeur de retour à deux variables distinctes déconstruit le tuple en ces deux variables:

(int tallyOne, int tallyTwo) = GetTallies();

var fonctionne aussi:

(var s, var c) = GetTallies();

Vous pouvez également utiliser une syntaxe plus courte, avec var dehors de () :

var (s, c) = GetTallies();

Vous pouvez également décomposer en variables existantes:

int s, c;
(s, c) = GetTallies();

L'échange est maintenant beaucoup plus simple (pas de variable temporaire nécessaire):

(b, a) = (a, b);

Fait intéressant, tout objet peut être déconstruit en définissant une méthode Deconstruct dans la classe:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

var person = new Person { FirstName = "John", LastName = "Smith" };
var (localFirstName, localLastName) = person;

Dans ce cas, la (localFirstName, localLastName) = person appelle Deconstruct sur la person .

La déconstruction peut même être définie dans une méthode d'extension. Ceci est équivalent à ce qui précède:

public static class PersonExtensions
{
    public static void Deconstruct(this Person person, out string firstName, out string lastName)
    {
        firstName = person.FirstName;
        lastName = person.LastName;
    }
}

var (localFirstName, localLastName) = person;

Une approche alternative pour la classe Person est de définir le Name lui-même comme un Tuple . Considérer ce qui suit:

class Person
{
    public (string First, string Last) Name { get; }

    public Person((string FirstName, string LastName) name)
    {
        Name = name;
    }
}

Ensuite, vous pouvez instancier une personne comme ça (où nous pouvons prendre un tuple comme argument):

var person = new Person(("Jane", "Smith"));

var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last;   // "Smith"

Initialisation du tuple

Vous pouvez également créer arbitrairement des tuples dans le code:

var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John

Console.WriteLine(name.Item2);
// Outputs Smith

Lors de la création d'un tuple, vous pouvez attribuer des noms d'élément ad hoc aux membres du tuple:

var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John

Type d'inférence

Plusieurs tuples définis avec la même signature (types et nombre correspondants) seront déduits comme types correspondants. Par exemple:

public (int sum, double average) Measure(List<int> items)
{
    var stats = (sum: 0, average: 0d);
    stats.sum = items.Sum();
    stats.average = items.Average();
    return stats;
}

stats peuvent être retournées car la déclaration de la variable stats et la signature de retour de la méthode correspondent.

Noms de champ de réflexion et de tuple

Les noms de membres n'existent pas à l'exécution. Reflection considérera les tuples avec le même nombre et les mêmes types de membres, même si les noms des membres ne correspondent pas. La conversion d'un tuple en object , puis en un tuple avec les mêmes types de membres, mais avec des noms différents, ne provoquera pas non plus d'exception.

Alors que la classe ValueTuple elle-même ne conserve pas les informations pour les noms de membres, les informations sont disponibles par réflexion dans un TupleElementNamesAttribute. Cet attribut n'est pas appliqué au tuple lui-même mais aux paramètres de méthode, aux valeurs de retour, aux propriétés et aux champs. Cela permet de conserver les noms d'élément de tuple dans les assemblages, c'est-à-dire que si une méthode retourne (nom de chaîne, int nombre), les noms et les noms seront disponibles pour les appelants de la méthode. "nom" et "compte".

Utiliser avec des génériques et async

Les nouvelles fonctionnalités de tuple (utilisant le type ValueTuple sous-jacent) prennent entièrement en charge les génériques et peuvent être utilisées comme paramètre de type générique. Cela permet de les utiliser avec le modèle async / await :

public async Task<(string value, int count)> GetValueAsync()
{
    string fooBar = await _stackoverflow.GetStringAsync();
    int num = await _stackoverflow.GetIntAsync();

    return (fooBar, num);
}

Utiliser avec des collections

Il peut être avantageux d'avoir une collection de tuples dans (par exemple) un scénario où vous essayez de trouver un tuple correspondant aux conditions pour éviter le branchement du code.

Exemple:

private readonly List<Tuple<string, string, string>> labels = new List<Tuple<string, string, string>>()
{
    new Tuple<string, string, string>("test1", "test2", "Value"),
    new Tuple<string, string, string>("test1", "test1", "Value2"),
    new Tuple<string, string, string>("test2", "test2", "Value3"),
};

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.Item1 == firstElement && w.Item2 == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.Item3;
}

Avec les nouveaux tuples peuvent devenir:

private readonly List<(string firstThingy, string secondThingyLabel, string foundValue)> labels = new List<(string firstThingy, string secondThingyLabel, string foundValue)>()
{
    ("test1", "test2", "Value"),
    ("test1", "test1", "Value2"),
    ("test2", "test2", "Value3"),
}

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.firstThingy == firstElement && w.secondThingyLabel == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.foundValue;
}

Bien que le nom donné à l'exemple ci-dessus soit assez générique, l'idée d'étiquettes pertinentes permet une meilleure compréhension de ce qui est tenté dans le code par rapport à "item1", "item2" et "item3".

Différences entre ValueTuple et Tuple

La principale raison de l'introduction de ValueTuple est la performance.

Nom du type ValueTuple Tuple
Classe ou structure struct class
Mutabilité (changer les valeurs après la création) mutable immuable
Nommer les membres et autre support linguistique Oui non (à déterminer )

Les références