Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 17. Введение в обобщения
Дальше: 19. Перечисляемые коллекции

18. Использование коллекций

Прочитав эту главу, вы научитесь:

объяснять, какие функциональные возможности предоставляются в различных классах коллекций, доступных в среде .NET Framework;

создавать безопасные по отношению к типам коллекции;

заполнять коллекции набором данных;

получать доступ к элементам данных, хранящимся в коллекциях, и оперировать ими;

искать в списочной коллекции соответствующие элементы с помощью предиката.

В главе 10 «Использование массивов» были представлены массивы, предназначенные для хранения данных. В этом качестве массивы, безусловно, очень полезны, но у них имеются ограничения. Массивы предоставляют весьма скромные функциональные возможности, например, нелегко дается увеличение или уменьшение размера массива, да и сортировать хранящиеся в массиве данные не так-то просто. Кроме того, массивы фактически предоставляют только одно средство доступа к данным — целочисленный индекс. Если ваше приложение нуждается в сохранении данных с использованием какого-либо другого механизма, например, основанного на применении очереди, работающей по принципу «первым пришел — первым ушел», рассмотренного в главе 17 «Введение в обобщения», массивы могут оказаться не самой удобной структурой данных. И здесь свою пользу могут доказать коллекции.

Что такое классы коллекций?

Среда Microsoft .NET предоставляет несколько классов, которые собирают элементы вместе таким образом, чтобы приложение могло получать доступ к элементам конкретно указанными способами. Это уже упоминавшиеся в главе 17 классы коллекций, которые находятся в пространстве имен System.Collections.Generic.

Из названия этого пространства имен видно, что коллекции относятся к обобщенным типам: все они ожидают от вас предоставления параметра типа, показывающего разновидность тех данных, которые будет в них хранить ваше приложение. Каждый класс коллекции оптимизирован под конкретную форму хранения данных и доступа к ним, и каждый из них предоставляет специализированные методы, поддерживающие эти функциональные возможности. Например, класс Stack<T> реализует модель «последним пришел — первым ушел», где элемент добавляется к вершине стека путем использования метода Push и забирается с вершины стека путем использования метода Pop. Метод Pop всегда извлекает самый последний помещенный в стек элемент и удаляет его из стека. В отличие от этого тип Queue<T> предоставляет методы Enqueue и Dequeue, рассмотренные в главе 17. Метод Enqueue добавляет элемент к очереди, а метод Dequeue извлекает элементы из очереди в порядке, реализующем модель «первым пришел — первым ушел». Доступно также множество других классов коллекций, наиболее востребованные из которых сведены в табл. 18.1.

Таблица 18.1

Коллекция

Описание

List<T>

Список объектов, доступных по индексу, как в случае с массивом, но с дополнительными методами, позволяющими вести поиск в списке и сортировать его содержимое

Queue<T>

Структура данных, работающая по принципу «первым пришел — первым ушел», с методами для добавления элемента к одному из концов очереди, удаления элемента с другого конца и изучения содержимого элемента без его удаления

Stack<T>

Структура данных, работающая по принципу «первым пришел — последним ушел», с методами для помещения элемента на вершину стека, извлечения элемента с вершины стека и изучения элемента, находящегося на вершине стека, без его удаления

LinkedList<T>

Двусторонний упорядоченный список, оптимизированный под поддержку вставки и удаления элементов с любого конца. Эта коллекция может работать как очередь или как стек, но, как и список, поддерживает произвольный доступ

HashSet<T>

Неупорядоченный набор значений, оптимизированный под быстрое извлечение данных. Предоставляет ориентированные на наборы методы для определения того, содержат ли элементы поднаборы таких же элементов в другом HashSet<T>-объекте, а также для вычисления пересечений и объединений HashSet<T>-объектов

Dictionary<TKey, TValue>

Коллекция значений, которые можно идентифицировать и извлечь с помощью ключей, а не индексов

SortedList<TKey, TValue>

Отсортированный список пар «ключ–значение». Ключи должны реализовывать IComparable<T>-интерфейс

Краткий обзор этих классов коллекций дается в следующем разделе. Более подробные сведения о каждом классе можно найти в документации библиотеки классов .NET Framework.

174100.png

ПРИМЕЧАНИЕ В библиотеке классов .NET Framework в пространстве имен System.Collections предоставляется еще один набор типов коллекций. Это необобщенные коллекции, разработанные до того, как в C# появилась поддержка типов-обобщений (обобщения были добавлены к версии C#, разработанной для .NET Framework версии 2.0). За единственным исключением, все эти типы хранят ссылки на объекты, и от вас при сохранении или извлечении элементов требуется соответствующее приведение типов. Эти классы включены в библиотеку с целью обратной совместимости с существующими приложениями, и применять их при создании новых решений не рекомендуется. Фактически при создании приложений универсальной платформы Windows (UWP) эти классы недоступны.

Единственным не содержащим ссылок на объект является класс BitArray. Этот класс реализует компактный массив булевых значений за счет использования int-значений, в которых каждый бит означает true (1) или false (0). Конечно же, это напоминает структуру IntBits, которая уже встречалась в главе 16 «Использование индексаторов». Класс BitArray доступен для создания UWP-приложений.

Доступен и другой набор коллекций, чьи классы определены в пространстве имен System.Collections.Concurrent. Это коллекции классов, предназначенные для безопасной работы в многопоточной среде, которыми можно воспользоваться при создании многопоточных приложений. Более подробно эти классы будут рассмотрены в главе 24 «Сокращение времени отклика путем выполнения асинхронных операций».

Класс коллекций List<T>

Класс-обобщение List<T> является самым простым из классов коллекций. Его можно использовать практически так же, как массив, ссылаясь на существующий в коллекции List<T> элемент с использованием обычной для массивов системы записи с квадратными скобками и индексом элемента, хотя для добавления новых элементов эту систему записи использовать нельзя. Но в целом класс List<T> обеспечивает более высокую степень гибкости, чем массивы, и был разработан для преодоления следующих свойственных массивам ограничений.

