Freigeben über


Objekt- und Auflistungsinitialisierer (C#-Programmierhandbuch)

Mit C# können Sie ein Objekt oder eine Auflistung instanziieren und Memberzuweisungen in einer einzelnen Anweisung ausführen.

Objektinitialisierer

Mit Objektinitialisierern können Sie beim Erstellen Werte allen barrierefreien Feldern oder Eigenschaften eines Objekts zuweisen. Sie müssen keinen Konstruktor aufrufen und dann Zuordnungsanweisungen verwenden. Mit der Objektinitialisierungssyntax können Sie Argumente für einen Konstruktor angeben oder die Argumente und Klammern weglassen. Anleitungen zur konsistenten Verwendung von Objektinitialisierern finden Sie unter Verwenden von Objektinitialisierern (Formatvorlagenregel IDE0017). Das folgende Beispiel zeigt, wie Sie einen Objektinitialisierer mit einem benannten Typ Catverwenden und wie der parameterlose Konstruktor aufgerufen wird. Beachten Sie die Verwendung von automatisch implementierten Eigenschaften in der Klasse Cat. Weitere Informationen finden Sie unter Automatisch implementierte Eigenschaften.

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

Mit der Objektinitialisierungssyntax können Sie eine Instanz erstellen und das neu erstellte Objekt mit seinen zugewiesenen Eigenschaften der Variablen in der Zuordnung zuweisen.

Beginnend mit geschachtelten Objekteigenschaften können Sie die Objektinitialisierungssyntax ohne das new Schlüsselwort verwenden. Mit dieser Syntax Property = { ... } können Sie Mitglieder vorhandener geschachtelter Objekte initialisieren, was bei schreibgeschützten Eigenschaften nützlich ist. Weitere Informationen finden Sie unter Object Initializers mit klassentypischen Eigenschaften.

Objektinitialisierer können nicht nur Felder und Eigenschaften zuweisen, sondern auch Indexer festlegen. Betrachten Sie diese grundlegende Matrix-Klasse:

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

Sie können die Identitätsmatrix mithilfe des folgenden Codes initialisieren:

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

Alle zugänglichen Indexer, die zugängliche Setter enthalten, können unabhängig von der Anzahl und Typen der Argumente als einer der Ausdrücke in einem Objektinitialisierer verwendet werden. Die Indexargumente bilden die linke Seite der Zuweisung, und der Wert befindet sich auf der rechten Seite des Ausdrucks. Die folgenden Initialisierer sind beispielsweise alle gültig, wenn IndexersExample über die entsprechenden Indexer verfügt:

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

Der IndexersExample-Typ muss für das Erstellen des obigen Codes die folgenden Member vorweisen:

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

Objektinitialisierer mit anonymen Typen

Obwohl Sie Objektinitialisierer in jedem Kontext verwenden können, sind sie besonders nützlich in Language-Integrated Query-Ausdrücken (LINQ). Abfrageausdrücke verwenden häufig anonyme Typen, die Sie nur mithilfe eines Objektinitialisierungsprogramms initialisieren können, wie in der folgenden Deklaration gezeigt.

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

Mithilfe anonymer Typen kann die select Klausel in einem LINQ-Abfrageausdruck Objekte der ursprünglichen Sequenz in Objekte transformieren, deren Wert und Form vom Ursprünglichen abweichen. Möglicherweise möchten Sie nur einen Teil der Informationen aus jedem Objekt in einer Sequenz speichern. Im folgenden Beispiel wird angenommen, dass ein Produktobjekt (p) viele Felder und Methoden enthält und dass Sie nur eine Sequenz von Objekten erstellen möchten, die den Produktnamen und den Einzelpreis enthalten.

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

Wenn Sie diese Abfrage ausführen, enthält die productInfos Variable eine Abfolge von Objekten, auf die Sie in einer foreach Anweisung zugreifen können, wie in diesem Beispiel gezeigt:

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

Jedes Objekt im neuen anonymen Typ weist zwei öffentliche Eigenschaften auf, die die gleichen Namen wie die Eigenschaften oder Felder im ursprünglichen Objekt erhalten. Sie können ein Feld auch umbenennen, wenn Sie einen anonymen Typ erstellen. Im folgenden Beispiel wird das UnitPrice Feld in Price umbenannt.

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

Objektinitialisierungen mit dem required Modifizierer

Verwenden Sie das required Schlüsselwort, um aufrufer zu erzwingen, den Wert einer Eigenschaft oder eines Felds mithilfe eines Objektinitialisierers festzulegen. Sie müssen die erforderlichen Eigenschaften nicht als Konstruktorparameter festlegen. Der Compiler stellt sicher, dass alle Aufrufer diese Werte initialisieren.

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

Es ist eine übliche Vorgehensweise, um sicherzustellen, dass Ihr Objekt ordnungsgemäß initialisiert wird, insbesondere wenn Sie mehrere Felder oder Eigenschaften verwalten müssen und diese nicht alle in den Konstruktor aufnehmen möchten.

Objektinitialisierer mit dem init Accessor

Mithilfe eines init Accessors können Sie sicherstellen, dass sich das Objekt nach der Initialisierung nicht ändert. Damit können Sie die Einstellung des Eigenschaftswertes einschränken.

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

Erforderliche Initialisierungseigenschaften ermöglichen unveränderliche Strukturen und gleichzeitig eine natürliche Syntax für Benutzer des Typs.

Objektinitialisierer mit Klassentypeigenschaften

Wenn Sie Objekte mit Klassentypeigenschaften initialisieren, können Sie zwei verschiedene Syntaxen verwenden:

  1. Objektinitialisierer ohne new Schlüsselwort: Property = { ... }
  2. Objektinitialisierer mit new Schlüsselwort: Property = new() { ... }

Diese Syntaxen verhalten sich anders. Im folgenden Beispiel werden beide Ansätze veranschaulicht:

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
}

