Clausola group (Riferimento C#)

La clausola group restituisce una sequenza di oggetti IGrouping<TKey,TElement> che contengono zero o più elementi corrispondenti al valore chiave per il gruppo. Ad esempio, è possibile raggruppare una sequenza di stringhe in base alla prima lettera di ogni stringa. In questo caso, la prima lettera è la chiave con tipo char e viene archiviata nella proprietà Key di ogni oggetto IGrouping<TKey,TElement>. Il compilatore deduce automaticamente il tipo della chiave.

È possibile terminare un'espressione di query con una clausola group, come illustrato nell'esempio seguente:

// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery1 =
    from student in students
    group student by student.Last[0];

Per eseguire operazioni di query aggiuntive per ogni gruppo, è possibile specificare un identificatore temporaneo usando la parola chiave contestuale into. Quando si usa into, è necessario continuare a eseguire la query ed eventualmente terminarla con un'istruzione select o un'altra clausola group, come illustrato nel seguente estratto di codice:

// Group students by the first letter of their last name
// Query variable is an IEnumerable<IGrouping<char, Student>>
var studentQuery2 =
    from student in students
    group student by student.Last[0] into g
    orderby g.Key
    select g;

Esempi di utilizzo di più completi di group con e senza into sono disponibili nella sezione Esempio di questo articolo.

Enumerazione dei risultati di una query con raggruppamento

Poiché gli oggetti IGrouping<TKey,TElement> prodotti da una query group sono essenzialmente un elenco di elenchi, è necessario usare un ciclo foreach nidificato per accedere agli elementi di ogni gruppo. Il ciclo esterno esegue l'iterazione delle chiavi del gruppo e il ciclo interno esegue l'iterazione di ogni voce nel gruppo. Un gruppo può avere una chiave ma non elementi. Di seguito è riportato il ciclo foreach che esegue la query negli esempi di codice precedenti:

// Iterate group items with a nested foreach. This IGrouping encapsulates
// a sequence of Student objects, and a Key of type char.
// For convenience, var can also be used in the foreach statement.
foreach (IGrouping<char, Student> studentGroup in studentQuery2)
{
     Console.WriteLine(studentGroup.Key);
     // Explicit type for student could also be used here.
     foreach (var student in studentGroup)
     {
         Console.WriteLine("   {0}, {1}", student.Last, student.First);
     }
 }

Tipi di chiave

Le chiavi di raggruppamento possono essere di qualsiasi tipo, ad esempio una stringa, un tipo numerico incorporato, un tipo con nome definito dall'utente o un tipo anonimo.

Raggruppamento per stringa

Negli esempi di codice precedenti è stato usato un oggetto char. Si potrebbe specificare in alternativa una chiave di stringa, ad esempio il cognome completo:

// Same as previous example except we use the entire last name as a key.
// Query variable is an IEnumerable<IGrouping<string, Student>>
var studentQuery3 =
    from student in students
    group student by student.Last;

Raggruppamento per valore booleano

L'esempio seguente illustra l'uso di un valore booleano per una chiave in modo da dividere i risultati in due gruppi. Si noti che il valore è generato da una sottoespressione nella clausola group.

class GroupSample1
{
    // The element type of the data source.
    public class Student
    {
        public required string First { get; init; }
        public required string Last { get; init; }
        public required int ID { get; init; }
        public required List<int> Scores;
    }

    public static List<Student> GetStudents()
    {
        // Use a collection initializer to create the data source. Note that each element
        //  in the list contains an inner sequence of scores.
        List<Student> students =
        [
           new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= [97, 72, 81, 60]},
           new Student {First="Claire", Last="O'Donnell", ID=112, Scores= [75, 84, 91, 39]},
           new Student {First="Sven", Last="Mortensen", ID=113, Scores= [99, 89, 91, 95]},
           new Student {First="Cesar", Last="Garcia", ID=114, Scores= [72, 81, 65, 84]},
           new Student {First="Debra", Last="Garcia", ID=115, Scores= [97, 89, 85, 82]}
        ];

        return students;
    }

    static void Main()
    {
        // Obtain the data source.
        List<Student> students = GetStudents();

        // Group by true or false.
        // Query variable is an IEnumerable<IGrouping<bool, Student>>
        var booleanGroupQuery =
            from student in students
            group student by student.Scores.Average() >= 80; //pass or fail!

        // Execute the query and access items in each group
        foreach (var studentGroup in booleanGroupQuery)
        {
            Console.WriteLine(studentGroup.Key == true ? "High averages" : "Low averages");
            foreach (var student in studentGroup)
            {
                Console.WriteLine("   {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
            }
        }
    }
}
/* Output:
  Low averages
   Omelchenko, Svetlana:77.5
   O'Donnell, Claire:72.25
   Garcia, Cesar:75.5
  High averages
   Mortensen, Sven:93.5
   Garcia, Debra:88.25
*/

Raggruppamento per intervallo numerico

Nell'esempio seguente viene usata un'espressione per creare le chiavi di raggruppamento numeriche che rappresentano un intervallo percentile. Si noti l'uso di let come comoda posizione di archiviazione del risultato della chiamata a un metodo, in modo da non dover chiamare il metodo due volte nella clausola group. Per altre informazioni su come usare in modo sicuro i metodi nelle espressioni di query, vedere Gestire le eccezioni nelle espressioni di query.

class GroupSample2
{
    // The element type of the data source.
    public class Student
    {
        public required string First { get; init; }
        public required string Last { get; init; }
        public required int ID { get; init; }
        public required List<int> Scores;
    }

    public static List<Student> GetStudents()
    {
        // Use a collection initializer to create the data source. Note that each element
        //  in the list contains an inner sequence of scores.
        List<Student> students =
        [
           new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= [97, 72, 81, 60]},
           new Student {First="Claire", Last="O'Donnell", ID=112, Scores= [75, 84, 91, 39]},
           new Student {First="Sven", Last="Mortensen", ID=113, Scores= [99, 89, 91, 95]},
           new Student {First="Cesar", Last="Garcia", ID=114, Scores= [72, 81, 65, 84]},
           new Student {First="Debra", Last="Garcia", ID=115, Scores= [97, 89, 85, 82]}
        ];

        return students;
    }

    // This method groups students into percentile ranges based on their
    // grade average. The Average method returns a double, so to produce a whole
    // number it is necessary to cast to int before dividing by 10.
    static void Main()
    {
        // Obtain the data source.
        List<Student> students = GetStudents();

        // Write the query.
        var studentQuery =
            from student in students
            let avg = (int)student.Scores.Average()
            group student by (avg / 10) into g
            orderby g.Key
            select g;

        // Execute the query.
        foreach (var studentGroup in studentQuery)
        {
            int temp = studentGroup.Key * 10;
            Console.WriteLine("Students with an average between {0} and {1}", temp, temp + 10);
            foreach (var student in studentGroup)
            {
                Console.WriteLine("   {0}, {1}:{2}", student.Last, student.First, student.Scores.Average());
            }
        }
    }
}
/* Output:
     Students with an average between 70 and 80
       Omelchenko, Svetlana:77.5
       O'Donnell, Claire:72.25
       Garcia, Cesar:75.5
     Students with an average between 80 and 90
       Garcia, Debra:88.25
     Students with an average between 90 and 100
       Mortensen, Sven:93.5
 */