• При необходимости изменения размера массива приходится создавать новый массив, копировать элементы (отбрасывая при этом некоторые из них, если массив становится меньше), после чего обновлять все ссылки на исходный массив, чтобы теперь они указывали на новый массив.

• При необходимости удаления элемента из массива приходится перемещать все следующие за ним элементы на одну позицию. Но даже это еще не все, поскольку у вас останутся две копии последнего элемента.

• При необходимости вставить элемент в массив приходится перемещать элементы на одну позицию, чтобы появилось свободное место. Но при этом последний элемента массива будет потерян!

Класс коллекций List<T> предоставляет следующие возможности, устраняющие перечисленные ограничения.

• Указывать емкость коллекции List<T> при ее создании не нужно. Коллекция может расширяться и сужаться по мере добавления или удаления элементов. Подобный динамичный характер поведения не обходится без издержек, и при необходимости можно указать начальный размер. Но если он будет превышен, то в силу необходимости коллекция List<T> просто расширится.

• Для удаления из коллекции List<T> указанного элемента можно воспользоваться методом Remove. Элементы коллекции List<T> автоматически перестроятся, закрывая прореху. С помощью метода RemoveAt можно также удалить элемент, указав его позицию в коллекции List<T>.

• Можно добавить элемент к концу коллекции List<T>, воспользовавшись имеющимся в ее классе методом Add, которому предоставляется добавляемый элемент. Размер коллекции изменяется List<T> автоматически.

• Можно вставить элемент в середину коллекции List<T>, воспользовавшись для этого методом Insert. При этом размер коллекции List<T> также изменится автоматически.

• Можно без особого труда отсортировать данные List<T>-объекта, вызвав для этого метод Sort.

174107.png

ПРИМЕЧАНИЕ Как и при работе с массивами, если для последовательного обхода элементов коллекции List<T> используется цикл foreach, то воспользоваться переменной итерации для изменения содержимого коллекции вы не сможете. Кроме того, в цикле foreach, обходящем все элементы коллекции List<T>, нельзя использовать метод Remove, Add или Insert — любая попытка такого рода приведет к выдаче исключения InvalidOperationException.

Рассмотрим пример, показывающий, как можно создавать содержимое коллекции List<int>, работать с этим содержимым и выполнять последовательный обход элементов:

using System;

using System.Collections.Generic;

...

List<int> numbers = new List<int>();

 

// Заполнение List<int> с помощью метода Add

foreach (int number in new int[12]{10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1})

{

    numbers.Add(number);

}

 

// Вставка элемента в предпоследнюю позицию списка и сдвиг последнего элемента

// вверх по списку

// В первом параметре указывается позиция, а во втором — вставляемое значение

numbers.Insert(numbers.Count-1, 99);

 

// Удаление первого элемента, чье значение равно 7 (4-й элемент с индексом 3)

numbers.Remove(3);

// Удаление элемента, который теперь находится на 7-й позиции с индексом 6 (10)

numbers.RemoveAt(6);

 

// Последовательный обход оставшихся 11 элементов с использованием инструкции for

Console.WriteLine("Iterating using a for statement:");

for (int i = 0; i < numbers.Count; i++)

{

    int number = numbers[i]; // Обратите внимание на использование такого же

                             // синтаксиса, как и у массива

    Console.WriteLine(number);

}

 

// Обход тех же 11 элементов с использованием инструкции foreach

Console.WriteLine("\nIterating using a foreach statement:");

foreach (int number in numbers)

{

    Console.WriteLine(number);

}

А вот как выглядит информация, выведенная данным кодом на экран:

Iterating using a for statement:

10

9

8

7

6

5

4

3

2

99

1

Iterating using a foreach statement:

10

9

8

7

6

5

4

3

2

99

1

174113.png

ПРИМЕЧАНИЕ Способ определения количества элементов в коллекции List<T> отличается от запроса количества элементов в массиве. При использовании коллекции List<T> исследуется свойство Count, а при использовании массива — свойство Length.

Класс коллекций LinkedList<T>

Класс коллекций LinkedList<T> реализует двусвязный список. В каждом элементе списка содержится значение элемента со ссылкой на следующий элемент списка (свойство Next) и его предыдущий элемент (свойство Previous). У элемента в начале списка свойство Previous имеет значение null, и такое же значение имеет свойство Next у элемента, расположенного в конце списка.

В отличие от класса List<T>, в классе LinkedList<T> при вставке или исследовании элементов система записи, присущая массивам, не поддерживается. Вместо этого можно воспользоваться методом AddFirst для вставки элемента в начало списка с перемещением предыдущего первого элемента дальше по списку и установки в качестве значения его свойства Previous ссылки на новый элемент. Аналогично этому для вставки элемента в конец списка можно воспользоваться методом AddLast, что повлечет за собой установку в качестве значения свойства Next предыдущего последнего элемента ссылки на новый элемент. Для вставки элемента перед указанным элементом списка или после него (с предварительным извлечением этого элемента) можно также воспользоваться методами AddBefore и AddAfter.

Первый элемент коллекции LinkedList<T> можно найти, запросив значение свойства First, а свойство Last даст ссылку на последний элемент списка. Для последовательного обхода элементов связного списка можно приступить к этой операции с одного конца и пошагово применять ссылки из свойства Next или Previous, пока не будет найден элемент, у которого это свойство имеет значение null. В качестве альтернативного варианта можно воспользоваться инструкцией foreach, которая выполнит последовательный обход элементов вперед по списку LinkedList<T>-объекта, автоматически остановившись в конце.

Удаление элемента из коллекции LinkedList<T> осуществляется с помощью методов Remove, RemoveFirst и RemoveLast.

В следующем примере показана работа с элементами коллекции LinkedList<T>. Обратите внимание на то, что код, реализующий обход элементов списка с помощью инструкции for, пошагово использует ссылки из свойства Next или Previous, останавливаясь только в случае встречи ссылки со значением null, имеющейся в конце списка:

using System;

using System.Collections.Generic;

...

LinkedList<int> numbers = new LinkedList<int>();

 

// Заполнение List<int> с помощью метода AddFirst

foreach (int number in new int[] { 10, 8, 6, 4, 2 })

{

    numbers.AddFirst(number);

}

 