Wichtige Unterschiede

  • Ohne new Schlüsselwort (ClassB = { BI = 100003 }): Diese Syntax ändert die vorhandene Instanz der Eigenschaft, die der Objektkonstruktor erstellt hat. Es ruft Mitgliedsinitialisierer für das vorhandene Objekt auf.

  • Mit dem Schlüsselwort new: Mit dieser Syntax wird eine neue Instanz erstellt und der Eigenschaft zugewiesen, wobei jede vorhandene Instanz ersetzt wird.ClassB = new() { BI = 100003 }

Der Initialisierer ohne new verwendet die aktuelle Instanz erneut. Im vorherigen Beispiel sind die Werte von ClassB: 100003 (neuer Wert zugewiesen), true (beibehalten von der Initialisierung von EmbeddedClassTypeA), BBBabc (unveränderter Standardwert von EmbeddedClassTypeB).

Objekt-Initialisierer ohne new für schreibgeschützte Eigenschaften

Die Syntax ohne new ist bei schreibgeschützten Eigenschaften nützlich. Sie können keine neue Instanz zuweisen, aber Sie können die Member der vorhandenen Instanz trotzdem initialisieren:

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

Mit diesem Ansatz können Sie geschachtelte Objekte auch dann initialisieren, wenn die enthaltende Eigenschaft keinen Setter aufweist.

Auflistungsinitialisierer

Mit Sammlungsinitialisierern können Sie einen oder mehrere Elementinitialisierer angeben, wenn Sie einen Sammlungstyp IEnumerable initialisieren, der eine Add Methode mit der entsprechenden Signatur als Instanzmethode oder Erweiterungsmethode implementiert und besitzt. Die Elementinitialisierer können ein Wert, ein Ausdruck oder ein Objektinitialisierer sein. Wenn Sie einen Sammlungsinitialisierer verwenden, ist es nicht nötig, selbst Aufrufe anzugeben, da der Compiler diese automatisch hinzufügt. Anleitungen zur konsistenten Verwendung von Sammlungsinitialisierern finden Sie unter Verwenden von Sammlungsinitialisierern (Formatvorlagenregel IDE0028). Sammlungsinitialisierer sind auch in LINQ-Abfragen nützlich.

