Menu

Кортежи в C#: именованные кортежи, возврат нескольких значений, деконструкция

Кортеж в C# объединяет несколько значений в одно без объявления типа. Синтаксис кортежей-значений, именованные элементы, возврат нескольких значений из метода, деконструкция через var (a, b), кортежи как ключи словаря и чем ValueTuple отличается от старого System.Tuple.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Кортеж объединяет небольшое фиксированное число значений в одно значение без объявления класса. ("Ana", 31) это кортеж из string и int. Кортежи это идиоматичный способ вернуть из метода две или три вещи и хранить недолговечные пары внутри метода.

Создание кортежа

Тип тоже записывается в скобках: (string, int) это тип кортежа, первый элемент которого string, а второй int.

Вывод:

Ana is 31
(3, 4)
179.70
(Ana, 32)

Без имён элементы называются Item1, Item2, Item3 и так далее. Эти кортежи являются структурами System.ValueTuple, поэтому присваивание одного другой переменной копирует значения, и изменение копии не затрагивает оригинал.

Именованные элементы

Item1 ничего не говорит о том, что это за значение. Дайте элементам имена либо в типе, либо в литерале:

Вывод:

Ana, 31
Desk lamp costs 24.99
Ana
Ana

Имена это удобство времени компиляции. Во время выполнения оба кортежа выше это обычные значения ValueTuple<string, int>, поэтому присваивание person кортежу с другими именами компилируется: совпадать должны только типы элементов и их порядок.

C# 7.1 и новее также выводят имена из переменных в литерале, поэтому var t = (name, age); даёт t.name и t.age без повторного написания. В C# 7.0 эти элементы доступны только как Item1 и Item2.

Возврат нескольких значений из метода

Это главная причина, по которой кортежи добавили в язык. До них вариантами были параметры out, разовый класс или System.Tuple с его безымянным Item1.

Вывод:

Lowest 60, highest 95
Average 78.75

Имена элементов в возвращаемом типе становятся именами, которые видит вызывающий код. Сравните с тем же методом на параметрах out:

static void Stats(int[] scores, out int min, out int max, out double average) { ... }

Stats(scores, out int min, out int max, out double avg);

Параметры out остаются соглашением для формы TryParse, где bool сообщает, получилось ли, а значение возвращается рядом с ним. Для «вычислить несколько вещей и вернуть их» кортеж читается лучше. См. ref и out.

Деконструкция

Деконструкция распаковывает кортеж в отдельные переменные одним оператором:

Вывод:

Lima 24.5
Lima
24.5
2 1

Имена переменных в var (city, temp) выбираете вы; они не обязаны совпадать с именами элементов кортежа, потому что деконструкция идёт по позиции. Обмен (a, b) = (b, a) сначала строит кортеж из старых значений, а затем присваивает, поэтому ни одно значение не теряется.

Ваши собственные классы могут поддерживать деконструкцию, объявив метод Deconstruct с параметрами out. Начиная с C# 7 и .NET Core 2.0 или новее такой метод есть и у KeyValuePair, поэтому цикл по словарю может распаковывать каждый элемент прямо в заголовке:

class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }
    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}

var (x, y) = new Point(3, 4);

foreach (var (name, score) in scoresByName)   // Dictionary<string, int>
{
    Console.WriteLine($"{name}: {score}");
}

Та же форма foreach (var (a, b) in listOfTuples) работает и для списка кортежей. Брать весь кортеж одной переменной цикла и читать его именованные элементы, как в следующем разделе, тоже вполне нормально.

Кортежи в списках, LINQ и словарях

Список именованных кортежей это лёгкий способ хранить строки данных внутри метода:

Вывод:

Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False

ValueTuple реализует Equals и GetHashCode поэлементно, поэтому (2, 7), созданный в двух разных местах, находит один и тот же элемент словаря. Это делает кортеж самым простым составным ключом: ни класса, ни написанной вручную хеш-функции, ни склеивания строк вроде row + ":" + col.

Равенство кортежей

Equals сравнивает поэлементно и работает с момента появления кортежей:

Вывод:

True
False

C# 7.3 добавил для кортежей == и !=, которые компилируются в такое же поэлементное сравнение и тоже не учитывают имена:

if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x"));   // True

System.Tuple и ValueTuple

В .NET 4 появился System.Tuple, класс, создаваемый через Tuple.Create. Он по-прежнему есть во фреймворке и по-прежнему встречается в старом коде и API, поэтому поиск «C# tuple» часто приводит к нему.

ValueTuple (C# 7)System.Tuple (.NET 4)
Синтаксис(1, "a"), (int, string)Tuple.Create(1, "a"), Tuple<int, string>
Видstruct (тип значения)class (ссылочный тип)
Имена элементовДаНет, только Item1, Item2
ИзменяемостьДа, элементы это поляНет, свойства только для чтения
ДеконструкцияДаДа, через методы расширения

Вывод:

Ben 25
(Ben, 25)

ToValueTuple() и ToTuple() преобразуют одно в другое. В новом коде предпочитайте кортежи-значения: они ничего не выделяют в куче и могут нести имена.

Когда вместо кортежа нужен класс или запись

Кортежи лучше всего подходят, когда группировка локальная и очевидная: метод возвращает два числа, запрос LINQ передаёт пару на следующий шаг, словарю нужен ключ из двух частей. Переходите на именованный тип, когда:

  • Одна и та же форма встречается в нескольких сигнатурах открытых методов. (string, string, int) в пяти местах это класс, который ещё не написан.
  • Значениям нужны поведение, проверка или больше трёх-четырёх элементов.
  • Данные пересекают границу, например сериализацию. Имён во время выполнения нет, поэтому Newtonsoft.Json пишет Item1 и Item2, а System.Text.Json пишет {}, потому что элементы кортежа это поля, которые он по умолчанию пропускает.

Записи C# 9 дают большую часть удобства кортежа, но с настоящим именем. У record Score(string Name, int Points); есть встроенные равенство по значению, читаемый ToString и деконструкция. См. записи.

Частые ошибки

  • Ожидание, что имена сохранятся во время выполнения. Рефлексия и сериализаторы видят Item1, Item2 (или вообще ничего).
  • Изменение копии. Кортежи это структуры: var t2 = t1; t2.Item1 = 5; не меняет t1.
  • Кортежи с большим числом элементов. После трёх-четырёх именованный тип читается лучше.
  • Путаница в порядке деконструкции. Она позиционная: с кортежем (Name, Age) запись var (age, name) = person; кладёт имя в age, и поскольку var выводит типы, ничего вас не предупредит.

Часто задаваемые вопросы

Как вернуть несколько значений из метода в C#?

Верните кортеж: объявите возвращаемый тип как (int Min, int Max) и напишите return (lowest, highest);. Вызывающий код читает result.Min и result.Max или деконструирует через var (min, max) = MinMax(data);. Альтернативы: параметры out, которые подходят для шаблона TryParse, или небольшой класс или запись, когда у результата есть собственный смысл.

Что такое именованный кортеж в C#?

Кортеж, у элементов которого есть имена вместо Item1, Item2: (string Name, int Age) person = ("Ana", 31); позволяет писать person.Name. Имена существуют только при компиляции; внутри это по-прежнему ValueTuple<string, int>, и Item1 продолжает работать.

Чем Tuple отличается от ValueTuple в C#?

System.Tuple (из .NET 4) это класс: выделяется в куче, неизменяемый, а его элементы всегда только Item1, Item2. System.ValueTuple (C# 7) это структура с изменяемыми полями, поддерживает имена элементов и синтаксис (a, b), и именно её создаёт каждый литерал кортежа. В новом коде используйте кортежи-значения.

Как работает деконструкция кортежа в C#?

var (name, age) = person; объявляет две переменные и присваивает им элементы кортежа по порядку. Можно также присваивать существующим переменным через (a, b) = (b, a), что меняет их местами, и пропускать элементы через отбрасывание _: var (_, age) = person;.

Можно ли сравнивать кортежи через == в C#?

Начиная с C# 7.3 == и != сравнивают кортежи поэлементно: (1, "a") == (1, "a") равно true. Имена не учитываются; значение имеют только позиции и значения. До C# 7.3 используйте Equals, который кортежи-значения реализуют так же.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