Delen via


Initializers voor objecten en verzamelingen (C#-programmeerhandleiding)

Met C# kunt u een object of verzameling instantiëren en lidtoewijzingen uitvoeren in één instructie.

Object-initialisatoren

Met object-initialisatiefuncties kunt u waarden toewijzen aan toegankelijke velden of eigenschappen van een object wanneer u het maakt. U hoeft geen constructor aan te roepen en vervolgens toewijzingsinstructies te gebruiken. Met de syntaxis van de object-initialisatiefunctie kunt u argumenten opgeven voor een constructor of de argumenten en haakjes weglaten. Zie Object initializers gebruiken (stijlregel IDE0017) voor hulp bij het consistent gebruiken van object-initializers. In het volgende voorbeeld ziet u hoe u een object-initialisatiefunctie gebruikt met een benoemd type Caten hoe u de parameterloze constructor aanroept. Let op het gebruik van automatisch geïmplementeerde eigenschappen in de Cat klasse. Zie Eigenschappen automatisch geïmplementeerd voor meer informatie.

public class Cat
{
    // Automatically implemented properties.
    public int Age { get; set; }
    public string? Name { get; set; }

    public Cat()
    {
    }

    public Cat(string name)
    {
        this.Name = name;
    }
}
Cat cat = new Cat { Age = 10, Name = "Fluffy" };
Cat sameCat = new Cat("Fluffy"){ Age = 10 };

Met de syntaxis van de object-initialisatiefunctie kunt u een exemplaar maken en het zojuist gemaakte object, met de toegewezen eigenschappen, toewijzen aan de variabele in de toewijzing.

Beginnend met geneste objecteigenschappen, kunt u de syntaxis van de object-initialisatiefunctie zonder het new trefwoord gebruiken. Met deze syntaxis Property = { ... }kunt u leden van bestaande geneste objecten initialiseren, wat handig is met alleen-lezeneigenschappen. Zie Object Initializers met class-typed eigenschappen voor meer informatie.

Object-initialisatiefuncties kunnen indexeerfuncties instellen, naast het toewijzen van velden en eigenschappen. Houd rekening met deze basisklasse Matrix :

public class Matrix
{
    private double[,] storage = new double[3, 3];

    public double this[int row, int column]
    {
        // The embedded array will throw out of range exceptions as appropriate.
        get { return storage[row, column]; }
        set { storage[row, column] = value; }
    }
}

U kunt de identiteitsmatrix initialiseren met behulp van de volgende code:

var identity = new Matrix
{
    [0, 0] = 1.0,
    [0, 1] = 0.0,
    [0, 2] = 0.0,

    [1, 0] = 0.0,
    [1, 1] = 1.0,
    [1, 2] = 0.0,

    [2, 0] = 0.0,
    [2, 1] = 0.0,
    [2, 2] = 1.0,
};

Elke toegankelijke indexeerfunctie die een toegankelijke setter bevat, kan worden gebruikt als een van de expressies in een object-initialisatiefunctie, ongeacht het aantal of de typen argumenten. De indexargumenten vormen de linkerkant van de toewijzing en de waarde is de rechterkant van de expressie. De volgende initialisatoren zijn bijvoorbeeld allemaal geldig als IndexersExample de juiste indexers heeft.

var thing = new IndexersExample
{
    name = "object one",
    [1] = '1',
    [2] = '4',
    [3] = '9',
    Size = Math.PI,
    ['C',4] = "Middle C"
}

Voordat de voorgaande code moet worden gecompileerd, moet het IndexersExample type de volgende leden hebben:

public string name;
public double Size { set { ... }; }
public char this[int i] { set { ... }; }
public string this[char c, int i] { set { ... }; }

Object-initialisatiefuncties met anonieme typen

Hoewel u object-initialisatiefuncties in elke context kunt gebruiken, zijn ze vooral handig in Language-Integrated QUERY-expressies (LINQ). Query-expressies gebruiken vaak anonieme typen, die u alleen kunt initialiseren met behulp van een object-initialisatiefunctie, zoals wordt weergegeven in de volgende declaratie.

var pet = new { Age = 10, Name = "Fluffy" };

Met behulp van anonieme typen kan de select component in een LINQ-queryexpressie objecten van de oorspronkelijke reeks transformeren in objecten waarvan de waarde en vorm verschillen van het origineel. Mogelijk wilt u slechts een deel van de informatie uit elk object in een reeks opslaan. In het volgende voorbeeld wordt ervan uitgegaan dat een productobject (p) veel velden en methoden bevat en dat u alleen geïnteresseerd bent in het maken van een reeks objecten die de productnaam en de eenheidsprijs bevatten.

var productInfos =
    from p in products
    select new { p.ProductName, p.UnitPrice };

Wanneer u deze query uitvoert, bevat de productInfos variabele een reeks objecten die u in een foreach instructie kunt openen, zoals wordt weergegeven in dit voorbeeld:

foreach(var p in productInfos){...}

Elk object in het nieuwe anonieme type heeft twee openbare eigenschappen die dezelfde namen ontvangen als de eigenschappen of velden in het oorspronkelijke object. U kunt ook de naam van een veld wijzigen wanneer u een anoniem type maakt. In het volgende voorbeeld wordt de naam van het UnitPrice veld gewijzigd in Price.

select new {p.ProductName, Price = p.UnitPrice};

Object-initialisatiefuncties met de required modifier

Gebruik het required trefwoord om aanroepers te dwingen de waarde van een eigenschap of veld in te stellen met behulp van een object-initialisatiefunctie. U hoeft de vereiste eigenschappen niet in te stellen als constructorparameters. De compiler zorgt ervoor dat alle bellers deze waarden initialiseren.

public class Pet
{
    public required int Age;
    public string Name;
}

// `Age` field is necessary to be initialized.
// You don't need to initialize `Name` property
var pet = new Pet() { Age = 10};

// Compiler error:
// Error CS9035 Required member 'Pet.Age' must be set in the object initializer or attribute constructor.
// var pet = new Pet();

Het is een gebruikelijke procedure om te garanderen dat uw object correct is geïnitialiseerd, met name wanneer u meerdere velden of eigenschappen hebt die u wilt beheren en deze niet allemaal in de constructor wilt opnemen.

Object-initialisatiefuncties met de init toegangsfunctie

Met behulp van een init accessor kunt u ervoor zorgen dat het object niet wordt gewijzigd na initialisatie. Hiermee kunt u de instelling van de eigenschapswaarde beperken.

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

// The `LastName` property can be set only during initialization. It CAN'T be modified afterwards.
// The `FirstName` property can be modified after initialization.
var pet = new Person() { FirstName = "Joe", LastName = "Doe"};

// You can assign the FirstName property to a different value.
pet.FirstName = "Jane";

// Compiler error:
// Error CS8852  Init - only property or indexer 'Person.LastName' can only be assigned in an object initializer,
//               or on 'this' or 'base' in an instance constructor or an 'init' accessor.
// pet.LastName = "Kowalski";

Vereiste init-only eigenschappen ondersteunen onveranderbare structuren en bieden natuurlijke syntaxis voor gebruikers van het type.

Objectinitiitiizers met klasse-type-eigenschappen

Wanneer u objecten initialiseert met class-typed eigenschappen, kunt u twee verschillende syntaxis gebruiken:

  1. Object initializer zonder new trefwoord: Property = { ... }
  2. Object initializer met new trefwoord: Property = new() { ... }

Deze syntaxis gedraagt zich anders. In het volgende voorbeeld ziet u beide benaderingen:

public class HowToClassTypedInitializer
{
    public class EmbeddedClassTypeA
    {
        public int I { get; set; }
        public bool B { get; set; }
        public string S { get; set; }
        public EmbeddedClassTypeB ClassB { get; set; }

        public override string ToString() => $"{I}|{B}|{S}|||{ClassB}";

        public EmbeddedClassTypeA()
        {
            Console.WriteLine($"Entering EmbeddedClassTypeA constructor. Values are: {this}");
            I = 3;
            B = true;
            S = "abc";
            ClassB = new() { BB = true, BI = 43 };
            Console.WriteLine($"Exiting EmbeddedClassTypeA constructor. Values are: {this})");
        }
    }

    public class EmbeddedClassTypeB
    {
        public int BI { get; set; }
        public bool BB { get; set; }
        public string BS { get; set; }

        public override string ToString() => $"{BI}|{BB}|{BS}";

        public EmbeddedClassTypeB()
        {
            Console.WriteLine($"Entering EmbeddedClassTypeB constructor. Values are: {this}");
            BI = 23;
            BB = false;
            BS = "BBBabc";
            Console.WriteLine($"Exiting EmbeddedClassTypeB constructor. Values are: {this})");
        }
    }

    public static void Main()
    {
        var a = new EmbeddedClassTypeA
        {
            I = 103,
            B = false,
            ClassB = { BI = 100003 }
        };
        Console.WriteLine($"After initializing EmbeddedClassTypeA: {a}");

        var a2 = new EmbeddedClassTypeA
        {
            I = 103,
            B = false,
            ClassB = new() { BI = 100003 } //New instance
        };
        Console.WriteLine($"After initializing EmbeddedClassTypeA a2: {a2}");
    }

    // Output:
    //Entering EmbeddedClassTypeA constructor Values are: 0|False||||
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)
    //After initializing EmbeddedClassTypeA: 103|False|abc|||100003|True|BBBabc
    //Entering EmbeddedClassTypeA constructor Values are: 0|False||||
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)
    //Entering EmbeddedClassTypeB constructor Values are: 0|False|
    //Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)
    //After initializing EmbeddedClassTypeA a2: 103|False|abc|||100003|False|BBBabc
}

