Коллекции являются одним из наиболее часто используемых инструментов в разработке программного обеспечения. В этом уроке мы познакомимся с пространством имен System.Collections.Generic, коллекциями List, Dictionary и типом Tuple.
- Что такое коллекции?
- Коллекции в языке C#. Пространство имен System.Collections.Generic
- Кортежи Tuple
Исходный код примеров из этой статьи можете скачать из нашего github-репозитория.
Коллекции
Самым примитивным способом хранения объектов в C# является использование массивов. Одной из основных проблем, с которой столкнется разработчик следуя такому подходу, является то, что массивы не предоставляют инструментов для динамического изменения размера. В языке C# есть два пространства имен для работы со структурами данных:
- System.Collections;
- System.Collections.Generic.
Первое из них – System.Collections предоставляет структуры данных для хранения объектов типа Object. У этого решения есть две основных проблемы – это производительность и безопасность типов. В настоящее время не рекомендуется использовать объекты классов из System.Collections.
Для решения указанных выше проблем Microsoft были разработаны коллекции с обобщенными типами (их ещё называют дженерики), они расположены в пространстве имен System.Collections.Generic. Суть их заключается в том, что вы не просто создает объект класса List, но и указываете, объекты какого типа будут в нем храниться, делается это так: List<T>, где T может быть int, string, double или какой-то ваш собственный класс.
В рамках данного урока мы не будем подробно останавливаться на особенностях обобщенных типов, на текущий момент можете их воспринимать как псевдонимы, для реальных типов данных.
Коллекции в языке C#. Пространство имен System.Collections.Generic
Пространство System.Collections.Generic содержит большой набор коллекций, которые позволяют удобно и эффективно решать широкий круг задач. Ниже, в таблице, перечислены некоторые из обобщенных классов с указанием интерфейсов, которые они реализуют.
| Обобщенный класс | Основные интерфейсы | Описание |
| List<T> | ICollection<T>, IEnumerable<T>, IList<T> | Список элементов с динамически изменяемым размером |
| Dictionary<TKey, TValue> | ICollection<T>, IDictionary<TKey, TValue>,
IEnumerable<T> |
Коллекция элементов связанных через уникальный ключ |
| Queue<T> | ICollection, IEnumerable<T> | Очередь – список, работающий по алгоритму FIFO |
| Stack<T> | ICollection, IEnumerable<T> | Стэк – список, работающий по алгоритму LIFO |
| SortedList<TKey,TValue> | IComparer<T>, ICollection<KeyValuePair<TKey,TValue>>, IDictionary<TKey,TValue> | Коллекция пар “ключ-значение”, упорядоченных по ключу |
Познакомимся поближе с несколькими классами из приведенной таблицы.
Класс List<T>
Начнем наше знакомство с коллекциями с класса List<T>. Эта коллекция является аналогом типизированного массива, который может динамически расширяться. В качестве типа можно указать любой встроенный либо пользовательский тип.
Создание объекта класса List<T>
Можно создать пустой список и добавить в него элементы позже, с помощью метода Add():
List<int> numsList = new List<int>(); numsList.Add(1);
Либо воспользоваться синтаксисом, позволяющем указать набор объектов, который будет храниться в списке:
List<int> nums = new List<int> {1, 2, 3, 4, 5};
var words = new List<string> {"one", "two", "three"};
Работа с объектами List<T>
Ниже приведены таблицы, в которых перечислены некоторые полезные свойства и методы класса List<T>. Более подробную информацию по методам и свойствам List<T> вы можете найти в официальной документации.
Свойства класса List<T>
| Свойство | Описание |
| Count | Количество элементов в списке |
| Capacity | Емкость списка – количество элементов, которое может вместить список без изменения размера |
Console.WriteLine("Свойства");
Console.WriteLine($"- Count: nums.Count = {nums.Count}");
Console.WriteLine($"- Capacity: nums.Capacity = {nums.Capacity}");
Методы класса List<T>
| Метод | Описание |
| Add(T) | Добавляет элемент к списку |
| BinarySearch(T) | Выполняет поиск по списку |
| Clear() | Очистка списка |
| Contains(T) | Возвращает true, если список содержит указанный элемент |
| IndexOf(T) | Возвращает индекс переданного элемента |
| ForEach(Action<T>) | Выполняет указанное действие для всех элементов списка |
| Insert(Int32, T) | Вставляет элемент в указанную позицию |
| Find(Predicate<T>) | Осуществляет поиск первого элемент, для которого выполняется заданный предикат |
| Remove(T) | Удаляет указанный элемент из списка |
| RemoveAt(Int32) | Удаляет элемент из заданной позиции |
| Sort() | Сортирует список |
| Reverse() | Меняет порядок расположения элементов на противоположный |
Console.WriteLine($"nums: {ListToString(nums)}");
nums.Add(6);
Console.WriteLine($"nums.Add(6): {ListToString(nums)}");
Console.WriteLine($"words.BinarySearch(\"two\"): {words.BinarySearch("two")}");
Console.WriteLine($"nums.Contains(10): {nums.Contains(10)}");
Console.WriteLine($"words.IndexOf(\"three\"): {words.IndexOf("three")}");
Console.WriteLine($"nums.ForEach(v => v * 10)");
nums.ForEach(v => Console.Write($"{v} => "));
nums.Insert(3, 7);
Console.WriteLine($"nums.Insert(3, 7): {ListToString(nums)}");
Console.WriteLine($"words.Find(v => v.Length == 3): {words.Find(v => v.Length == 3)}");
words.Remove("two");
Console.WriteLine($"words.Remove(\"two\"): {ListToString(words)}");
Код метода ListToString:
static private string ListToString<T>(List<T> list) =>
"{" + string.Join(", ", list.ToArray()) + "}";
Далее приведен пример работы со списком, в котором хранятся объекты пользовательского типа. Создадим класс Player, имеющий свойства: Name и Skill.
class Player
{
public string Name { get; set; }
public string Skill { get; set; }
}
Создадим список игроков и выполним с ним ряд действий:
Console.WriteLine("Работа с пользовательским типом");
List<Player> players = new List<Player> {
new Player { Name = "Psy", Skill = "Monster"},
new Player { Name = "Kubik", Skill = "Soldier"},
new Player { Name = "Triver", Skill = "Middle"},
new Player { Name = "Terminator", Skill = "Very High"}
};
Console.WriteLine("Количество элементов в players:{0}", players.Count);
//Добавим новый элемент списка players
players.Insert(1, new Player { Name = "Butterfly", Skill = "flutter like a butterfly, pity like a bee"});
//Посмотрим на все элементы списка
players.ForEach(p => Console.WriteLine($"{p.Name}, skill: {p.Skill}"));
Класс Dictionary<TKey,TValue>
Класс Dictionary реализует структуру данных Отображение, которую иногда называют Словарь или Ассоциативный массив. Идея довольно проста: в обычном массиве доступ к данным мы получаем через целочисленный индекс, в словаре используется ключ, который может быть числом, строкой или любым другим типом данных, который реализует метод GetHashCode(). При добавлении нового объекта в такую коллекцию для него указывается уникальный ключ, который используется для последующего доступа к нему.
Создание объекта класса Dictionary
Пустой словарь:
var dict = new Dictionary<string, int>();
Словарь с набором элементов:
var prodPrice = new Dictionary<string, double>()
{
["bread"] = 23.3,
["apple"] = 45.2
};
Console.WriteLine($"bread price: {prodPrice["bread"]}");
Работа с объектами Dictionary
Рассмотрим некоторые из свойств и методов класса Dictionary<TKey, TValue>. Полное описание возможностей этого класса вы можете найти на официальной странице Microsoft.
Свойства класса Dictionary
| Свойство | Описание |
| Count | Количество объектов в словаре |
| Keys | Ключи словаря |
| Values | Значения элементов словаря |
Console.WriteLine("Свойства");
Console.WriteLine($"Словарь prodPrice: {DictToString(prodPrice)}");
Console.WriteLine($"Count: {prodPrice.Count}");
Console.WriteLine($"Keys: {ListToString(prodPrice.Keys.ToList<string>())}");
Console.WriteLine($"Values: {ListToString(prodPrice.Values.ToList<double>())}");
Методы класса Dictionary
| Метод | Описание |
| Add(TKey, TValue) | Добавляет в словарь элемент с заданным ключом и значением |
| Clear() | Удаляет из словаря все ключи и значения |
| ContainsValue(TValue) | Проверяет наличие в словаре указанного значения |
| ContainsKey(TKey) | Проверяет наличие в словаре указанного ключа |
| GetEnumerator() | Возвращает перечислитель для перебора элементов словаря |
| Remove(TKey) | Удаляет элемент с указанным ключом |
| TryAdd(TKey, TValue) | Метод, реализующий попытку добавить в словарь элемент с заданным ключом и значением |
| TryGetValue(TKey, TValue) | Метод, реализующий попытку получить значение по заданному ключу |
prodPrice.Add("tomate", 11.2);
Console.WriteLine($"Словарь prodPrice: {DictToString(prodPrice)}");
var isExistValue = prodPrice.ContainsValue(11.2);
Console.WriteLine($"isExistValue = {isExistValue}");
var isExistKey = prodPrice.ContainsKey("tomate");
Console.WriteLine($"isExistKey = {isExistKey}");
prodPrice.Remove("bread");
Console.WriteLine($"Словарь prodPrice: {DictToString(prodPrice)}");
var isOrangeAdded = prodPrice.TryAdd("orange", 20.1);
Console.WriteLine($"isOrangeAdded = {isOrangeAdded}");
double orangePrice;
var isPriceGetted = prodPrice.TryGetValue("orange", out orangePrice);
Console.WriteLine($"isPriceGetted = {isPriceGetted}");
Console.WriteLine($"orangePrice = {orangePrice}");
prodPrice.Clear();
Console.WriteLine($"Словарь prodPrice: {DictToString(prodPrice)}");
Кортежи Tuple и ValueTuple
Относительно недавним нововведением в языке C# (начиная с C# 7) являются кортежи. Кортежем называют структуру данных типа Tuple или ValueTuple (чуть ниже мы рассмотрим различия между ними), которые позволяют группировать объекты разных типов друг с другом. С практической точки зрения они являются удобным способом возврата из метода нескольких значений – это наиболее частый вариант использования кортежей.
Различия между Tuple и ValueTuple приведены в таблице ниже.
| Tuple | ValueTuple |
| Ссылочный тип | Тип значение |
| Неизменяемый тип | Изменяемый тип |
| Элементы данных – это свойства | Элементы данных – это поля |
Создание кортежей
Рассмотрим несколько вариантов создания кортежей.
Создание кортежа без явного и с явным указанием имен полей:
(string, int) p1 = ("John", 21);
(string Name, int Age) p2 = ("Mary", 23);
При этом для доступа к элементам кортежа в первом варианте используются свойства Item с числом, указывающем на порядок элемента, во втором – заданные пользователем имена:
Console.WriteLine($"p1: Name: {p1.Item1}, Age: {p1.Item2}");
Console.WriteLine($"p1: Name: {p2.Name}, Age: {p2.Age}");
Возможны следующие способы создания кортежей с явным заданием имен:
var p3 = (Name: "Alex", Age: 24);
var Name = "Lynda";
var Age = 25;
var p4 = (Name, Age);
Console.WriteLine($"p3: Name: {p3.Name}, Age: {p3.Age}");
Console.WriteLine($"p4: Name: {p4.Name}, Age: {p4.Age}");
При этом возможность обращаться через свойства Item1 и Item2 для созданных выше переменных остается:
Console.WriteLine($"p3: Name: {p3.Item1}, Age: {p3.Item2}");
Console.WriteLine($"p4: Name: {p4.Item1}, Age: {p4.Item2}");
Работа с кортежами
Как было сказано в начале раздела, кортежи можно возвращать в качестве результата работы метода. Пример метода, который сравнивает длину переданной строки с некоторым порогом и возвращает соответствующее bool-значение и целое число – длину строки:
static (bool isLonger, int count) LongerThenLimit(string str, int limit) => str.Length > limit ? (true, str.Length) : (false, str.Length);
Кортежи можно присваивать друг другу, при этом необходимо, чтобы соблюдались следующие условия:
- количество элементов в обоих кортежах одинаковое;
- типы соответствующих элементов совпадают, либо могут быть приведены друг к другу.
var p5 = ("Jane", 26);
(string, int) p6 = p5;
Console.WriteLine($"p6: Name: {p6.Item1}, Age: {p6.Item2}");
Операцию присваивания можно использовать для деструкции кортежа.
(string name, int age) = p5;
Console.WriteLine($"Name: {name}, Age: {age}");
Исходный код примеров из этой статьи можете скачать из нашего github-репозитория.