Udostępnij przez


Inicjatory obiektów i kolekcji (przewodnik programowania w języku C#)

Język C# umożliwia utworzenie wystąpienia obiektu lub kolekcji i wykonanie przypisań składowych w jednej instrukcji.

Inicjatory obiektów

Inicjatory obiektów umożliwiają przypisywanie wartości do dowolnych dostępnych pól lub właściwości obiektu podczas jego tworzenia. Nie musisz wywoływać konstruktora, a następnie używać instrukcji przypisania. Składnia inicjatora obiektów umożliwia określenie argumentów konstruktora lub pominięcie argumentów i nawiasów. Aby uzyskać wskazówki dotyczące spójnego używania inicjatorów obiektów, zobacz Używanie inicjatorów obiektów (reguła stylu IDE0017). W poniższym przykładzie pokazano, jak używać inicjatora obiektów z nazwanym typem Cati jak wywołać konstruktor bez parametrów. Zwróć uwagę na użycie automatycznie zaimplementowanych właściwości w Cat klasie . Aby uzyskać więcej informacji, zobacz Automatycznie zaimplementowane właściwości.

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

Składnia inicjatora obiektów pozwala utworzyć instancję i przypisać nowo utworzony obiekt wraz z przypisanymi właściwościami do zmiennej podczas przypisywania.

Począwszy od właściwości obiektu zagnieżdżonego, można użyć składni inicjatora obiektów bez słowa kluczowego new . Ta składnia Property = { ... } umożliwia inicjalizację składowych istniejących zagnieżdżonych obiektów, co jest przydatne w przypadku właściwości tylko do odczytu. Aby uzyskać więcej informacji, zobacz Inicjatory obiektu z właściwościami typu klasy.

Inicjatory obiektów mogą ustawiać indeksatory oprócz przypisywania pól i właściwości. Rozważmy tę podstawową Matrix klasę:

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

Macierz tożsamości można zainicjować przy użyciu następującego kodu:

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

Każdy dostępny indeksator, który zawiera dostępny setter, może być użyty jako jedno z wyrażeń w inicjalizatorze obiektu, niezależnie od liczby lub typów argumentów. Argumenty indeksu tworzą lewą stronę przypisania, a wartość jest prawą stroną wyrażenia. Na przykład następujące inicjatory są prawidłowe, jeśli IndexersExample mają odpowiednie indeksatory:

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

Aby poprzedni kod został skompilowany, IndexersExample typ musi mieć następujących członków:

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

Inicjatory obiektów z typami anonimowymi

Chociaż można używać inicjatorów obiektów w dowolnym kontekście, są one szczególnie przydatne w wyrażeniach Language-Integrated Query (LINQ). Wyrażenia zapytań często używają typów anonimowych, które można zainicjować tylko za pomocą inicjatora obiektów, jak pokazano w poniższej deklaracji.

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

Korzystając z typów anonimowych, klauzula select w wyrażeniu zapytania LINQ może przekształcać obiekty oryginalnej sekwencji w obiekty, których wartość i kształt różnią się od oryginalnego. Możesz przechowywać tylko część informacji z każdego obiektu w sekwencji. W poniższym przykładzie przyjęto założenie, że obiekt produktu (p) zawiera wiele pól i metod oraz że interesuje Cię tylko utworzenie sekwencji obiektów zawierających nazwę produktu i cenę jednostkową.

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

Podczas wykonywania tego zapytania zmienna productInfos zawiera sekwencję obiektów, do których można uzyskać dostęp w foreach instrukcji, jak pokazano w tym przykładzie:

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

Każdy obiekt w nowym typie anonimowym ma dwie właściwości publiczne, które otrzymują takie same nazwy jak właściwości lub pola w oryginalnym obiekcie. Możesz również zmienić nazwę pola podczas tworzenia typu anonimowego. Poniższy przykład zmienia nazwę UnitPrice pola na Price.

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

Inicjatory obiektów z modyfikatorem required

Użyj słowa kluczowego required , aby wymusić, aby wywołania ustawiały wartość właściwości lub pola przy użyciu inicjatora obiektu. Nie trzeba ustawiać wymaganych właściwości jako parametrów konstruktora. Kompilator gwarantuje, że wszystkie wywołujące zainicjują te wartości.

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

Typowym rozwiązaniem jest zagwarantowanie, że obiekt jest poprawnie zainicjowany, zwłaszcza jeśli masz wiele pól lub właściwości do zarządzania i nie chcesz dołączać ich wszystkich do konstruktora.

Inicjatory obiektów z akcesorem init

Korzystając z init akcesora, możesz upewnić się, że obiekt nie zmienia się po inicjalizacji. Pomaga ograniczyć możliwość ustawienia wartości właściwości.

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

Wymagane właściwości tylko do inicjowania obsługują niezmienne struktury, zezwalając na składnię naturalną dla użytkowników typu.

Inicjalizatory obiektów z właściwościami typów klas

Podczas inicjowania obiektów z właściwościami wpisanymi przez klasę można użyć dwóch różnych składni:

  1. Inicjator obiektu bez new słowa kluczowego: Property = { ... }
  2. Inicjator obiektu ze new słowem kluczowym: Property = new() { ... }

Te składnie zachowują się inaczej. W poniższym przykładzie przedstawiono obie metody:

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
}

