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

Эволюция кортежей в C#
Инструмент автодополнения для кортежа отображает именованные элементы как свойства

В 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 в качестве рефакторинга этой переменной:

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

Эволюция кортежей в C#
Метод 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#

Сообщение Эволюция кортежей в C# появились сначала на NOP::Nuances of programming.

Похожие записи

AEGIS Algorithms Android Angular Apache Airflow Apache Druid Apache Flink Apache Spark API API Canvas AppSec Architecture Artificial Intelligence Astro Authentication Authorization AutoGPT AWS AWS Aurora AWS Boto3 AWS EC2 AWS Lambda Azure Babylon.js Backend bash Beautiful Soup Bento UI Big Data Binary Tree Browser API Bun Career Cassandra Charts ChatGPT Chrome Extension Clean Code CLI ClickHouse Coding Codux Combine Compose Computer Context Fusion Copilot Cosmo Route CProgramming cron Cryptography CSS CTF Cypress DALL-E Data Analysis Data science Database dbt dbt Cloud deno Design Design Patterns Detekt Development Distributed Systems Django Docker Docker Hub Drizzle DRY DuckDB Express FastAPI Flask Flutter For Beginners Front End Development Game Development GCN GCP Geospatial Git GitHub Actions GitHub Pages Gitlab GMS GoFr Golang Google Google Sheets Google Wire GPT-3 GPT3 Gradio Gradle Grafana Graphic Design GraphQL gRPC Guidance HMS Hotwire HTML Huawei HuggingFace IndexedDB InfoSec Interview iOS Jackknife Java JavaScript Jetpack Compose JSON Kafka Kotlin Kubernetes LangChain Laravel Linux LlaMA LLM localStorage Logging Machine Learning Magento Math Mermaid Micro Frontends Mobile Mobile App Development mondayDB MongoDB Mongoose MySQL Naming NestJS NET NetMock Networks NextJS NLP Node.js Nodejs NoSQL NPM OOP OpenAI OTP Pandas PDF PHP Playwright Plotly Polars PostgreSQL Prefect Productivity Programming Prometheus Puppeteer Pushover Python Pytorch Quarkus Rabbitmq RAG Ramda Raspberry Pi React React Native Reactor Redis REST API Revolut Riverpod RProgramming Ruby Ruby on Rails Rust Scalene SCDB ScyllaDB Selenium Servers Sklearn SLO SnowFlake Snowkase Software Architecture Software Development Solara Solid Spring Boot SQL SQLite Streamlit SudoLang Supabase Swift SwiftUI Tailwind CSS Taipy Terraform Testing Transformers TURN TypeScript Ubuntu UI Design Unix UX UX Design Vim Vite VSCode Vue Web Architecture Web Components Web Development Web Frameworks Web Scraping Web-разработка Webassembly Websocket Whisper Widgets WordPress YAML YouTube Наука о данных Разное Тренды

Go — единственный выбор для бэкенд-разработчика?

Вы пропустили

