Подробное знакомство с кортежами в 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


Переместимся на семь лет вперед в 2017 год, когда вышел C#7. В этой версии кортежам было уделено особое внимание.


Теперь нам уже не нужно было использовать сам класс Tuple, а для выражения намерений в нашем распоряжении появились специальные конструкции.


Определение нового кортежа


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


private (bool, int) GetData()
{
return (true, 47);
}

Неплохо, но относительно извлечения данных ничего не изменилось —  это по-прежнему происходило при помощи Item1 и Item2.


Именование элементов в кортеже


Однако так все работало, пока вы не присваивали этим элементам имена:


private (bool isSuccessful, int totalItems) GetData()
{
return (true, 47);
}

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


Автоподстановка для Tuple показывает именованные элементы в качестве свойств

Да, в 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’

Здесь у нас два варианта:



  • использовать вместо 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;

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


Игнорирование некоторых элементов кортежа


А как быть, если некоторые возвращаемые кортежем элементы вас не интересуют? Обязательно ли придется сталкиваться с этими лишними переменными, которые вы все равно игнорируете? Нет!


Для любого элемента можно использовать отмену (discard), и соответствующий фрагмент кода весьма прост:


(bool isSuccessful, _) = GetData();

Вместо определения int totaltime мы просто используем нижнее подчеркивание. Таким образом мы сообщаем компилятору, что не будем использовать этот элемент, и он не будет загромождать им оставшуюся часть кода.


Что можно хранить в кортеже?


Хранить в кортеже можно практически все. Если сильно захочется, то можно даже создавать кортежи функций. Вот один из способов:


private (Func<bool> function1, Func<bool> function2) GetFunctionTuples()
{
return (() => true, () => false);
}

Сколько элементов вмещает кортеж?


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


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


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


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


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



384   0  

Comments

    Ничего не найдено.