// Итерация с использованием инструкции for

Console.WriteLine("Iterating using a for statement:");

for (LinkedListNode<int> node = numbers.First; node != null; node = node.Next)

{

    int number = node.Value;

    Console.WriteLine(number);

}

 

// Итерация с использованием инструкции foreach

Console.WriteLine("\nIterating using a foreach statement:");

foreach (int number in numbers)

{

    Console.WriteLine(number);

}

 

// Итерация в обратном направлении

Console.WriteLine("\nIterating list in reverse order:");

for (LinkedListNode<int> node = numbers.Last; node != null; node = node.Previous)

{

    int number = node.Value;

    Console.WriteLine(number);

}

А вот как выглядит информация, выведенная данным кодом на экран:

Iterating using a for statement:

2

4

6

8

10

Iterating using a foreach statement:

2

4

6

8

10

Iterating list in reverse order:

10

8

6

4

2

Класс коллекций Queue<T>

В классе Queue<T> реализован механизм «первым пришел — первым ушел». Элемент вставляется в конец очереди (операция Enqueue) и удаляется из начала очереди (операция Dequeue).

В следующем коде показаны пример Queue<int>-коллекции и самые распространенные из проводимых с ней операций:

using System;

using System.Collections.Generic;

...

Queue<int> numbers = new Queue<int>();

 

// Заполнение очереди

Console.WriteLine("Populating the queue:");

foreach (int number in new int[4]{9, 3, 7, 2})

{

    numbers.Enqueue(number);

    Console.WriteLine($"{number} has joined the queue");

}

 

// Последовательный обход элементов очереди

Console.WriteLine("\nThe queue contains the following items:");

foreach (int number in numbers)

{

    Console.WriteLine(number);

}

 

// Опустошение очереди

Console.WriteLine("\nDraining the queue:");

while (numbers.Count > 0)

{

    int number = numbers.Dequeue();

    Console.WriteLine($"{number} has left the queue");

}

А вот как выглядит информация, выведенная данным кодом на экран:

Populating the queue:

9 has joined the queue

3 has joined the queue

7 has joined the queue

2 has joined the queue

The queue contains the following items:

9

3

7

2

Draining the queue:

9 has left the queue

3 has left the queue

7 has left the queue

2 has left the queue

Класс коллекций Stack<T>

В классе Stack<T> реализован механизм «последним пришел — первым ушел». Элемент присоединяется к вершине стека (операция pop). Представьте себе стопку тарелок: новые тарелки кладут на вершину стопки и удаляют их с вершины, при этом последняя помещенная в стек тарелка удаляется первой. (Тарелка в самом низу используется редко, и прежде чем класть в нее еду, ее нужно помыть, поскольку к этому времени она сильно запылится!) Рассмотрим пример, обращая внимание на порядок, в котором инструкция foreach выводит элементы на экран:

using System;

using System.Collections.Generic;

...

Stack<int> numbers = new Stack<int>();

 

// Заполнение стека

Console.WriteLine("Pushing items onto the stack:");

foreach (int number in new int[4]{9, 3, 7, 2})

{

    numbers.Push(number);

    Console.WriteLine($"{number} has been pushed on the stack");

}

 

// Последовательный обход элементов стека

Console.WriteLine("\nThe stack now contains:");

foreach (int number in numbers)

{

    Console.WriteLine(number);

}

 

// Опустошение стека

Console.WriteLine("\nPopping items from the stack:");

while (numbers.Count > 0)

{

    int number = numbers.Pop();

    Console.WriteLine($"{number} has been popped off the stack");

}

А вот как выглядит информация, выведенная программой на экран:

Pushing items onto the stack:

9 has been pushed on the stack

3 has been pushed on the stack

7 has been pushed on the stack

2 has been pushed on the stack

The stack now contains:

2

7

3

9

Popping items from the stack:

2 has been popped off the stack

7 has been popped off the stack

3 has been popped off the stack

9 has been popped off the stack

Класс коллекций Dictionary<TKey, TValue>

Массив и объекты типа List<T> предоставляют способ отображения на элемент целочисленного индекса. Целочисленный индекс указывается с помощью квадратных скобок (например, [4]), и извлекается элемент по индексу 4, будучи фактически пятым. Но иногда может понадобиться реализация отображения, при котором используется другой, не целочисленный тип, например string, double или Time. В других языках программирования такая организация хранения данных часто называется ассоциативным массивом. Эта функциональная возможность реализуется в классе Dictionary<TKey, TValue> путем внутреннего обслуживания двух массивов, один из которых предназначен для ключей, от которых выполняется отображение на одно из отображаемых значений. Когда в коллекцию Dictionary<TKey, TValue> вставляется пара «ключ–значение», класс автоматически отслеживает принадлежность ключа к значению, позволяя быстро и легко извлекать значение, связанное с указанным ключом. В конструкции класса Dictionary<TKey, TValue> имеется ряд важных особенностей.

• В коллекции Dictionary<TKey, TValue> не могут содержаться продублированные ключи. Если для добавления уже имеющегося в массиве ключа вызывается метод Add, выдается исключение. Но для добавления пары «ключ–значение» можно воспользоваться системой записи с использованием квадратных скобок (как показано в следующем примере), не опасаясь при этом выдачи исключения, даже если ключ уже был добавлен: любое значение с таким же самым ключом будет переписано новым значением. Протестировать наличие в коллекции Dictionary<TKey, TValue> конкретного ключа можно с помощью метода ContainsKey.

• По внутреннему устройству коллекция Dictionary<TKey, TValue> является разряженной структурой данных, работающей наиболее эффективно, когда в ее распоряжении имеется довольно большой объем памяти. По мере вставки элементов размер коллекции Dictionary<TKey, TValue> в памяти может очень быстро увеличиваться.

• Когда для последовательного обхода элементов коллекции Dictionary<TKey, TValue> используется инструкция foreach, возвращается элемент KeyValue­Pair<TKey, TValue>. Это структура, содержащая копию элементов ключа и значения, находящихся в коллекции Dictionary<TKey, TValue>, и доступ к каждому элементу можно получить через свойства Key и Value. Эти элементы доступны только для чтения, и их нельзя использовать для изменения данных в коллекции Dictionary<TKey, TValue>.