Belangrijkste verschillen

  • Zonder new trefwoord (ClassB = { BI = 100003 }): Met deze syntaxis wordt het bestaande exemplaar van de eigenschap gewijzigd die door de objectconstructor is gemaakt. Het roept initialisatiefuncties voor leden aan op het bestaande object.

  • Met new sleutelwoord (ClassB = new() { BI = 100003 }): Deze syntaxis maakt een nieuw exemplaar aan en wijst het toe aan de eigenschap, waarbij een bestaand exemplaar wordt vervangen.

De initializer zonder new herbruikt de huidige instantie. In het vorige voorbeeld zijn de waarden van ClassB: 100003 (nieuwe waarde toegewezen), true (behouden van de initialisatie van EmbeddedClassTypeA), BBBabc (ongewijzigde standaardwaarde van EmbeddedClassTypeB).

Object initializers zonder new voor alleen-lezen eigenschappen

De syntaxis zonder new is handig met alleen-lezeneigenschappen. U kunt geen nieuw exemplaar toewijzen, maar u kunt de leden van het bestaande exemplaar nog steeds initialiseren:

public class ReadOnlyPropertyExample
{
    public class Settings
    {
        public string Theme { get; set; } = "Light";
        public int FontSize { get; set; } = 12;
    }

    public class Application
    {
        public string Name { get; set; } = "";
        // This property is read-only - it can only be set during construction
        public Settings AppSettings { get; } = new();
    }

