Какие же классные эти кортежи! Отчетливо помню времена до их появления: сидишь и думаешь, как лучше всего вернуть несколько значений из метода. Как только кортежи были добавлены в C#, я сразу увидел отличные перспективы их эффективного применения.
С каждой новой версией C# они дорабатывались и улучшались. Если вам довелось работать только с самыми ранними вариантами кортежей, вы бы сейчас их не узнали. Это и не удивительно: настолько они изменились. Но все к лучшему.
Рассмотрим путь развития кортежей с момента их создания и узнаем, как они используются сейчас.
C# 4: рождение кортежей
В далеком 2010 году появилась новая версия .NET и C#, где и были впервые представлены кортежи.
Цель этой новой для C# концепции заключалась в упрощении работы с небольшим количеством значений. До кортежей приходилось либо создавать пользовательский класс/структуру для хранения значений, либо задействовать набор параметров ref
. Но оба способа были далеки от совершенства.
Итак, мы получили новый класс с ожидаемым названием Tuple
для хранения нескольких элементов данных в одной структуре:
private Tuple<bool, int> GetData()
{
return new Tuple<bool, int>(true, 47);
}
Выглядит довольно просто.
Определяем кортеж с обобщениями для указания типов данных, которые он будет содержать. После этого создаем кортеж с возможностью его передачи как единой структуры.
Теперь извлекаем значения из полученного кортежа:
Tuple<bool, int> tuple = new Tuple<bool, int>(true, 47);
bool boolValue = tuple.Item1; // 'true'
int intValue = tuple.Item2; // '47'
Сказано-сделано. Но, как по мне, свойства Item1
и Item2
мало о чем говорят. При отдельном их рассмотрении сложно сказать, что представляют собой эти значения.
Изначальная задача решена, но есть возможности для улучшения.
C# 7: совершенствование кортежей
Переносимся на 7 лет вперед — в 2017 год. Тогда состоялся выпуск C# 7, в котором много внимания уделялось кортежам.
Программистам больше не нужно было использовать сам класс Tuple
. Им предоставлялась возможность задействовать специальные конструкции в самом языке для воплощения своих намерений.
Определение нового кортежа
Эта версия предоставляла более быстрый способ создания кортежа. Вместо создания экземпляра класса Tuple
мы просто заключали элементы в скобки, как показано ниже:
private (bool, int) GetData()
{
return (true, 47);
}
Отметим лаконизм данного способа. Однако ничего не изменилось при попытке извлечь данные из кортежа — у нас все те же Item1
и Item2
.
Именование элементов в кортеже
Как только мы даем имена элементам, ситуация меняется:
private (bool isSuccessful, int totalItems) GetData()
{
return (true, 47);
}
В сигнатуру метода можно добавлять имена к определению кортежа. В самом способе создания кортежа, возвращаемого таким образом, ничего не меняется. Но посмотрите, что показывает IntelliSense
при попытке извлечь данные из этой структуры:
В Visual Studio 2022 года и более ранних версиях мы получаем имена элементов, которые значатся как свойства и применяются как свойства класса. Пример кода:
var returnedData = GetData();
bool boolValue = returnedData.isSuccessful;
int intValue = returnedData.totalItems;
Теперь отчетливо видно, что означает каждый элемент в кортеже. Случись вам задействовать кортеж с целой кучей элементов int
, вы обязательно меня поблагодарите. Ведь теперь не придется запоминать нужный для работы элемент: Item5
или Item6
.
Также предоставляется возможность называть элементы при создании кортежа. Этот прием особенно эффективен, если вы создаете встроенный кортеж, а не возвращаете его из метода. Вот строка кода:
var tupleData = (isSuccessful: true, totalItems: 47);
Var с кортежами
Как видно из предыдущего примера, var
используется в качестве типа локальной переменной, что очень удобно.
Однако, применяя только var
, мы упускаем возможность узнать дополнительные приемы работы с кортежами. Посмотрим, что предлагает Visual Studio в качестве рефакторинга этой переменной:
2 варианта рефакторинга:
- явный тип вместо
var
; - деконструкция объявления переменной.
Рассмотрим их по очереди.
Определение кортежа как явного типа вместо var
Для использования явного типа вместо var
потребуется та же самая конструкция, что была в сигнатуре метода:
(bool isSuccessful, int totalItems) returnedData = GetData();
bool boolValue = returnedData.isSuccessful;
int intValue = returnedData.totalItems;
По ней отчетливо видно, какие элементы содержатся в кортеже.
Рассмотрим еще один вариант работы с кортежами, который выводит их на новый уровень оптимизации.
Деконструкция объявления переменной кортежа
Когда мы возвращаем кортеж из метода, и нас интересует не сам кортеж, а его содержимое, то можно деконструировать объявление на элементы данных и работать с ними напрямую как с переменными. Пример кода:
(bool isSuccessful, int totalItems) = GetData();
bool boolValue = isSuccessful;
int intValue = totalItems;
Несмотря на необычный вид, возможность определять переменные прямо из кортежа несет в себе практическую пользу. Для остальной части кода совсем не важно, что они получены из кортежа. А вам не нужно самостоятельно определять и инстанцировать другие локальные переменные.
Игнорирование некоторых элементов из кортежа
Что делать, если вы не собираетесь задействовать какие-то элементы, возвращаемые из кортежа? Стоит ли оставлять в коде лишние переменные, которые вы игнорируете?
Нет!
Можно отбросить все ненужное. Как видно из примера, эта задача решается с помощью простого кода:
(bool isSuccessful, _) = GetData();
Вместо определения int totalItems
применяется знак нижнего подчеркивания _
. Так мы сообщаем компилятору, что не собираемся задействовать этот элемент. Как следствие, он больше не загромождает остальную часть кода.
Что можно хранить в кортеже?
В кортеже можно хранить практически все что угодно. При желании можно создать кортеж функций. Рассмотрим один из способов:
private (Func<bool> function1, Func<bool> function2) GetFunctionTuples()
{
return (() => true, () => false);
}
Сколько элементов вмещает кортеж?
Традиционный класс Tuple
допускает размещение максимум 8 элементов в одном кортеже. Если нужно больше, то кортежем становится последний элемент. Суть в том, чтобы вложить объекты кортежа друг в друга для хранения всего необходимого.
Однако новый синтаксис для кортежей не предусматривает таких ограничений. Кортеж будет хранит все, что вы пропишите.
Интересно, что для работы с кортежами, созданными подобным образом, предоставляется метод .ToTuple()
, который преобразует новоформатный кортеж в тип класса Tuple
. При таком подходе вложенные кортежи хранят дополнительные элементы в случае их большого размера. Рассмотрим код:
Сравнение кортежей
Кортежи — это типы значений, что подразумевает возможность их сравнения. Однако во избежание ошибок следует учитывать ряд моментов.
При наличии двух кортежей с одинаковыми значениями проверка на равенство покажет, что они равны. Пример кода:
var tuple1 = (1, 2, 3);
var tuple2 = (1, 2, 3);
bool isEqual = tuple1.Equals(tuple2); // Результат 'true'
Начиная с C# 7.3, операции равенства можно выполнять с помощью ==
:
var tuple1 = (1, 2, 3);
var tuple2 = (1, 2, 3);
bool isEqual = tuple1 == tuple2; // Результат 'true'
Прежде всего, отметим, что сравнение выполняется по значениям и не учитывает имена элементов данных. Поэтому два кортежа с разными именами, но с одинаковыми значениями, все равно рассматриваются как равные. Пример кода:
var tuple1 = (element1: 1, element2: 2, element3: 3);
var tuple2 = (other1: 1, other2: 2, other3: 3);
bool isEqual = tuple1 == tuple2; // Результат 'true'
Важно помнить: именованные элементы могут совпадать, но сравнивается всегда порядок значений:
var tuple1 = (element1: 1, element2: 2, element3: 3);
var tuple2 = (element3: 3, element2: 2, element1: 1);
bool isEqual = tuple1 == tuple2; // Результат 'false'
Это легко проверить с помощью модульного теста:
[TestMethod]
public void CompareTuple()
{
var tuple1 = (element1: 1, element2: 2, element3: 3);
var tuple2 = (element3: 3, element2: 2, element1: 1);
Assert.AreEqual(tuple1, tuple2);
}
Тест не пройдет и выдаст ошибку, что равенство не подтверждено:
Assert.AreEqual failed. Expected:<(1, 2, 3)>. Actual:<(3, 2, 1)>.
Как видно, здесь перечисляются не имена элементов, а только значения в указанном порядке внутри кортежа.
Наилучшим источником полезной информации по проверке равенства кортежей является документация Microsoft.
Заключение
Кортежи прошли долгий путь в своем развитии. Отсчет начался с класса Tuple
, который частично решил проблему управления несколькими элементами данных. Конечным этапом эволюции кортежей стал новейший синтаксис, позволяющий быстро и легко создавать структуры для хранения необходимых данных.
Намного упрощен процесс именования элементов внутри кортежа, так что сразу становится понятно, о каких данных идет речь. Разработан простой синтаксис для извлечения из кортежа элементов с последующим их использованием в коде.
Предоставляется возможность сравнения кортежей. При этом следует помнить, что сравнивается порядок значений внутри кортежа, а не именованные элементы.
Читайте также:
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jamie Burns: A Deep Dive Into Tuples in C#