Далее показан пример, в котором данные о возрасте членов моей семьи связываются с их именами, а затем эта информация выводится на экран:

using System;

using System.Collections.Generic;

...

Dictionary<string, int> ages = new Dictionary<string, int>();

 

// Заполнение словаря Dictionary

ages.Add("John", 51); // Использование метода Add

ages.Add("Diana", 50);

ages["James"] = 23;   // Использование такой же системы записи, что и у массивов

ages["Francesca"] = 21;

 

// Последовательный обход элементов с использованием инструкции foreach,

// при этом итератор создает элемент KeyValuePair

Console.WriteLine("The Dictionary contains:");

foreach (KeyValuePair<string, int> element in ages)

{

    string name = element.Key;

    int age = element.Value;

    Console.WriteLine($"Name: {name}, Age: {age}");

}

А вот что данная программа выведет на экран:

The Dictionary contains:

Name: John, Age: 51

Name: Diana, Age: 50

Name: James, Age: 23

Name: Francesca, Age: 21

174119.png

ПРИМЕЧАНИЕ Пространство имен System.Collections.Generic включает также тип коллекций SortedDictionary<TKey, TValue>. Этот класс содержит коллекцию в порядке, получаемом сортировкой ключей.

Класс коллекций SortedList<TKey, TValue>

Класс SortedList<TKey, TValue> очень похож на класс Dictionary<TKey, TValue> тем, что его можно использовать для связи ключей со значениями. Основное отличие заключается в том, что массив ключей всегда находится в отсортированном состоянии, поэтому класс и носит название SortedList. В большинстве случаев времени на вставку данных в объект типа SortedList<TKey, TValue> тратится больше, чем на вставку в объект SortedDictionary<TKey, TValue>, но данные зачастую извлекаются быстрее (во всяком случае так же быстро), и класс SortedList<TKey, TValue> использует меньше памяти.

Когда в коллекцию SortedList<TKey, TValue> вставляется пара «ключ–значение», ключ вставляется в массив ключей на место с правильным индексом, чтобы массив ключей оставался отсортированным. Затем значение вставляется в массив значений на место с таким же индексом. Класс SortedList<TKey, TValue> автоматически обеспечивает поддержку синхронизированности ключей и значений даже при добавлении и удалении элементов. Это означает, что вставлять пары «ключ–значение» в SortedList<TKey, TValue> можно в любой последовательности — они всегда будут отсортированы на основе значений ключей.

Как и класс Dictionary<TKey, TValue>, коллекция SortedList<TKey, TValue> не может содержать продублированные ключи. Когда для последовательного обхода элементов коллекции SortedList<TKey, TValue> используется инструкция foreach, возвращается элемент KeyValuePair<TKey, TValue>. Но элементы KeyValuePair<TKey, TValue> будут возвращаться отсортированными по значению свойства Key.

Далее показан тот же пример, в котором данные о возрасте членов моей семьи связываются с их именами, после чего информация выводится на экран, но эта версия настроена на использование вместо коллекции Dictionary<TKey, TValue> объекта SortedList<TKey, TValue>:

using System;

using System.Collections.Generic;

...

SortedList<string, int> ages = new SortedList<string, int>();

 

// заполнение SortedList

ages.Add("John", 51); // Использование метода Add

ages.Add("Diana", 50);

ages["James"] = 23; // Использование такой же системы записи, что и у массивов

ages["Francesca"] = 21;

 

// Последовательный обход элементов с использованием инструкции foreach,

// при этом итератор создает элемент KeyValuePair

Console.WriteLine("The SortedList contains: ");

foreach (KeyValuePair<string, int> element in ages)

{

    string name = element.Key;

    int age = element.Value;

    Console.WriteLine($"Name: {name}, Age: {age}");

}

Информация, выведенная этой программой, отсортирована в алфавитном порядке по именам членов моей семьи:

The SortedList contains:

Name: Diana, Age: 50

Name: Francesca, Age: 21

Name: James, Age: 23

Name: John, Age: 51

Класс коллекций HashSet<T>

Класс HashSet<T> оптимизирован для выполнения набора таких операций, как обнаружение присутствия элемента в наборе и создание объединений и пересечений наборов.

Элементы в коллекцию HashSet<T> добавляются с помощью метода Add, а удаляются из нее с помощью метода Remove. Но настоящая эффективность класса HashSet<T> определяется предоставлением методов IntersectWith, UnionWith и ExceptWith. Эти методы изменяют коллекцию HashSet<T>, создавая из нее новый набор, который содержит либо элементы, встречающиеся в обеих указанных коллекциях, либо элементы из обеих коллекций, либо элементы, которых нет в указанной HashSet<T>-коллекции. Эти операции носят деструктивный характер, поскольку в результате их выполнения исходное содержимое HashSet<T>-объекта переписывается новым набором данных. Можно также определить, являются ли данные исходной HashSet<T>-коллекции поднабором данных другой коллекции и наоборот, для чего используются методы IsSubsetOf, IsSupersetOf, IsProperSubsetOf и IsProperSupersetOf. Эти методы возвращают булевы значения и не разрушают данные коллекции.

Внутри HashSet<T>-коллекции хранится хэш-таблица, позволяющая выполнять быстрый поиск элементов. Но для быстрой работы с данными крупной HashSet<T>-коллекции может потребоваться большой объем памяти.

В следующем примере показывается, как заполнить HashSet<T>-коллекцию, и иллюстрируется использование метода IntersectWith для поиска данных, присутствующих в обоих наборах:

using System;

using System.Collections.Generic;

...

HashSet<string> employees = new HashSet<string>(new string[]

                                               {"Fred","Bert","Harry","John"});

HashSet<string> customers = new HashSet<string>(new string[]

                                               {"John","Sid","Harry","Diana"});

 

employees.Add("James");

customers.Add("Francesca");

 

Console.WriteLine("Employees:");

foreach (string name in employees)

{

    Console.WriteLine(name);

}

 

Console.WriteLine("\nCustomers:");