    public static void Example()
    {
        // You can still initialize the nested object's properties
        // even though AppSettings property has no setter
        var app = new Application
        {
            Name = "MyApp",
            AppSettings = { Theme = "Dark", FontSize = 14 }
        };

        // This would cause a compile error because AppSettings has no setter:
        // app.AppSettings = new Settings { Theme = "Dark", FontSize = 14 };

        Console.WriteLine($"App: {app.Name}, Theme: {app.AppSettings.Theme}, Font Size: {app.AppSettings.FontSize}");
    }
}

** Met deze aanpak kunt u geneste objecten initialiseren, zelfs als de omvattende eigenschap geen setter heeft.

Initialisatie van verzamelingen

Met initialisatie voor verzamelingen kunt u een of meer elementinitialisaties opgeven wanneer u een verzamelingstype initialiseert dat IEnumerable implementeert en beschikt over een Add-methode met de juiste handtekening als instantiemethode of als extensiemethode. De element-initialisatiefuncties kunnen een waarde, een expressie of een object-initialisatiefunctie zijn. Met behulp van een initialisatiefunctie voor verzamelingen hoeft u niet meerdere aanroepen op te geven; de compiler voegt de aanroepen automatisch toe. Zie Initializers voor verzamelingen consistent gebruiken (stijlregel IDE0028) voor hulp bij het consistent gebruik van initialisatieprogramma's voor verzamelingen. Initializers voor verzamelingen zijn ook handig in LINQ-query's.