Kluczowe różnice

  • Bez new słowa kluczowego (ClassB = { BI = 100003 }): ta składnia modyfikuje istniejące wystąpienie właściwości utworzonej przez konstruktora obiektu. Wywołuje inicjatory składowych w istniejącym obiekcie.

  • Za new pomocą słowa kluczowego (ClassB = new() { BI = 100003 }): Ta składnia tworzy nowe wystąpienie i przypisuje je do właściwości, zastępując dowolne istniejące wystąpienie.

Inicjator bez specyfikacji new ponownie używa bieżącego wystąpienia. W poprzednim przykładzie wartości klasy to: 100003 (nowa przypisana wartość), true (zachowana z inicjowania EmbeddedClassTypeA) BBBabc (niezmieniona wartość domyślna z EmbeddedClassTypeB).

Inicjalizatory obiektów bez new właściwości tylko do odczytu

Składnia bez new jest przydatna we właściwościach tylko do odczytu. Nie można przypisać nowego wystąpienia, ale nadal można zainicjować elementy istniejącego wystąpienia:

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

Takie podejście umożliwia inicjowanie zagnieżdżonych obiektów nawet wtedy, gdy właściwość zawierająca nie ma elementu ustawiającego.

Inicjalizatory kolekcji

Inicjatory kolekcji pozwalają na określenie jednego lub więcej inicjatorów elementów podczas inicjalizacji typu kolekcji, który implementuje metodę IEnumerable i ma metodę Add z odpowiednim podpisem jako metodę instancji lub metodę rozszerzeń. Inicjatory elementów mogą być wartością, wyrażeniem lub inicjatorem obiektu. Za pomocą inicjatora kolekcji nie trzeba określać wielu wywołań; kompilator dodaje wywołania automatycznie. Aby uzyskać wskazówki dotyczące spójnego używania inicjatorów kolekcji, zobacz Używanie inicjatorów kolekcji (reguła stylu IDE0028). Inicjatory kolekcji są również przydatne w zapytaniach LINQ.

W poniższym przykładzie przedstawiono dwa proste inicjatory kolekcji:

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

Poniższy inicjator kolekcji używa inicjatorów obiektów do inicjowania obiektów Cat klasy zdefiniowanej w poprzednim przykładzie. Inicjalizatory poszczególnych obiektów są ujęte w nawiasy klamrowe i oddzielone przecinkami.

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

Można użyć null jako elementu w inicjatorze kolekcji, jeśli metoda kolekcji Add na to zezwala.

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

Można użyć elementu spread, aby utworzyć jedną listę, która kopiuje inne listy.

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

Dołącz dodatkowe elementy wraz z użyciem elementu spread.

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

Można określić indeksowane elementy, jeśli kolekcja obsługuje indeksowanie odczytu/zapisu.

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

Powyższy przykład generuje kod, który wywołuje metodę Item[TKey], aby ustawić wartości. Słowniki i inne kontenery asocjacyjne można również zainicjować przy użyciu następującej składni. Zamiast składni indeksatora z nawiasami i przypisaniem używa obiektu z wieloma wartościami:

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

Ten przykład inicjatora wywołuje Add(TKey, TValue), aby dodać trzy elementy do słownika. Te dwa różne sposoby inicjalizacji kolekcji asocjacyjnych mają nieco odmienny charakter, ponieważ kompilator generuje różne wywołania metody. Oba warianty współpracują z klasą Dictionary . Inne typy mogą obsługiwać tylko jedno lub drugie na podstawie ich publicznego interfejsu API.

Argumenty wyrażeń kolekcji

Od C# 15, użyj elementu with(...) jako pierwszego elementu w wyrażeniu kolekcji aby przekazać argumenty do konstruktora kolekcji. Korzystając z tej funkcji, można określić pojemność, porównania lub inne parametry konstruktora bezpośrednio w składni wyrażeń kolekcji:

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

Aby uzyskać więcej informacji na temat argumentów wyrażeń kolekcji, w tym obsługiwanych typów docelowych i ograniczeń, zobacz Argumenty wyrażeń kolekcji.

Inicjalizatory obiektów z inicjalizacją właściwości kolekcji tylko do odczytu

Niektóre klasy mają właściwości kolekcji, w których właściwość jest tylko do odczytu, podobnie jak właściwość Cats w CatOwner w następującym przypadku:

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

Nie można użyć omówionej wcześniej składni inicjatora kolekcji, ponieważ nie można przypisać nowej listy właściwości:

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

Możesz jednak dodawać nowe wpisy do Cats, korzystając ze składni inicjalizacji i pomijając tworzenie listy (new List<Cat>), jak pokazano w poniższym przykładzie:

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

Zestaw wpisów do dodania pojawi się otoczony nawiasami klamrowymi. Poprzedni kod jest identyczny jak zapisanie:

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

Przykłady

Poniższy przykład łączy pojęcia inicjalizatorów obiektów i kolekcji.

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

Poniższy przykład przedstawia obiekt, który implementuje IEnumerable i zawiera metodę Add z wieloma parametrami. Używa inicjatora kolekcji, w którym każdy element listy odpowiada specyfikacji metody Add.

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 metody mogą użyć params słowa kluczowego , aby użyć zmiennej liczby argumentów, jak pokazano w poniższym przykładzie. W tym przykładzie pokazano również niestandardową implementację indeksatora w celu zainicjowania kolekcji przy użyciu indeksów. Począwszy od języka C# 13, params parametr nie jest ograniczony do tablicy. Może to być typ kolekcji lub interfejs.

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