foreach (string name in customers)

{

    Console.WriteLine(name);

}

 

Console.WriteLine("\nCustomers who are also employees:");

customers.IntersectWith(employees);

foreach (string name in customers)

{

    Console.WriteLine(name);

}

Этот код выведет на экран следующую информацию:

Employees:

Fred

Bert

Harry

John

James

Customers:

John

Sid

Harry

Diana

Francesca

Customers who are also employees:

John

Harry

174128.png

ПРИМЕЧАНИЕ В пространстве имен System.Collections.Generic предоставляется также тип коллекций SortedSet<T>, работа которого похожа на работу класса HashSet<T>. Основное отличие, судя по названию, состоит в том, что данные в этой коллекции содержатся в отсортированном порядке. Классы SortedSet<T> и HashSet<T> обладают функциональной совместимостью, позволяющей, к примеру, объединить коллекцию SortedSet<T> с коллекцией HashSet<T>.

Использование инициализаторов коллекций

В примерах из предыдущих разделов было показано, как к коллекции с помощью наиболее походящего для нее метода (Add для коллекции List<T>, Enqueue для коллекции Queue<T>, Push для коллекции Stack<T> и т.д.) добавляются отдельные элементы. Некоторые типы коллекций можно инициализировать при их объявлении, используя для этого синтаксис, похожий на тот, который поддерживается массивами. Например, следующая инструкция создает и инициализирует показанный ранее List<int>-объект numbers, демонстрируя альтернативу многократному вызову метода Add:

List<int> numbers = new List<int>(){10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1};

Внутри среды компилятор C# преобразует эту инициализацию в серию вызовов метода Add. Следовательно, этот синтаксис может использоваться только для коллекций, поддерживающих метод Add. (Классы Stack<T> и Queue<T> его не поддерживают.)

Для более сложных коллекций, принимающих пары «ключ–значение», например коллекций класса Dictionary<TKey, TValue>, можно указывать значения каждого ключа, пользуясь системой записи индексатора:

Dictionary<string, int> ages = new Dictionary<string, int>()

    {

        ["John"] = 51,

        ["Diana"] = 50,

        ["James"] = 23,

        ["Francesca"] = 21

    };

При желании можно также указать в списке инициализации каждую пару «ключ–значение» в виде безымянных типов:

Dictionary<string, int> ages = new Dictionary<string, int>()

    {

        {"John", 51},

        {"Diana", 50},

        {"James", 23},

        {"Francesca", 21}

    };

В данном случае первым элементом каждой пары является ключ, а вторым — значение. Для придания коду максимальной разборчивости при инициализации словарного типа рекомендуется везде, где только можно, использовать систему записи, присущую индексаторам.

Методы Find, предикаты и лямбда-выражения

Используя словарно-ориентированные коллекции Dictionary<TKey, TValue>, SortedDictionary<TKey, TValue> и SortedList<TKey, TValue>, можно быстро найти значение, указав искомый ключ, и, как уже было показано в предыдущих примерах, для доступа к значению можно воспользоваться системой записи, присущей массивам. В других коллекциях, поддерживающих произвольный доступ без применения ключей, например в коллекциях классов List<T> и LinkedList<T>, использование системы записи, присущей массивам, не поддерживается, но вместо этого для обнаружения элемента предоставляется метод Find. В этих классах аргументом для метода Find служит предикат, в котором указывается критерий поиска. Формой предиката является метод, исследующий каждый элемент коллекции и возвращающий булево значение, показывающее, соответствует ли элемент критерию поиска. В случае использования метода Find, как только будет найдено первое же совпадение, возвращается соответствующий элемент. Следует учесть, что классы List<T> и LinkedList<T> поддерживают и другие методы, например метод FindLast, возвращающий последний совпадающий объект, а класс List<T> кроме этого поддерживает метод FindAll, возвращающий коллекцию List<T>, состоящую из всех элементов, соответствующих условиям поиска.

Самым простым методом указания предиката является использование лямбда-выражения. Так называется выражение, возвращающее метод. Это звучит несколько необычно, поскольку большинство встречавшихся до сих пор в C# выражений возвращали значение. Если вам знакомы языки функционального программирования, например Haskell, то вы, вероятно, знакомы и с этим понятием. Если же нет, не стоит переживать: в лямбда-выражениях нет ничего слишком сложного, и после освоения новых синтаксических особенностей вы сможете убедиться в том, что они весьма полезны.

174134.png

ПРИМЕЧАНИЕ Если вас заинтересовало функциональное программирование на языке Haskell, зайдите на веб-сайт, посвященный этому языку, который находится по адресу .

В главе 3 «Создание методов и применение областей видимости» объясняется, что обычный метод состоит из четырех элементов: возвращаемого типа, имени метода, списка параметров и тела метода. Лямбда-выражение содержит два из этих элементов: список параметров и тело метода. В лямбда-выражении не определяется имя метода, а возвращаемый тип (при наличии такового) выводится из контекста, в котором используется лямбда-выражение. В случае использования метода Find предикат по очереди обрабатывает каждый элемент коллекции: тело предиката должно исследовать элемент и в зависимости от его соответствия критерию поиска возвратить true или false. В следующем примере метод Find (выделенный жирным шрифтом) показан в коллекции List<Person>, где Person является структурой. Метод Find возвращает первый элемент списка, у которого для свойства ID установлено значение 3:

struct Person

{