In het volgende voorbeeld ziet u twee eenvoudige initialisatieprogramma's voor verzamelingen:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> digits2 = new List<int> { 0 + 1, 12 % 3, MakeInt() };

De volgende initialisatiefunctie voor verzamelingen maakt gebruik van objectinitialiseerde objecten van de Cat klasse die in een eerder voorbeeld is gedefinieerd. De afzonderlijke objectinitialiseerders worden omsloten door accolades en gescheiden door komma's.

List<Cat> cats =
[
    new Cat { Name = "Sylvester", Age = 8 },
    new Cat { Name = "Whiskers", Age = 2 },
    new Cat { Name = "Sasha", Age = 14 }
];

U kunt null als een element in een verzamelingsinitializer opgeven als de Add-methode van de verzameling dit toestaat.

List<Cat?> moreCats = new List<Cat?>
{
    new Cat{ Name = "Furrytail", Age=5 },
    new Cat{ Name = "Peaches", Age=4 },
    null
};

U kunt een verspreid element gebruiken om één lijst te maken waarmee andere lijsten of lijsten worden gekopieerd.

List<Cat> allCats = [.. cats, .. moreCats];

En voeg extra elementen toe en maak gebruik van een verspreid element.

List<Cat> additionalCats = [.. cats, new Cat { Name = "Furrytail", Age = 5 }, .. moreCats];

U kunt geïndexeerde elementen opgeven als de verzameling indexering voor lezen/schrijven ondersteunt.

var numbers = new Dictionary<int, string>
{
    [7] = "seven",
    [9] = "nine",
    [13] = "thirteen"
};

In het voorgaande voorbeeld wordt er code gegenereerd die de Item[TKey] aanroept om de waarden in te stellen. U kunt ook woordenlijsten en andere associatieve containers initialiseren met behulp van de volgende syntaxis. In plaats van de syntaxis van de indexeerfunctie, met haakjes en een toewijzing, wordt een object met meerdere waarden gebruikt:

var moreNumbers = new Dictionary<int, string>
{
    {19, "nineteen" },
    {23, "twenty-three" },
    {42, "forty-two" }
};

In dit initialisatievoorbeeld roept Add(TKey, TValue) aan om de drie items toe te voegen aan het woordenboek. Deze twee verschillende manieren om associatieverzamelingen te initialiseren, hebben een iets ander gedrag vanwege de methodes die de compiler genereert. Beide varianten werken met de Dictionary klasse. Andere typen ondersteunen mogelijk slechts één of de andere op basis van hun openbare API.

Argumenten voor verzamelingsexpressie

Gebruik vanaf C# 15 het with(...) element als het eerste element in een verzamelingsexpressie om argumenten door te geven aan de constructor van de verzameling. Met deze functie kunt u capaciteit, vergelijkingsparameters of andere constructorparameters rechtstreeks in de syntaxis van de verzamelingsexpressie opgeven:

public static void CollectionExpressionWithArgumentsExample()
{
    string[] values = ["one", "two", "three"];

    // Use with() to pass capacity to the List<T> constructor
    List<string> names = [with(capacity: values.Length * 2), .. values];

    Console.WriteLine($"Created List<string> with capacity: {names.Capacity}");
    Console.WriteLine($"List contains {names.Count} elements: [{string.Join(", ", names)}]");

    // Use with() to pass a comparer to the HashSet<T> constructor
    HashSet<string> caseInsensitiveSet = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO"];
    // caseInsensitiveSet contains only one element because "Hello" and "HELLO" are equal

    Console.WriteLine($"HashSet with case-insensitive comparer contains {caseInsensitiveSet.Count} elements: [{string.Join(", ", caseInsensitiveSet)}]");
    Console.WriteLine("Note: 'Hello' and 'HELLO' are treated as the same element due to OrdinalIgnoreCase comparer");
}

Zie Expressieargumenten voor verzamelingen voor meer informatie over argumenten voor verzamelingsexpressies, waaronder ondersteunde doeltypen en beperkingen.

