$ sudo teach IT
Модуль 7 · Обработка данных

Урок 7.1 — Распаковка кортежей и деконструкция

Кортежи — временные контейнеры для нескольких значений. Деконструкция и обмен переменных.

Представь, что тебе нужно передать координаты точки — X и Y. Можно передавать два числа отдельно, а можно упаковать их в кортеж — как будто ты складываешь две вещи в один пакет. В C# кортежи позволяют хранить несколько значений разных типов в одной переменной, а деструктуризация — удобно «распаковывать» их обратно.

📦

Что такое кортеж?

Кортеж (tuple) — это упорядоченный набор значений, объединённых в одну переменную. Элементы кортежа могут быть разных типов. Это удобно, когда нужно вернуть из метода несколько значений или временно сгруппировать связанные данные.

В C# есть два вида кортежей:

ValueTuple (рекомендуется)
Tuple (устаревший)
(int, string) — синтаксис в скобках
Tuple<int, string> — обобщённый класс
Тип значения (struct)
Ссылочный тип (class)
Можно давать имена элементам
Только .Item1, .Item2
Можно деструктурировать
Нельзя деструктурировать

Разница в том, что Tuple<> — это класс из .NET Framework 4.0, а (int, string) — это value tuple, появившийся в C# 7.0. Используй всегда value tuple — он быстрее, удобнее и современнее.

C# · Создание кортежей
// 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 см
💡 Одной переменной кортежа можно хранить до 8 значений. Если нужно больше — можно вкладывать кортежи друг в друга, но лучше создать класс или record.
🏷️

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

Обращаться к элементам через .Item1, .Item2 — неудобно и нечитаемо. C# позволяет давать элементам осмысленные имена:

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
💡 Имена элементов существуют только на этапе компиляции. В runtime их нет — это синтаксический сахар. Поэтому имена не сохраняются при передаче между сборками (например, через API).
📤

Деконструкция кортежа (распаковка)

Деконструкция — это обратный процесс: мы «разбираем» кортеж на отдельные переменные. Это гораздо удобнее, чем обращаться к .Item1 каждый раз:

C# · Деконструкция кортежа
(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)

Символ _ (подчёркивание) — специальная переменная-пустышка. Она говорит компилятору: «мне не нужно это значение, не создавай для него переменную».

C# · 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 через кортеж — обмен без временной переменной

Классическая задача: поменять значения двух переменных местами. Раньше для этого нужна была третья временная переменная. С кортежами — одной строкой:

C# · 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, Анна (строки)
💡 Swap через кортеж работает потому, что в C# сначала вычисляется правая часть (y, x) — создаётся временный кортеж. Затем он деструктурируется в левую часть (x, y). Временная переменная не нужна — всё делает компилятор.
🔧

Метод Deconstruct в своих классах

Деконструкцию можно сделать для любого своего класса, если реализовать метод Deconstruct. Тогда объект твоего класса можно будет «распаковывать» так же, как кортеж:

C# · 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 с разным количеством параметров:

C# · Несколько 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}");
🧩

Практический пример: работа с кортежами

Давай объединим всё, что узнали, в реальном примере — программа работы с оценками студентов:

C# · Пример с кортежами
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 анонимный тип

Когда использовать кортеж, а когда полноценный класс? Вот сравнение:

Критерий
Кортеж
Класс
Количество полей
2-5 значений
Любое
Семантика
«Просто набор значений»
«Сущность с поведением»
Методы
Нет
Могут быть
Равенство
По значениям (struct)
По ссылке (class)
Имена полей
Только в compile-time
Всегда явные

✅ Золотое правило: используй кортеж для временной группировки значений внутри метода или возврата нескольких значений из метода. Если данные имеют смысл и за пределами одного метода — создай класс или 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-параметров определяет, как кортеж распаковывается.

🧩

Задание для тренировки

Напиши программу «Книжная полка», которая:

  1. Создаёт список кортежей (string Title, string Author, int Year, double Price) — 5 книг
  2. Реализует метод, который возвращает кортеж с количеством книг, средней ценой и самой старой книгой
  3. Выводит все книги, отсортированные по году
  4. Меняет местами самую дорогую и самую дешёвую книгу через swap
  5. Выводит результат на экран
C# · Решение
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: самая дорогая стала на месте самой дешёвой");
    }
}
📌

Что важно запомнить

1️⃣
Value tuple (int, string) — это struct, современный способ группировки значений. Всегда используй его вместо старого Tuple<>.
2️⃣
Именованные элементы делают код читаемым: (int X, int Y) point. Обращайся через point.X вместо point.Item1.
3️⃣
Деконструкция var (x, y) = GetPoint() — распаковывает кортеж в отдельные переменные. Используй _ для игнорирования ненужных элементов.
4️⃣
Swap через кортеж: (a, b) = (b, a) — меняет значения местами без временной переменной.
5️⃣
Метод Deconstruct в своём классе позволяет деструктурировать объект так же, как кортеж. Количество out-параметров определяет количество элементов распаковки.

Тест: 7.1: Распаковка кортежей и деконструкция

3 вопроса

Кортежи

Premium