Opérations de projection (C#)

La projection désigne l’opération de transformation d’un objet en une nouvelle forme qui se compose souvent seulement de propriétés utilisées ultérieurement. À l'aide de la projection, vous pouvez créer un nouveau type qui est généré à partir de chaque objet. Vous pouvez projeter une propriété et effectuer une fonction mathématique sur celle-ci. Vous pouvez également projeter l’objet d’origine sans le modifier.

Les méthodes d’opérateurs de requête standard qui effectuent des opérations de projection sont répertoriées dans la section suivante.

Méthodes

Noms des méthodes Description Syntaxe d'expression de requête C# Informations complémentaires
Sélectionnez Projette les valeurs qui sont basées sur une fonction de transformation. select Enumerable.Select
Queryable.Select
SelectMany Projette les séquences de valeurs qui sont basées sur une fonction de transformation, puis les aplatit en une seule séquence. Utilisation de plusieurs clauses from Enumerable.SelectMany
Queryable.SelectMany
Zip Produit une séquence de tuples avec des éléments de 2 à 3 séquences spécifiées. Non applicable. Enumerable.Zip
Queryable.Zip

Select

L’exemple suivant utilise la clause select pour projeter la première lettre de chaque chaîne dans une liste de chaînes.

List<string> words = ["an", "apple", "a", "day"];

var query = from word in words
            select word.Substring(0, 1);

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

List<string> words = ["an", "apple", "a", "day"];

var query = words.Select(word => word.Substring(0, 1));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

SelectMany

L’exemple suivant utilise plusieurs clauses from pour projeter tous les mots de chaque chaîne dans une liste de chaînes.

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = from phrase in phrases
            from word in phrase.Split(' ')
            select word;

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = phrases.SelectMany(phrases => phrases.Split(' '));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

La méthode SelectMany peut également former la combinaison résultant de la mise en correspondance de chaque élément de la première séquence avec chaque élément de la seconde séquence :

var query = from number in numbers
            from letter in letters
            select (number, letter);

foreach (var item in query)
{
    Console.WriteLine(item);
}

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

var method = numbers
    .SelectMany(number => letters,
    (number, letter) => (number, letter));

foreach (var item in method)
{
    Console.WriteLine(item);
}

Zip

Il existe plusieurs surcharges pour l’opérateur de projection Zip. Toutes les méthodes Zip fonctionnent sur des séquences de deux types ou plus potentiellement hétérogènes. Les deux premières surcharges retournent des tuples, avec le type positionnel correspondant à partir des séquences données.

Tenez compte des collections suivantes :

// An int array with 7 elements.
IEnumerable<int> numbers = [1, 2, 3, 4, 5, 6, 7];
// A char array with 6 elements.
IEnumerable<char> letters = ['A', 'B', 'C', 'D', 'E', 'F'];

Pour projeter ces séquences ensemble, utilisez l’opérateur Enumerable.Zip<TFirst,TSecond>(IEnumerable<TFirst>, IEnumerable<TSecond>) :

foreach ((int number, char letter) in numbers.Zip(letters))
{
    Console.WriteLine($"Number: {number} zipped with letter: '{letter}'");
}
// This code produces the following output:
//     Number: 1 zipped with letter: 'A'
//     Number: 2 zipped with letter: 'B'
//     Number: 3 zipped with letter: 'C'
//     Number: 4 zipped with letter: 'D'
//     Number: 5 zipped with letter: 'E'
//     Number: 6 zipped with letter: 'F'

Important

La séquence résultante d’une opération zip n’est jamais plus longue que la séquence la plus courte. Les collections numbers et letters diffèrent par leur longueur, et la séquence qui en résulte omet le dernier élément de la collection numbers, car il n’a rien à compresser.

La deuxième surcharge accepte une séquence third. Créons une autre collection, à savoir emoji :

// A string array with 8 elements.
IEnumerable<string> emoji = [ "🤓", "🔥", "🎉", "👀", "⭐", "💜", "✔", "💯"];

Pour projeter ces séquences ensemble, utilisez l’opérateur Enumerable.Zip<TFirst,TSecond,TThird>(IEnumerable<TFirst>, IEnumerable<TSecond>, IEnumerable<TThird>) :

foreach ((int number, char letter, string em) in numbers.Zip(letters, emoji))
{
    Console.WriteLine(
        $"Number: {number} is zipped with letter: '{letter}' and emoji: {em}");
}
// This code produces the following output:
//     Number: 1 is zipped with letter: 'A' and emoji: 🤓
//     Number: 2 is zipped with letter: 'B' and emoji: 🔥
//     Number: 3 is zipped with letter: 'C' and emoji: 🎉
//     Number: 4 is zipped with letter: 'D' and emoji: 👀
//     Number: 5 is zipped with letter: 'E' and emoji: ⭐
//     Number: 6 is zipped with letter: 'F' and emoji: 💜

Tout comme la surcharge précédente, la méthode Zip projette un tuple, mais cette fois avec trois éléments.

La troisième surcharge accepte un argument Func<TFirst, TSecond, TResult> qui agit comme sélecteur de résultats. Vous pouvez projeter une nouvelle séquence résultante à partir des séquences zippées.

foreach (string result in
    numbers.Zip(letters, (number, letter) => $"{number} = {letter} ({(int)letter})"))
{
    Console.WriteLine(result);
}
// This code produces the following output:
//     1 = A (65)
//     2 = B (66)
//     3 = C (67)
//     4 = D (68)
//     5 = E (69)
//     6 = F (70)

Avec la surcharge précédente Zip, la fonction spécifiée est appliquée aux éléments numbers et letter correspondants, ce qui produit une séquence des résultats string.

Select contre SelectMany

Les deux clauses Select et SelectMany retournent une valeur de résultat (ou des valeurs) à partir des valeurs sources. Select retourne une seule valeur de résultat pour chaque valeur source. Le résultat global est donc une collection qui a le même nombre d’éléments que la collection source. En revanche, SelectMany retourne un résultat global unique qui contient des sous-collections concaténées provenant de chaque valeur source. La fonction de transformation qui est passée comme argument à SelectMany doit retourner une séquence énumérable de valeurs pour chaque valeur source. SelectMany concatène ces séquences énumérables pour créer une grande séquence.

Les deux illustrations suivantes montrent en quoi les actions de ces deux méthodes sont différentes d’un point de vue conceptuel. Dans chaque cas, supposons que la fonction (de transformation) du sélecteur sélectionne le tableau de fleurs (Flowers) à partir de chaque valeur source.

Cette illustration montre de quelle manière Select retourne une collection qui a le même nombre d’éléments que la collection source.

Graphique montrant l’action de Select()

Cette illustration montre de quelle façon SelectMany concatène la séquence intermédiaire de tableaux en une seule valeur de résultat final qui contient chaque valeur de chaque tableau intermédiaire.

Graphique montrant l’action de SelectMany()

Exemple de code

L’exemple suivant compare le comportement de Select et de SelectMany. Le code crée un « bouquet » de fleurs en prenant les éléments de chaque liste de noms de fleurs de la collection source. Dans l’exemple suivant, la « valeur unique » que la fonction de transformation Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) utilise est une collection de valeurs. Cet exemple nécessite la boucle foreach supplémentaire pour énumérer chaque chaîne de chaque sous-séquence.

class Bouquet
{
    public required List<string> Flowers { get; init; }
}

static void SelectVsSelectMany()
{
    List<Bouquet> bouquets =
    [
        new Bouquet { Flowers = ["sunflower", "daisy", "daffodil", "larkspur"] },
        new Bouquet { Flowers = ["tulip", "rose", "orchid"] },
        new Bouquet { Flowers = ["gladiolis", "lily", "snapdragon", "aster", "protea"] },
        new Bouquet { Flowers = ["larkspur", "lilac", "iris", "dahlia"] }
    ];

    IEnumerable<List<string>> query1 = bouquets.Select(bq => bq.Flowers);

    IEnumerable<string> query2 = bouquets.SelectMany(bq => bq.Flowers);

    Console.WriteLine("Results by using Select():");
    // Note the extra foreach loop here.
    foreach (IEnumerable<string> collection in query1)
    {
        foreach (string item in collection)
        {
            Console.WriteLine(item);
        }
    }

    Console.WriteLine("\nResults by using SelectMany():");
    foreach (string item in query2)
    {
        Console.WriteLine(item);
    }
}

Voir aussi