Initialisatie van objecten met het instellen van alleen-lezen verzamelings-eigenschappen

Sommige klassen hebben collectie-eigenschappen waarbij de eigenschap alleen-lezen is, zoals de Cats eigenschap van CatOwner in het volgende geval:

public class CatOwner
{
    public IList<Cat> Cats { get; } = new List<Cat>();
}

U kunt de syntaxis van de initialisatiefunctie voor verzamelingen die eerder is besproken, niet gebruiken omdat aan de eigenschap geen nieuwe lijst kan worden toegewezen:

CatOwner owner = new CatOwner
{
    Cats = new List<Cat>
    {
        new Cat{ Name = "Sylvester", Age=8 },
        new Cat{ Name = "Whiskers", Age=2 },
        new Cat{ Name = "Sasha", Age=14 }
    }
};

U kunt echter nieuwe vermeldingen aan Cats toevoegen door de initialisatiesyntaxis te gebruiken en het weglaten van de lijstcreatie (new List<Cat>), zoals wordt weergegeven in het volgende voorbeeld:

CatOwner owner = new CatOwner
{
    Cats =
    {
        new Cat{ Name = "Sylvester", Age=8 },
        new Cat{ Name = "Whiskers", Age=2 },
        new Cat{ Name = "Sasha", Age=14 }
    }
};

De set vermeldingen die moeten worden toegevoegd, wordt omgeven door accolades. De voorgaande code is identiek aan het schrijven:

CatOwner owner = new ();
owner.Cats.Add(new Cat{ Name = "Sylvester", Age=8 });
owner.Cats.Add(new Cat{ Name = "Whiskers", Age=2 });
owner.Cats.Add(new Cat{ Name = "Sasha", Age=14 });

Voorbeelden

In het volgende voorbeeld worden de concepten van object- en verzamelingsinitialisators gecombineerd.

public class InitializationSample
{
    public class Cat
    {
        // Automatically implemented properties.
        public int Age { get; set; }
        public string? Name { get; set; }

        public Cat() { }

        public Cat(string name)
        {
            Name = name;
        }
    }

    public static void Main()
    {
        Cat cat = new Cat { Age = 10, Name = "Fluffy" };
        Cat sameCat = new Cat("Fluffy"){ Age = 10 };

        List<Cat> cats =
        [
            new Cat { Name = "Sylvester", Age = 8 },
            new Cat { Name = "Whiskers", Age = 2 },
            new Cat { Name = "Sasha", Age = 14 }
        ];

        List<Cat?> moreCats = new List<Cat?>
        {
            new Cat { Name = "Furrytail", Age = 5 },
            new Cat { Name = "Peaches", Age = 4 },
            null
        };

        List<Cat> allCats = [.. cats, new Cat { Name = "Łapka", Age = 5 }, cat, .. moreCats];

        // Display results.
        foreach (Cat? c in allCats)
        {
            if (c != null)
            {
                System.Console.WriteLine(c.Name);
            }
            else
            {
                System.Console.WriteLine("List element has null value.");
            }
        }
    }
    // Output:
    // Sylvester
    // Whiskers
    // Sasha
    // Łapka
    // Fluffy
    // Furrytail
    // Peaches
    // List element has null value.
}

In het volgende voorbeeld ziet u een object dat IEnumerable implementeert en een Add methode met meerdere parameters bevat. Er wordt een initialisatiefunctie voor verzamelingen gebruikt met meerdere elementen per item in de lijst die overeenkomen met de handtekening van de Add methode.

public class FullExample
{
    class FormattedAddresses : IEnumerable<string>
    {
        private List<string> internalList = new();
        public IEnumerator<string> GetEnumerator() => internalList.GetEnumerator();

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => internalList.GetEnumerator();

        public void Add(string firstname, string lastname,
            string street, string city,
            string state, string zipcode) => internalList.Add($"""
            {firstname} {lastname}
            {street}
            {city}, {state} {zipcode}
            """
            );
    }

