Урок 7.1 — Распаковка кортежей и деконструкция
Кортежи — временные контейнеры для нескольких значений. Деконструкция и обмен переменных.
Представь, что тебе нужно передать координаты точки — X и Y. Можно передавать два числа отдельно, а можно упаковать их в кортеж — как будто ты складываешь две вещи в один пакет. В C# кортежи позволяют хранить несколько значений разных типов в одной переменной, а деструктуризация — удобно «распаковывать» их обратно.
Что такое кортеж?
Кортеж (tuple) — это упорядоченный набор значений, объединённых в одну переменную. Элементы кортежа могут быть разных типов. Это удобно, когда нужно вернуть из метода несколько значений или временно сгруппировать связанные данные.
В C# есть два вида кортежей:
(int, string) — синтаксис в скобкахTuple<int, string> — обобщённый класс.Item1, .Item2Разница в том, что Tuple<> — это класс из .NET Framework 4.0, а (int, string) — это value tuple, появившийся в C# 7.0. Используй всегда value tuple — он быстрее, удобнее и современнее.
// Value tuple (современный способ)
(int, string) pair = (42, "Ответ");
Console.WriteLine(pair.Item1); // 42
Console.WriteLine(pair.Item2); // Ответ
// Старый класс Tuple (лучше не использовать)
Tuple<int, string> old = Tuple.Create(42, "Ответ");
Console.WriteLine(old.Item1); // 42
// Создание кортежа из метода
(int, int, string) GetPerson()
{
return (25, 180, "Анна");
}
var person = GetPerson();
Console.WriteLine($"{person.Item3}, {person.Item1} лет, {person.Item2} см");
// → Анна, 25 лет, 180 см
Именованные элементы кортежа
Обращаться к элементам через .Item1, .Item2 — неудобно и нечитаемо. C# позволяет давать элементам осмысленные имена:
// Явные имена в объявлении
(int Age, int Height, string Name) person = (25, 180, "Анна");
Console.WriteLine($"{person.Name}, {person.Age} лет");
// Имена в возвращаемом типе метода
(int X, int Y) GetPoint()
{
return (10, 20);
}
var point = GetPoint();
Console.WriteLine($"X = {point.X}, Y = {point.Y}");
// При создании кортежа — имена подхватываются автоматически
int x = 5;
int y = 7;
var coord = (x, y); // имена будут x и y!
Console.WriteLine(coord.x); // 5
// Смешивание: можно задать имена частично
var data = (Name: "Иван", 30); // второй элемент без имени → Item2
Console.WriteLine(data.Name); // Иван
Console.WriteLine(data.Item2); // 30
Деконструкция кортежа (распаковка)
Деконструкция — это обратный процесс: мы «разбираем» кортеж на отдельные переменные. Это гораздо удобнее, чем обращаться к .Item1 каждый раз:
(int, string) pair = (42, "Ответ");
// ❌ Без деконструкции — неудобно
int number = pair.Item1;
string text = pair.Item2;
// ✅ С деконструкцией — элегантно
var (number2, text2) = pair;
Console.WriteLine(number2); // 42
Console.WriteLine(text2); // Ответ
// Можно сразу в методе:
(int X, int Y) GetPoint() => (10, 20);
// Деконструкция при вызове метода
var (x, y) = GetPoint();
Console.WriteLine($"({x}, {y})"); // (10, 20)
// Можно только часть переменных через _
var (x2, _) = GetPoint(); // y проигнорирован
Console.WriteLine(x2); // 10
Игнорирование элементов через _ (discard)
Символ _ (подчёркивание) — специальная переменная-пустышка. Она говорит компилятору: «мне не нужно это значение, не создавай для него переменную».
// Нам нужен только возраст, имя игнорируем
(string Name, int Age) GetUser() => ("Олег", 28);
var (_, age) = GetUser();
Console.WriteLine($"Возраст: {age}");
// Можно несколько discard'ов
(int a, int b, int c) GetThree() => (1, 2, 3);
var (first, _, _) = GetThree();
Console.WriteLine(first); // только 1
✅ Discard — это не переменная, её нельзя прочитать. Она существует только для того, чтобы компилятор не создавал ненужное хранилище. Идеально, когда из кортежа нужна только часть значений.
Swap через кортеж — обмен без временной переменной
Классическая задача: поменять значения двух переменных местами. Раньше для этого нужна была третья временная переменная. С кортежами — одной строкой:
// ❌ Старый способ с временной переменной
int a = 5, b = 10;
int temp = a;
a = b;
b = temp;
// a=10, b=5
// ✅ Современный способ через кортеж и деконструкцию
int x = 5, y = 10;
(x, y) = (y, x);
// x=10, y=5
// Можно и с разными типами (но obj тогда)
string name = "Анна";
int age = 30;
(name, age) = (age.ToString(), int.Parse(name)); // Осторожно с типами!
Console.WriteLine($"{name}, {age}"); // 30, Анна (строки)
(y, x) — создаётся временный кортеж. Затем он деструктурируется в левую часть (x, y). Временная переменная не нужна — всё делает компилятор.Метод Deconstruct в своих классах
Деконструкцию можно сделать для любого своего класса, если реализовать метод Deconstruct. Тогда объект твоего класса можно будет «распаковывать» так же, как кортеж:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
// Метод Deconstruct (специальное имя!)
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}
// Теперь можно делать так:
var point = new Point(3, 7);
var (x, y) = point; // вызывается Deconstruct!
Console.WriteLine($"x={x}, y={y}"); // x=3, y=7
Можно реализовать несколько перегрузок Deconstruct с разным количеством параметров:
class Person
{
public string Name { get; }
public int Age { get; }
public string City { get; }
public Person(string name, int age, string city)
=> (Name, Age, City) = (name, age, city);
// Деконструкция на 2 элемента
public void Deconstruct(out string name, out int age)
=> (name, age) = (Name, Age);
// Деконструкция на 3 элемента
public void Deconstruct(out string name, out int age, out string city)
=> (name, age, city) = (Name, Age, City);
}
var person = new Person("Мария", 32, "Казань");
var (name, age) = person; // 2 параметра
Console.WriteLine($"{name}, {age}");
var (name2, age2, city) = person; // 3 параметра
Console.WriteLine($"{name2}, {age2}, {city}");
Практический пример: работа с кортежами
Давай объединим всё, что узнали, в реальном примере — программа работы с оценками студентов:
using System;
using System.Collections.Generic;
class Program
{
// Возвращаем кортеж с именованными элементами
static (string Name, double Avg, int Min, int Max) AnalyzeGrades(
string name, List<int> grades)
{
int min = grades[0];
int max = grades[0];
int sum = 0;
foreach (var g in grades)
{
sum += g;
if (g < min) min = g;
if (g > max) max = g;
}
double avg = (double)sum / grades.Count;
return (name, avg, min, max);
}
static void Main()
{
var students = new Dictionary<string, List<int>>
{
["Анна"] = new() { 85, 90, 78, 92, 88 },
["Борис"] = new() { 70, 65, 80, 75, 72 },
["Вика"] = new() { 95, 100, 97, 92, 98 }
};
foreach (var (name, grades) in students)
{
var (_, avg, min, max) = AnalyzeGrades(name, grades);
Console.WriteLine($"{name}: ср={avg:F1}, мин={min}, макс={max}");
}
// Swap — меняем порядок
int a = 5, b = 10;
(a, b) = (b, a);
Console.WriteLine($"a={a}, b={b}"); // a=10, b=5
}
}
foreach (var (name, grades) in students) мы деструктурируем KeyValuePair прямо на ходу! Это ещё одно удобное применение распаковки.Кортеж vs класс vs анонимный тип
Когда использовать кортеж, а когда полноценный класс? Вот сравнение:
✅ Золотое правило: используй кортеж для временной группировки значений внутри метода или возврата нескольких значений из метода. Если данные имеют смысл и за пределами одного метода — создай класс или record.
Мини-тест
Вопрос 1
Какой синтаксис кортежа предпочтительнее в современном C#?
Ответ: (int, string) — value tuple (C# 7+). Он быстрее и позволяет именовать элементы.
Вопрос 2
Что будет в переменных после выполнения: (int a, int b) = (10, 20); (a, b) = (b, a);?
Ответ: a=20, b=10. Произошёл swap через кортеж.
Вопрос 3
Что делает символ _ при деконструкции кортежа?
Ответ: discard — игнорирует значение. var (x, _) = point; — x получит значение, второй элемент проигнорирован.
Вопрос 4
Какой метод нужно реализовать в классе, чтобы его можно было деструктурировать?
Ответ: public void Deconstruct(out T1 param1, out T2 param2, ...). Количество out-параметров определяет, как кортеж распаковывается.
Задание для тренировки
Напиши программу «Книжная полка», которая:
- Создаёт список кортежей
(string Title, string Author, int Year, double Price)— 5 книг - Реализует метод, который возвращает кортеж с количеством книг, средней ценой и самой старой книгой
- Выводит все книги, отсортированные по году
- Меняет местами самую дорогую и самую дешёвую книгу через swap
- Выводит результат на экран
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static (int Count, double AvgPrice, string OldestBook) AnalyzeBooks(
List<(string Title, string Author, int Year, double Price)> books)
{
int count = books.Count;
double avgPrice = books.Average(b => b.Price);
var oldest = books.OrderBy(b => b.Year).First();
return (count, avgPrice, oldest.Title);
}
static void Main()
{
var books = new List<(string Title, string Author, int Year, double Price)>
{
("Война и мир", "Толстой", 1869, 1200),
("Преступление и наказание", "Достоевский", 1866, 950),
("Мастер и Маргарита", "Булгаков", 1966, 1100),
("1984", "Оруэлл", 1949, 800),
("Гарри Поттер", "Роулинг", 1997, 1500),
};
var (count, avgPrice, oldest) = AnalyzeBooks(books);
Console.WriteLine($"Книг: {count}, ср. цена: {avgPrice:F1} руб.");
Console.WriteLine($"Самая старая: {oldest}\n");
Console.WriteLine("Книги по году:");
foreach (var (title, author, year, price) in books.OrderBy(b => b.Year))
{
Console.WriteLine($" [{year}] {title} — {author}, {price} руб.");
}
var (maxBook, minBook) = (books[0], books[0]);
foreach (var book in books)
{
if (book.Price > maxBook.Price) maxBook = book;
if (book.Price < minBook.Price) minBook = book;
}
int maxIdx = books.IndexOf(maxBook);
int minIdx = books.IndexOf(minBook);
(books[maxIdx], books[minIdx]) = (books[minIdx], books[maxIdx]);
Console.WriteLine($"\nПосле swap: самая дорогая стала на месте самой дешёвой");
}
}
Что важно запомнить
(int, string) — это struct, современный способ группировки значений. Всегда используй его вместо старого Tuple<>.(int X, int Y) point. Обращайся через point.X вместо point.Item1.var (x, y) = GetPoint() — распаковывает кортеж в отдельные переменные. Используй _ для игнорирования ненужных элементов.(a, b) = (b, a) — меняет значения местами без временной переменной.Тест: 7.1: Распаковка кортежей и деконструкция
3 вопроса