    public int ID { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

}

...

// Создание и заполнение штатного расписания

List<Person> personnel = new List<Person>()

{

    new Person() { ID = 1, Name = "John", Age = 51 },

    new Person() { ID = 2, Name = "Sid", Age = 28 },

    new Person() { ID = 3, Name = "Fred", Age = 34 },

    new Person() { ID = 4, Name = "Paul", Age = 22 },

};

 

// Поиск элемента списка, у которого свойство ID имеет значение 3

Person match = personnel.Find((Person p) => { return p.ID == 3; });

 

Console.WriteLine($"ID: {match.ID}\nName: {match.Name}\nAge: {match.Age}");

А вот что этот код выводит на экран:

ID: 3

Name: Fred

Age: 34

В вызове метода Find аргумент (Person p) => { return p.ID == 3; } является лямбда-выражением, фактически выполняющим всю работу. В нем содержатся следующие элементы синтаксиса:

• список параметров, заключенный в круглые скобки. Как и в обычном методе, если определяемый метод (как в предыдущем примере) не принимает никаких параметров, вы все равно должны поставить круглые скобки. В случае использования метода Find предикату по очереди предоставляется каждый элемент коллекции, и этот элемент передается в качестве параметра лямбда-выражению;

• оператор =>, который показывает компилятору C#, что это лямбда-выражение;

• тело метода. Здесь приведен очень простой пример, содержащий всего одну инструкцию, возвращающую булево значение, показывающее, соответствует ли элемент, указанный в параметре, критерию поиска. Но в лямбда-выражении может содержаться несколько инструкций, которые можно отформатировать для наилучшей читаемости. Нужно лишь не забыть поставить после каждой из них точку с запятой, как это делается в обычном методе.

174909.png

ВНИМАНИЕ В главе 3 также было показано, как оператор => используется для определения методов, тело которых заключено в выражение. По совершенно непонятной причине оператор => имеет такое вот совместное использование. При некоторой схожести системы записи методы, имеющие тело, заключенное в выражение, и лямбда-выражения семантически (и функционально) — совершенно разные вещи, которые не следует путать друг с другом.

Собственно говоря, тело лямбда-выражения может быть телом метода, содержащим несколько инструкций, или одним выражением. Если тело лямбда-выражения содержит только одно выражение, то фигурные скобки и точку с запятой можно не ставить (но для завершения всей инструкции точка с запятой все же понадобится). Кроме того, если выражение получает один параметр, вы можете не ставить вокруг него круглые скобки. И наконец, во многих случаях можно не указывать тип параметров, поскольку компилятор сам извлекает эту информацию из того контекста, в котором вызывается лямбда-выражение. Упрощенная форма ранее показанной инструкции Find имеет следующий вид, в котором намного проще разобраться:

Person match = personnel.Find(p => p.ID == 3);

Формы лямбда-выражений

Лямбда-выражения являются весьма эффективными конструкциями, и чем глубже вы станете вникать в программирование на C#, тем чаще они будут встречаться. Формы самих выражений могут немного отличаться друг от друга. Изначально лямбда-выражения были частью математической формы записи под названием «лямбда-исчисление», которая предназначалась для описания функций. (Функции можно рассматривать как методы, возвращающие значения.) Хотя в имеющейся в C# реализации лямбда-выражений используются расширенный синтаксис и семантика лямбда-исчисления, многие исходные принципы по-прежнему применимы. Далее приведен ряд примеров, показывающих различные формы лямбда-выражений, доступных в C#:

x => x * x     // Простое выражение, возвращающее свой параметр в квадрате.

               // Тип параметра x выводится из контекста.

 

x => { return x * x ; }  // Семантически то же самое выражение, что и предыдущее,

                         // но использующее в качестве тела не отдельное

                         // выражение, а блок инструкций C#.

 

(int x) => x / 2     // Простое выражение, возвращающее значение параметра,

                     // деленное на 2

                     // Тип параметра x указан явным образом.

 

() => folder.StopFolding(0)     // Вызов метода.

                                // Выражение не получает параметров.

                                // Выражение может возвращать, а может и не

                                // возвращать значение.

 

(x, y) => { x++; return x / y; }     // Несколько параметров; тип параметров

                                     // выводится компилятором.

                                     // Параметр x передается значением, поэтому

                                     // эффект операции ++ имеет локальный по

                                     // отношению к выражению характер.

(ref int x, int y) => { x++; return x / y; }     // Несколько параметров

                                                 // с явно указанными типами.

                                                 // Параметр x передан по ссылке,

                                                 // поэтому эффект от операции ++

                                                 // не органичивается выражением.

Чтобы подвести итог, перечислим ряд свойств лямбда-выражений, о которых вам следует знать.

• Если лямбда-выражение получает параметры, их нужно указать в круглых скобках слева от оператора =>. Типы параметров можно не указывать, и тогда компилятор C# выведет их типы из контекста самого лямбда-выражения. Если нужно, чтобы лямбда-выражение могло изменять значения параметров не только локально, параметры можно передавать по ссылке, используя ключевое слово ref, но делать это не рекомендуется.

• Лямбда-выражения могут возвращать значения, но их тип должен совпадать с типом соответствующего делегата.

• Тело лямбда-выражения может быть простым выражением или блоком кода C#, состоящим из нескольких инструкций, вызовов методов, определений переменных и других элементов кода.

• Переменные, определенные в методе лямбда-выражения, по завершении работы метода исчезают из области видимости.

• Лямбда-выражение может получать доступ ко всем переменным за его пределами, находящимися в той области видимости, в которой определено это лямбда-выражение, и вносить в них изменения. Но пользоваться этим свойством нужно весьма осмотрительно!

лямбда-выражения и безымянные методы

Лямбда-выражения были добавлены к языку C# в версии 3.0. А в C# версии 2.0 были введены безымянные методы, способные выполнять сходные задачи, но не обладающие такой же гибкостью. Безымянные методы были введены главным образом для того, чтобы вы могли определять делегаты без необходимости со­здания именованных методов, просто указывая вместо имени метода определение его тела:

this.stopMachinery += delegate { folder.StopFolding(0); };

Безымянный метод также можно передать вместо делегата в качестве параметра:

control.Add(delegate { folder.StopFolding(0); } );

Обратите внимание на то, что при вставке безымянного метода перед ним нужно ставить ключевое слово delegate. Кроме того, все необходимые параметры указываются в круглых скобках, которые, как показано в следующем примере, следуют за ключевым словом delegate:

control.Add(delegate(int param1, string param2))

    { /* код, использующий param1 и param2 */ ... });

Лямбда-выражения позволяют применить более лаконичный и естественный синтаксис, чем безымянные методы, и они охватывают более совершенные аспекты C#, в чем вы сможете убедиться в следующих главах книги. Стало быть, в своем коде вам стоит отдавать предпочтение не безымянным методам, а лямбда-выражениям.