AEGIS Algorithms Android Angular Apache Airflow Apache Druid Apache Flink Apache Spark API API Canvas AppSec Architecture Artificial Intelligence Astro Authentication Authorization AutoGPT AWS AWS Aurora AWS Boto3 AWS EC2 AWS Lambda Azure Babylon.js Backend bash Beautiful Soup Bento UI Big Data Binary Tree Browser API Bun Career Cassandra Charts ChatGPT Chrome Extension Clean Code CLI ClickHouse Coding Codux Combine Compose Computer Context Fusion Copilot Cosmo Route CProgramming cron Cryptography CSS CTF Cypress DALL-E Data Analysis Data science Database dbt dbt Cloud deno Design Design Patterns Detekt Development Distributed Systems Django Docker Docker Hub Drizzle DRY DuckDB Express FastAPI Flask Flutter For Beginners Front End Development Game Development GCN GCP Geospatial Git GitHub Actions GitHub Pages Gitlab GMS GoFr Golang Google Google Sheets Google Wire GPT-3 GPT3 Gradio Gradle Grafana Graphic Design GraphQL gRPC Guidance HMS Hotwire HTML Huawei HuggingFace IndexedDB InfoSec Interview iOS Jackknife Java JavaScript Jetpack Compose JSON Kafka Kotlin Kubernetes LangChain Laravel Linux LlaMA LLM localStorage Logging Machine Learning Magento Math Mermaid Micro Frontends Mobile Mobile App Development mondayDB MongoDB Mongoose MySQL Naming NestJS NET NetMock Networks NextJS NLP Node.js Nodejs NoSQL NPM OOP OpenAI OTP Pandas PDF PHP Playwright Plotly Polars PostgreSQL Prefect Productivity Programming Prometheus Puppeteer Pushover Python Pytorch Quarkus Rabbitmq RAG Ramda Raspberry Pi React React Native Reactor Redis REST API Revolut Riverpod RProgramming Ruby Ruby on Rails Rust Scalene SCDB ScyllaDB Selenium Servers Sklearn SLO SnowFlake Snowkase Software Architecture Software Development Solara Solid Spring Boot SQL SQLite Streamlit SudoLang Supabase Swift SwiftUI Tailwind CSS Taipy Terraform Testing Transformers TURN TypeScript Ubuntu UI Design Unix UX UX Design Vim Vite VSCode Vue Web Architecture Web Components Web Development Web Frameworks Web Scraping Web-разработка Webassembly Websocket Whisper Widgets WordPress YAML YouTube Наука о данных Разное Тренды

Go — единственный выбор для бэкенд-разработчика?

AEGIS Algorithms Android Angular Apache Airflow Apache Druid Apache Flink Apache Spark API API Canvas AppSec Architecture Artificial Intelligence Astro Authentication Authorization AutoGPT AWS AWS Aurora AWS Boto3 AWS EC2 AWS Lambda Azure Babylon.js Backend bash Beautiful Soup Bento UI Big Data Binary Tree Browser API Bun Career Cassandra Charts ChatGPT Chrome Extension Clean Code CLI ClickHouse Coding Codux Combine Compose Computer Context Fusion Copilot Cosmo Route CProgramming cron Cryptography CSS CTF Cypress DALL-E Data Analysis Data science Database dbt dbt Cloud deno Design Design Patterns Detekt Development Distributed Systems Django Docker Docker Hub Drizzle DRY DuckDB Express FastAPI Flask Flutter For Beginners Front End Development Game Development GCN GCP Geospatial Git GitHub Actions GitHub Pages Gitlab GMS GoFr Golang Google Google Sheets Google Wire GPT-3 GPT3 Gradio Gradle Grafana Graphic Design GraphQL gRPC Guidance HMS Hotwire HTML Huawei HuggingFace IndexedDB InfoSec Interview iOS Jackknife Java JavaScript Jetpack Compose JSON Kafka Kotlin Kubernetes LangChain Laravel Linux LlaMA LLM localStorage Logging Machine Learning Magento Math Mermaid Micro Frontends Mobile Mobile App Development mondayDB MongoDB Mongoose MySQL Naming NestJS NET NetMock Networks NextJS NLP Node.js Nodejs NoSQL NPM OOP OpenAI OTP Pandas PDF PHP Playwright Plotly Polars PostgreSQL Prefect Productivity Programming Prometheus Puppeteer Pushover Python Pytorch Quarkus Rabbitmq RAG Ramda Raspberry Pi React React Native Reactor Redis REST API Revolut Riverpod RProgramming Ruby Ruby on Rails Rust Scalene SCDB ScyllaDB Selenium Servers Sklearn SLO SnowFlake Snowkase Software Architecture Software Development Solara Solid Spring Boot SQL SQLite Streamlit SudoLang Supabase Swift SwiftUI Tailwind CSS Taipy Terraform Testing Transformers TURN TypeScript Ubuntu UI Design Unix UX UX Design Vim Vite VSCode Vue Web Architecture Web Components Web Development Web Frameworks Web Scraping Web-разработка Webassembly Websocket Whisper Widgets WordPress YAML YouTube Наука о данных Разное Тренды

Производительность в Jetpack Compose: стабильность и неизменяемость