Raggruppamento per chiavi composte

Usare una chiave composta se si vuole raggruppare gli elementi in base a più di una chiave. Per creare una chiave composta, usare un tipo anonimo o un tipo denominato per contenere l'elemento key. Nell'esempio seguente si suppone che una classe Person sia stata dichiarata con i membri denominati surname e city. La clausola group determina la creazione di un gruppo separato per ogni insieme di persone con lo stesso cognome e la stessa città.

group person by new {name = person.surname, city = person.city};

Usare un tipo denominato se è necessario passare la variabile di query a un altro metodo. Creare una classe speciale usando proprietà implementate automaticamente per le chiavi e quindi eseguire l'override dei metodi Equals e GetHashCode. È anche possibile usare uno struct e in questo caso non è strettamente necessario eseguire l'override dei metodi. Per altre informazioni, vedere Come implementare una classe leggera con proprietà implementate automaticamente e Come eseguire una query per trovare i file duplicati in un albero di directory. Il secondo articolo contiene un esempio di codice che illustra come usare una chiave composta con un tipo denominato.

Esempio 1

Nell'esempio seguente viene illustrato il modello standard per l'ordinamento dei dati di origine nei gruppi quando non viene applicata una logica di query aggiuntiva ai gruppi. L'operazione è definita raggruppamento senza continuazione. Gli elementi in una matrice di stringhe vengono raggruppati in base alla prima lettera. Il risultato della query è un tipo IGrouping<TKey,TElement> che contiene una proprietà pubblica Key di tipo char e una raccolta IEnumerable<T> che contiene ogni elemento nel raggruppamento.

Il risultato di una clausola group è una sequenza di sequenze. Di conseguenza, per accedere ai singoli elementi all'interno di ogni gruppo restituito, usare un ciclo foreach nidificato all'interno del ciclo che esegue l'iterazione delle chiavi di raggruppamento, come illustrato nell'esempio seguente.

class GroupExample1
{
    static void Main()
    {
        // Create a data source.
        string[] words = ["blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese"];

        // Create the query.
        var wordGroups =
            from w in words
            group w by w[0];

        // Execute the query.
        foreach (var wordGroup in wordGroups)
        {
            Console.WriteLine("Words that start with the letter '{0}':", wordGroup.Key);
            foreach (var word in wordGroup)
            {
                Console.WriteLine(word);
            }
        }
    }
}
/* Output:
      Words that start with the letter 'b':
        blueberry
        banana
      Words that start with the letter 'c':
        chimpanzee
        cheese
      Words that start with the letter 'a':
        abacus
        apple
     */

Esempio 2

In questo esempio viene illustrato come eseguire la logica aggiuntiva per i gruppi dopo che sono stati creati, usando una continuazione con into. Per altre informazioni, vedere into. Nell'esempio seguente viene eseguita una query di ogni gruppo per selezionare solo quelli il cui valore della chiave è una vocale.

class GroupClauseExample2
{
    static void Main()
    {
        // Create the data source.
        string[] words2 = ["blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese", "elephant", "umbrella", "anteater"];

        // Create the query.
        var wordGroups2 =
            from w in words2
            group w by w[0] into grps
            where (grps.Key == 'a' || grps.Key == 'e' || grps.Key == 'i'
                   || grps.Key == 'o' || grps.Key == 'u')
            select grps;

        // Execute the query.
        foreach (var wordGroup in wordGroups2)
        {
            Console.WriteLine("Groups that start with a vowel: {0}", wordGroup.Key);
            foreach (var word in wordGroup)
            {
                Console.WriteLine("   {0}", word);
            }
        }
    }
}
/* Output:
    Groups that start with a vowel: a
        abacus
        apple
        anteater
    Groups that start with a vowel: e
        elephant
    Groups that start with a vowel: u
        umbrella
*/

Osservazioni:

In fase di compilazione le clausole group vengono convertite in chiamate al metodo GroupBy.

Vedi anche