Сравнение массивов и коллекций

Далее перечислены важные отличия массивов от коллекций.

• Экземпляр массива имеет фиксированный размер и не может увеличиваться или уменьшаться. Коллекция может динамически изменяться в размере по мере надобности.

• У массива может быть более одной размерности. Коллекция носит линейный характер. Но элементы коллекции могут сами быть коллекциями, поэтому многомерный массив можно имитировать в виде коллекции коллекций.

• Элементы в массиве сохраняются и извлекаются с помощью индекса. Этот принцип поддерживается лишь некоторыми коллекциями. Например, для сохранения элемента в коллекции List<T> используется метод Add или Insert, а для извлечения элемента — метод Find.

• Многие классы коллекций для создания и заполнения массива, содержащего элементы коллекции, предоставляют метод ToArray. Элементы копируются в массив без удаления их из коллекции. Кроме того, эти коллекции предоставляют конструкторы, способные заполнять коллекцию непосредственно из массива.

Применение классов коллекций к игральным картам

В следующем упражнении карточная игра, разработанная в главе 10, будет переделана под использование не массивов, а коллекций.

Использование коллекций для реализации карточной игры

Откройте в среде Microsoft Visual Studio 2015 проект Cards, который находится в папке \Microsoft Press\VCSBS\Chapter 18\Cards вашей папки документов.

Этот проект содержит измененную версию проекта из главы 10, занимавшегося раздачей карт с помощью массивов. Класс PlayingCard изменен таким образом, чтобы демонстрировать достоинство и масть карты в качестве свойств, предназначенных только для чтения.

Выведите в окно редактора файл Pack.cs. Добавьте к началу файла следующую директиву using:

using System.Collections.Generic;

Внесите в класс Pack изменения, показанные жирным шрифтом, заменив определение двумерного массива cardPack на объект Dictionary<Suit, List<PlayingCard>>:

class Pack

{

    ...

    private Dictionary<Suit, List<PlayingCard>> cardPack;

    ...

}

В исходном приложении двумерный массив использовался для представления карточной колоды. В этом коде массив заменен словарем (Dictionary), где ключ указывает на масть, а значение является перечнем карт в этой масти.

Найдите конструктор Pack. Внесите в первую инструкцию конструктора изменения, выделенные жирным шрифтом, позволяющие создавать в ней экземпляр переменной cardPack в виде новой Dictionary-коллекции, а не массива:

public Pack()

{

    this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);

    ...

}

Несмотря на то что Dictionary-коллекция будет изменять свой размер автоматически по мере добавления элементов, если вероятность изменения размера коллекции мала, то при создании ее экземпляра можно указать начальный размер. Это упростит выделение памяти (но при этом, если размер окажется недостаточным, коллекция по-прежнему сможет его увеличить). В данном случае Dictionary-коллекция будет содержать коллекцию из четырех списков (по одному для каждой масти), следовательно, будет выделено пространство для четырех элементов (NumSuits является константой со значением 4).

Объявите во внешнем цикле for объект коллекции List<PlayingCard> по имени cardsInSuit, достаточно большой, чтобы содержать количество карт, которое имеется в каждой масти, используя для этого константу CardsPerSuit. Соответствующий код выделен жирным шрифтом:

public Pack()

{

    this.cardPack = new Dictionary<Suit, List<PlayingCard>>(NumSuits);

    for (Suit = Suit.Clubs; suit <= Suit.Spades; suit++)

    {

        List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

        for (Value value = Value.Two; value <= Value.Ace; value++)

        {

            ...

        }

    }

}

Внесите изменения, показанные жирным шрифтом, во внутренний цикл for, добавив к этой коллекции не массив, а новые объекты PlayingCard:

for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)

{

    List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

    for (Value value = Value.Two; value <= Value.Ace; value++)

    {

        cardsInSuit.Add(new PlayingCard(suit, value));

    }

}

Добавьте к Dictionary-коллекции cardPack после внутреннего цикла for объект типа List, указав в качестве ключа к этому элементу переменную suit:

for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)

{

    List<PlayingCard> cardsInSuit = new List<PlayingCard>(CardsPerSuit);

    for (Value value = Value.Two; value <= Value.Ace; value++)

    {

        cardsInSuit.Add(new PlayingCard(suit, value));

    }

    this.cardPack.Add(suit, cardsInSuit);

}

Найдите метод DealCardFromPack. Этот метод выбирает произвольную карту из колоды, удаляет ее и возвращает вызывавшему метод коду. Логика выбора карты не требует никаких изменений, но инструкция в конце метода, извлекающая карту из массива, должна быть изменена для использования вместо массива Dictionary-коллекции. Кроме того, нужно изменить код, удаляющий карту (которая уже была сдана) из массива: эту карту нужно найти в списке, а затем удалить оттуда. Для поиска карты воспользуйтесь методом Find и укажите предикат, с помощью которого можно будет отыскать карту с совпадающим значением. Параметром предиката должен быть объект типа PlayingCard (в списке содержатся элементы типа PlayingCard).

Как показано жирным шрифтом в следующем примере, измененные инструкции находятся после закрывающей фигурной скобки второго цикла while:

public PlayingCard DealCardFromPack()

{

    Suit suit = (Suit)randomCardSelector.Next(NumSuits);

    while (this.IsSuitEmpty(suit))

    {

        suit = (Suit)randomCardSelector.Next(NumSuits);

    }

 

    Value value = (Value)randomCardSelector.Next(CardsPerSuit);

    while (this.IsCardAlreadyDealt(suit, value))

    {

        value = (Value)randomCardSelector.Next(CardsPerSuit);

    }

 

    List<PlayingCard> cardsInSuit = this.cardPack[suit];

    PlayingCard card = cardsInSuit.Find(c => c.CardValue == value);

    cardsInSuit.Remove(card);

    return card;

}

Найдите метод IsCardAlreadyDealt. Он определяет, была ли карта уже сдана, проверяя для этого, установлено ли для соответствующего элемента в массиве значение null. Вам нужно изменить этот метод, чтобы он определял, имеется ли в списке соответствующей масти в Dictionary-коллекции cardPack карта указанного достоинства.

