Эволюция кортежей в C#

Какие же классные эти кортежи! Отчетливо помню времена до их появления: сидишь и думаешь, как лучше всего вернуть несколько значений из метода. Как только кортежи были добавлены в 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 в качестве рефакторинга этой переменной: 

Варианты рефакторинга для кортежа var

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. При таком подходе вложенные кортежи хранят дополнительные элементы в случае их большого размера. Рассмотрим код: 

Метод 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, который частично решил проблему управления несколькими элементами данных. Конечным этапом эволюции кортежей стал новейший синтаксис, позволяющий быстро и легко создавать структуры для хранения необходимых данных. 

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

Предоставляется возможность сравнения кортежей. При этом следует помнить, что сравнивается порядок значений внутри кортежа, а не именованные элементы. 

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Jamie Burns: A Deep Dive Into Tuples in C#

Предыдущая статьяВнешнее конфигурирование базы данных Spring Boot с помощью AWS Secrets Manager
Следующая статьяСегментация изображений с использованием сети обратного внимания