Im folgenden Beispiel werden zwei einfache Sammlungsinitialisierer dargestellt:

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

Der folgende Auflistungsinitialisierer verwendet Objektinitialisierer, um Objekte der Cat-Klasse, die in einem vorherigen Beispiel definiert wurden, zu initialisieren. Die einzelnen Objektinitialisierer werden in geschweifte Klammern eingeschlossen und durch Kommas voneinander getrennt.

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

Sie können NULL als ein Element in einem Auflistungsinitialisierer festlegen, wenn die Add-Methode der Auflistung dies zulässt.

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

Sie können ein Spread-Element verwenden, um eine Liste zu erstellen, die eine andere Liste oder mehrere Listen kopiert.

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

Und fügen Sie zusätzliche Elemente zusammen mit einem Spread-Element hinzu.

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

Sie können indizierte Elemente angeben, wenn die Sammlung das Lesen oder Schreiben von Indizierungen unterstützt.

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

Im vorherigen Beispiel wurde Code generiert, der die Item[TKey]-Eigenschaft zum Festlegen der Werte aufruft. Sie können wörterbücher und andere assoziative Container auch mithilfe der folgenden Syntax initialisieren. Anstelle der Indexersyntax mit Klammern und einer Zuweisung wird ein Objekt mit mehreren Werten verwendet:

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

Bei diesem Beispiel eines Initialisierers wird Add(TKey, TValue) aufgerufen, um die drei Elemente dem Wörterbuch hinzuzufügen. Diese zwei verschiedenen Arten des Initialisierens assoziativer Sammlungen verhalten sich aufgrund der vom Compiler generierten Methodenaufrufe etwas unterschiedlich. Beide Varianten unterstützen aber die Dictionary-Klasse. Bei anderen Typen wird abhängig von deren öffentlicher API möglicherweise nur eine der beiden unterstützt.

Auflistungsausdrucksargumente

Verwenden Sie ab C# 15 das with(...) Element als erstes Element in einem Auflistungsausdruck , um Argumente an den Konstruktor der Auflistung zu übergeben. Mithilfe dieses Features können Sie Kapazitäts-, Vergleichs- oder andere Konstruktorparameter direkt in der Sammlungsausdruckssyntax angeben:

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

Weitere Informationen zu Auflistungsausdrucksargumenten, einschließlich unterstützter Zieltypen und Einschränkungen, finden Sie unter Sammlungsausdrucksargumente.

Objektinitialisierer mit Initialisierung schreibgeschützter Eigenschaften einer Sammlung

Einige Klassen haben Sammlungseigenschaften, bei denen die Eigenschaft nur lesbar ist, wie die Cats Eigenschaft in dem CatOwner Fall.

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

Sie können die zuvor erläuterte Sammlungsinitialisierungssyntax nicht verwenden, da der Eigenschaft keine neue Liste zugewiesen werden kann:

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

Sie können jedoch neue Einträge Cats hinzufügen, indem Sie die Initialisierungssyntax verwenden und die Listenerstellung auslassen (new List<Cat>), wie im folgenden Beispiel gezeigt:

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

Das Set der hinzuzufügenden Einträge erscheint in geschweiften Klammern. Der vorangehende Code ist identisch mit:

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

Beispiele

Im folgenden Beispiel werden die Konzepte des Objekt- und Auflistungsinitialisierers verbunden.

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

Das folgende Beispiel zeigt ein Objekt, das IEnumerable implementiert und eine Add-Methode mit mehreren Parametern enthält. Es verwendet einen Sammlungsinitialisierer mit mehreren Elementen pro Element in der Liste, die der Signatur der Add-Methode entsprechen.

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 können durch das Schlüsselwort params eine variable Anzahl von Argumenten akzeptieren, wie im folgenden Beispiel gezeigt wird. In diesem Beispiel wird die benutzerdefinierte Implementierung eines Indexers sowie die Initialisierung einer Sammlung mithilfe von Indizes veranschaulicht. Ab C# 13 ist der params Parameter nicht auf ein Array beschränkt. Dabei kann es sich um einen Sammlungstyp oder eine Schnittstelle handeln.

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
        */
}