    public static void Main()
    {
        FormattedAddresses addresses = new FormattedAddresses()
        {
            {"John", "Doe", "123 Street", "Topeka", "KS", "00000" },
            {"Jane", "Smith", "456 Street", "Topeka", "KS", "00000" }
        };

        Console.WriteLine("Address Entries:");

        foreach (string addressEntry in addresses)
        {
            Console.WriteLine("\r\n" + addressEntry);
        }
    }

    /*
        * Prints:

        Address Entries:

        John Doe
        123 Street
        Topeka, KS 00000

        Jane Smith
        456 Street
        Topeka, KS 00000
        */
}

Add methoden kunnen het params trefwoord gebruiken om een variabel aantal argumenten te gebruiken, zoals wordt weergegeven in het volgende voorbeeld. In dit voorbeeld ziet u ook de aangepaste implementatie van een indexeerfunctie om een verzameling te initialiseren met behulp van indexen. Vanaf C# 13 is de params parameter niet beperkt tot een matrix. Dit kan een verzamelingstype of interface zijn.

public class DictionaryExample
{
    class RudimentaryMultiValuedDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, List<TValue>>> where TKey : notnull
    {
        private Dictionary<TKey, List<TValue>> internalDictionary = new Dictionary<TKey, List<TValue>>();

        public IEnumerator<KeyValuePair<TKey, List<TValue>>> GetEnumerator() => internalDictionary.GetEnumerator();

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => internalDictionary.GetEnumerator();

        public List<TValue> this[TKey key]
        {
            get => internalDictionary[key];
            set => Add(key, value);
        }

        public void Add(TKey key, params TValue[] values) => Add(key, (IEnumerable<TValue>)values);

        public void Add(TKey key, IEnumerable<TValue> values)
        {
            if (!internalDictionary.TryGetValue(key, out List<TValue>? storedValues))
            {
                internalDictionary.Add(key, storedValues = new());
            }
            storedValues.AddRange(values);
        }
    }

    public static void Main()
    {
        RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary1
            = new RudimentaryMultiValuedDictionary<string, string>()
            {
                {"Group1", "Bob", "John", "Mary" },
                {"Group2", "Eric", "Emily", "Debbie", "Jesse" }
            };
        RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary2
            = new RudimentaryMultiValuedDictionary<string, string>()
            {
                ["Group1"] = new List<string>() { "Bob", "John", "Mary" },
                ["Group2"] = new List<string>() { "Eric", "Emily", "Debbie", "Jesse" }
            };
        RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary3
            = new RudimentaryMultiValuedDictionary<string, string>()
            {
                {"Group1", new string []{ "Bob", "John", "Mary" } },
                { "Group2", new string[]{ "Eric", "Emily", "Debbie", "Jesse" } }
            };

        Console.WriteLine("Using first multi-valued dictionary created with a collection initializer:");

        foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary1)
        {
            Console.WriteLine($"\r\nMembers of group {group.Key}: ");

            foreach (string member in group.Value)
            {
                Console.WriteLine(member);
            }
        }

        Console.WriteLine("\r\nUsing second multi-valued dictionary created with a collection initializer using indexing:");

        foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary2)
        {
            Console.WriteLine($"\r\nMembers of group {group.Key}: ");

            foreach (string member in group.Value)
            {
                Console.WriteLine(member);
            }
        }
        Console.WriteLine("\r\nUsing third multi-valued dictionary created with a collection initializer using indexing:");

        foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary3)
        {
            Console.WriteLine($"\r\nMembers of group {group.Key}: ");

            foreach (string member in group.Value)
            {
                Console.WriteLine(member);
            }
        }
    }

    /*
        * Prints:

        Using first multi-valued dictionary created with a collection initializer:

        Members of group Group1:
        Bob
        John
        Mary

        Members of group Group2:
        Eric
        Emily
        Debbie
        Jesse

        Using second multi-valued dictionary created with a collection initializer using indexing:

        Members of group Group1:
        Bob
        John
        Mary

        Members of group Group2:
        Eric
        Emily
        Debbie
        Jesse

        Using third multi-valued dictionary created with a collection initializer using indexing:

        Members of group Group1:
        Bob
        John
        Mary

        Members of group Group2:
        Eric
        Emily
        Debbie
        Jesse
        */
}