Кортеж объединяет небольшое фиксированное число значений в одно значение без объявления класса. ("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, который кортежи-значения реализуют так же.