Чтобы определить, есть ли элемент в List<T>-коллекции, воспользуйтесь методом Exists. Этот метод похож на метод Find тем, что он получает в качестве аргумента предикат. Этому предикату поочередно передается каждый элемент коллекции, и он должен возвратить true, если элемент соответствует указанному критерию, и false, если не соответствует. В данном случае коллекция типа List<T> содержит PlayingCard-объекты, и критерий для предиката метода Exists должен быть выбран таким, чтобы возвращалось значение true, если ему передан элемент PlayingCard с мастью и достоинством, которые соответствуют параметрам, переданным методу IsCardAlreadyDealt. Внесите в метод изменения, показанные в следующем примере жирным шрифтом:

private bool IsCardAlreadyDealt(Suit suit, Value value)

{

    List<PlayingCard> cardsInSuit = this.cardPack[suit];

    return (!cardsInSuit.Exists(c => c.CardSuit == suit && c.CardValue == value));

}

Выведите в окно редактора файл Hand.cs. Добавьте к списку в самом начале файла следующую директиву using:

using System.Collections.Generic;

В данный момент класс Hand для хранения розданных игральных карт использует массив по имени cards. Измените, как показано далее жирным шрифтом, определение переменной cards, чтобы она стала List<PlayingCard>-коллекцией:

class Hand

{

    public const int HandSize = 13;

    private List<PlayingCard> cards = new List<PlayingCard>(HandSize);

    ...

}

Найдите метод AddCardToHand. Сейчас он занимается проверкой окончания раздачи карт: если карты еще не розданы, добавляет карту, предоставляемую в качестве параметра, в массив cards по индексу, указанному переменной playingCardCount. Измените этот метод, чтобы в нем вместо этого использовался метод Add из List<PlayingCard>-коллекции.

В результате изменения исчезнет также надобность в явном отслеживании количества карт, содержащихся в коллекции, поскольку вместо этого вы можете воспользоваться присущим коллекции карт свойством Count. Следовательно, вам нужно удалить из класса переменную playingCardCount и изменить инструкцию if, проверяющую заполнение раздачи, воспользовавшись ссылкой на свойство Count, принадлежащее коллекции карт.

Метод должен приобрести следующий окончательный вид, где все изменения выделены жирным шрифтом:

public void AddCardToHand(PlayingCard cardDealt)

{

    if (this.cards.Count >= HandSize)

    {

        throw new ArgumentException("Too many cards");

    }

    this.cards.Add(cardDealt);

}

Щелкните в меню Отладка на пункте Начать отладку, чтобы инициировать сборку и запуск приложения.

После появления формы Card Game щелкните на кнопке Deal.

174142.png

ПРИМЕЧАНИЕ Кнопка Deal расположена на панели команд. Для ее обнаружения может понадобиться раскрыть эту панель.

Убедитесь в том, что карты розданы на руки и эта раздача выглядит точно так же, как и прежде. Еще раз щелкните на кнопке Deal, чтобы создать еще один произвольный набор розданных на руки карт.

Приложение в процессе работы показано на рис. 18.1.

18_01.tif 

Рис. 18.1

Вернитесь в среду Visual Studio 2015 и остановите отладку.

Выводы

В этой главе вы научились использовать некоторые самые распространенные классы коллекций, применяемые для хранения данных и обеспечения доступа к ним. В частности, вы изучили порядок использования классов коллекций-обобщений для создания коллекций, безопасных в отношении применяемых типов. Вы также научились создавать лямбда-выражения для поиска в коллекциях указанных элементов.

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 19 «Перечисляемые коллекции».

Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелк­ните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Создать новую коллекцию

Воспользуйтесь конструктором для класса коллекции, например:

List<PlayingCard> cards = new List<PlayingCard>();

Добавить к коллекции еще один элемент

Для списков, хэш-наборов и словарно-ориентированных коллекций воспользуйтесь (в зависимости от обстоятельств) методами Add или Insert. Для Queue<T>-коллекций воспользуйтесь методом Enqueue, а для Stack<T>-коллекций — методом Push, например:

HashSet<string> employees = new HashSet<string>();

employees.Add(«John»);

...

LinkedList<int> data = new LinkedList<int>();

data.AddFirst(101);

...

Stack<int> numbers = new Stack<int>();

numbers.Push(99);

Удалить элемент из коллекции

Для списков, хэш-наборов и словарно-ориентированных коллекций воспользуйтесь методом Remove. Для Queue<T>-коллекций воспользуйтесь методом Dequeue, а для Stack<T>-коллекций — методом Pop, например:

HashSet<string> employees = new HashSet<string>();

employees.Remove(«John»);

...

LinkedList<int> data = new LinkedList<int>();

data.Remove(101);

...

Stack<int> numbers = new Stack<int>();

...

int item = numbers.Pop();

Определить количество элементов коллекции

Воспользуйтесь свойством Count, например:

List<PlayingCard> cards = new List<PlayingCard>();

...

int noOfCards = cards.Count;

Найти в коллекции указанный элемент

Для словарно-ориентированных коллекций воспользуйтесь системой записи, присущей массивам. Для списков воспользуйтесь методом Find, например:

Dictionary<string, int> ages =

new Dictionary<string, int>();

ages.Add(«John», 47);

int johnsAge = ages[«John»];

...

List<Person> personnel = new List<Person>();

Person match = personnel.Find(p => p.ID == 3);

 

Примечание: в классах коллекций Stack<T>, Queue<T> и хэш-наборов поиск не поддерживается, хотя в хэш-наборе вы можете протестировать наличие того или иного элемента, воспользовавшись для этого методом Contains

Совершить последовательный обход всех элементов коллекции

Воспользуйтесь инструкцией for или foreach, например:

LinkedList<int> numbers = new LinkedList<int>();

...

for (LinkedListNode<int> node = numbers.First;

     node != null; node = node.Next)

{

    int number = node.Value;

    Console.WriteLine(number);

}

...

foreach (int number in numbers)

{

    Console.WriteLine(number);

}

Назад: 17. Введение в обобщения
Дальше: 19. Перечисляемые коллекции

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com