Перейти к содержимому

Tuple c что это

  • автор:

Типы кортежей (справочник по C#)

Функция кортежей предоставляет краткий синтаксис для группировки нескольких элементов данных в упрощенной структуре данных. В следующем примере показано, как можно объявить переменную кортежа, инициализировать ее и получить доступ к ее элементам данных.

(double, int) t1 = (4.5, 3); Console.WriteLine($"Tuple with elements and ."); // Output: // Tuple with elements 4.5 and 3. (double Sum, int Count) t2 = (4.5, 3); Console.WriteLine($"Sum of elements is ."); // Output: // Sum of 3 elements is 4.5. 

Как показано в предыдущем примере, для определения типа кортежа необходимо указать типы всех его элементов данных и, при необходимости, имена полей. Невозможно определить методы в типе кортежа, но можно использовать методы, предоставляемые .NET, как показано в следующем примере:

(double, int) t = (4.5, 3); Console.WriteLine(t.ToString()); Console.WriteLine($"Hash code of is ."); // Output: // (4.5, 3) // Hash code of (4.5, 3) is 718460086. 

Типы кортежей поддерживают операторы == равенства и != . Дополнительные сведения см. в разделе Равенство кортежей.

Типы кортежей являются типами значений, а элементы кортежа — общедоступными полями. Поэтому кортежи представляют собой изменяемые типы значений.

Можно определить кортежи со сколь угодно большим числом элементов.

var t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26); Console.WriteLine(t.Item26); // output: 26 

Варианты использования кортежей

Чаще всего кортежи используются как возвращаемый методом тип. То есть вместо определения параметров метода out можно сгруппировать результаты метода в возвращаемый тип кортежа, как показано в следующем примере.

int[] xs = [4, 7, 9]; var limits = FindMinMax(xs); Console.WriteLine($"Limits of [] are and "); // Output: // Limits of [4 7 9] are 4 and 9 int[] ys = [-9, 0, 67, 100]; var (minimum, maximum) = FindMinMax(ys); Console.WriteLine($"Limits of [] are and "); // Output: // Limits of [-9 0 67 100] are -9 and 100 (int min, int max) FindMinMax(int[] input) < if (input is null || input.Length == 0) < throw new ArgumentException("Cannot find minimum and maximum of a null or empty array."); >// Initialize min to MaxValue so every value in the input // is less than this initial value. var min = int.MaxValue; // Initialize max to MinValue so every value in the input // is greater than this initial value. var max = int.MinValue; foreach (var i in input) < if (i < min) < min = i; >if (i > max) < max = i; >> return (min, max); > 

Как показано в предыдущем примере, с возвращаемым экземпляром кортежа можно работать напрямую или деконструировать его в отдельные переменные.

Типы кортежей можно также использовать вместо анонимных типов, например в запросах LINQ. Дополнительные сведения см. в статье Выбор между анонимными типами и кортежами.

Как правило, кортежи используются для группирования слабо связанных элементов данных. В общедоступных API рекомендуется определить класс или тип структуры .

Имена полей кортежей

Вы явно указываете имена полей кортежей в выражении инициализации кортежа или в определении типа кортежа, как показано в следующем примере:

var t = (Sum: 4.5, Count: 3); Console.WriteLine($"Sum of elements is ."); (double Sum, int Count) d = (4.5, 3); Console.WriteLine($"Sum of elements is ."); 

Если имя поля не указано, оно может быть выведено из имени соответствующей переменной в выражении инициализации кортежа, как показано в следующем примере:

var sum = 4.5; var count = 3; var t = (sum, count); Console.WriteLine($"Sum of elements is ."); 

Это называется инициализаторами проекции кортежей. Имя переменной не проецируется на имя поля кортежа в следующих случаях:

  • Имя кандидата — это имя элемента типа кортежа, например Item3 , ToString или Rest .
  • Имя кандидата является дубликатом другого имени поля кортежа, явного или неявного.

В предыдущих случаях можно явно указать имя поля или получить доступ к полю по умолчанию.

По умолчанию поля кортежа имеют имена Item1 , Item2 , Item3 и т. д. Всегда можно использовать имя поля по умолчанию, даже если имя поля указано явно или является выводимым, как показано в следующем примере.

var a = 1; var t = (a, b: 2, 3); Console.WriteLine($"The 1st element is (same as )."); Console.WriteLine($"The 2nd element is (same as )."); Console.WriteLine($"The 3rd element is ."); // Output: // The 1st element is 1 (same as 1). // The 2nd element is 2 (same as 2). // The 3rd element is 3. 

Во время компиляции компилятор заменяет имена полей не по умолчанию соответствующими именами по умолчанию. В результате явно указанные или выводимые имена полей будут недоступны во время выполнения.

Включите правило стиля кода .NET IDE0037 , чтобы задать предпочтения для выводимых или явных имен полей кортежей.

Начиная с C# 12, можно указать псевдоним для типа кортежа с директивой using . В следующем примере добавляется global using псевдоним для типа кортежа с двумя целыми значениями допустимого Min и Max значения:

global using BandPass = (int Min, int Max); 

После объявления псевдонима можно использовать BandPass имя в качестве псевдонима для этого типа кортежа:

BandPass bracket = (40, 100); Console.WriteLine($"The bandpass filter is to "); 

Псевдоним не вводит новый тип, но создает синоним только для существующего типа. Вы можете деконструировать кортеж, объявленный псевдонимом BandPass так же, как и с его базовым типом кортежа:

(int a , int b) = bracket; Console.WriteLine($"The bracket is to "); 

Как и при назначении кортежа или деконструкции, имена элементов кортежа не должны соответствовать; Типы выполняются.

Аналогичным образом можно использовать второй псевдоним с одинаковыми типами arity и членов взаимозаменяемо с исходным псевдонимом. Можно объявить второй псевдоним:

using Range = (int Minimum, int Maximum); 

Кортеж можно назначить Range BandPass кортеже. Как и во всех назначениях кортежей, имена полей не должны совпадать, только типы и arity.

Range r = bracket; Console.WriteLine($"The range is to "); 

Псевдоним для типа кортежа предоставляет более семантические сведения при использовании кортежей. Он не вводит новый тип. Чтобы обеспечить безопасность типов, вместо этого следует объявить позиционный record элемент.

Присваивание и деконструкция кортежей

В C# поддерживается присваивание между типами кортежей, которые соответствуют обоим следующим условиям:

  • оба типа кортежей должны содержать одинаковое количество элементов;
  • для каждой позиции кортежа тип правого элемента кортежа аналогичен типу соответствующего левого элемента кортежа или может быть неявно преобразован в этот тип.

Значения элементов кортежа присваиваются в порядке расположения элементов кортежа. Имена полей кортежа не учитываются и не присваиваются, как показано в следующем примере.

(int, double) t1 = (17, 3.14); (double First, double Second) t2 = (0.0, 1.0); t2 = t1; Console.WriteLine($": and "); // Output: // t2: 17 and 3.14 (double A, double B) t3 = (2.0, 3.0); t3 = t2; Console.WriteLine($": and "); // Output: // t3: 17 and 3.14 

Оператор присваивания = можно также использовать для деконструкции экземпляра кортежа в отдельные переменные. Это можно сделать различными способами:

    Вы можете использовать ключевое слово var за пределами круглых скобок, чтобы объявить неявно типизированные переменные и позволить компилятору вывести их типы.

var t = ("post office", 3.6); var (destination, distance) = t; Console.WriteLine($"Distance to is kilometers."); // Output: // Distance to post office is 3.6 kilometers. 
var t = ("post office", 3.6); (string destination, double distance) = t; Console.WriteLine($"Distance to is kilometers."); // Output: // Distance to post office is 3.6 kilometers. 
var t = ("post office", 3.6); (var destination, double distance) = t; Console.WriteLine($"Distance to is kilometers."); // Output: // Distance to post office is 3.6 kilometers. 
var destination = string.Empty; var distance = 0.0; var t = ("post office", 3.6); (destination, distance) = t; Console.WriteLine($"Distance to is kilometers."); // Output: // Distance to post office is 3.6 kilometers. 

Назначение деконструкционного выражения может включать как существующие переменные, так и переменные, объявленные в объявлении деконструкции.

Вы также можете объединить деконструкцию с сопоставлением шаблонов, чтобы проверить характеристики полей в кортеже. В следующем примере выполняется цикл по нескольким целым числам и выводится число, которое делится на 3. Он деконструирует результат Int32.DivRem кортежа и соответствует Remainder 0:

for (int i = 4; i < 20; i++) < if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 )) < Console.WriteLine($"is divisible by 3, with quotient "); > > 

Подробнее о деконструкции кортежей с помощью и других типов см. в статье Деконструкция кортежей и других типов.

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

Типы кортежей == поддерживают операторы и != операторы. Эти операторы сравнивают элементы левого операнда с соответствующими элементами правого операнда в соответствии с порядком расположения элементов кортежа.

(int a, byte b) left = (5, 10); (long a, int b) right = (5, 10); Console.WriteLine(left == right); // output: True Console.WriteLine(left != right); // output: False var t1 = (A: 5, B: 10); var t2 = (B: 5, A: 10); Console.WriteLine(t1 == t2); // output: True Console.WriteLine(t1 != t2); // output: False 

Как показано в предыдущем примере, в операциях == и != не учитываются имена полей кортежей.

Два кортежа сравнимы, если выполнены оба следующих условия:

  • оба кортежа содержат одинаковое количество элементов. Например, t1 != t2 не компилируется, если t1 и t2 имеют разное количество элементов.
  • Для каждой позиции кортежа соответствующие элементы из левого и правого операндов кортежа сравниваются с помощью операторов == и != . Например, не компилируется, (1, (2, 3)) == ((1, 2), 3) так как 1 не сравнивается с (1, 2) .

Операторы == и != сравнивают кортежи с сокращенной обработкой. Это значит, что операция останавливается, как только она соответствует паре неравных элементов или достигает конца кортежей. Однако перед любым сравнением все элементы кортежа вычисляются, как показано в следующем примере.

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4))); int Display(int s) < Console.WriteLine(s); return s; >// Output: // 1 // 2 // 3 // 4 // False 

Кортежи как параметры вывода

Как правило, вы выполняете рефакторинг метода, имеющего параметры out , в метод, возвращающий кортеж. Однако бывают случаи, когда параметр out может иметь тип кортежа. В следующем примере показано, как работать с кортежами в виде параметров out .

var limitsLookup = new Dictionary() < [2] = (4, 10), [4] = (10, 20), [6] = (0, 23) >; if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits)) < Console.WriteLine($"Found limits: min is , max is "); > // Output: // Found limits: min is 10, max is 20 

Кортежи и System.Tuple

Кортежи C# с типами System.ValueTuple, отличаются от кортежей, представленных типами System.Tuple. Основные различия заключаются в следующем.

  • Типы System.ValueTuple являются типами значений. Типы System.Tuple являются ссылочными типами.
  • Типы System.ValueTuple являются изменяемыми. Типы System.Tuple являются неизменяемыми.
  • Элементами данных типов System.ValueTuple являются поля. Элементами данных типов System.Tuple являются свойства.

Спецификация языка C#

Дополнительные сведения см. в разделе:

  • Типы кортежей
  • Операторы равенства кортежей

См. также

  • справочник по C#
  • Типы значений
  • Выбор между анонимными типами и кортежами
  • System.ValueTuple

Совместная работа с нами на GitHub

Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

Шаблоны C++: как итерировать по std::tuple — основы

Для большинства стандартных контейнеров перебор элементов можно осуществлять просто с помощью цикла for с диапазоном прямо во время выполнения. Но что насчет кортежей ( std::tuple )? В этом случае мы не можем использовать обычный цикл, так как он не “понимает” список аргументов кортежа во время компиляции. В этой статье я покажу вам несколько приемов, которые вы можете использовать для итерации по элементам кортежа.

В качестве примера будем использовать следующий кортеж:

std::tuple tp < 10, 20, 3.14, 42, "hello">; printTuple(tp);

Мы бы хотели, чтобы в консоль выводилось следующее:

(10, 20, 3.14, 42, hello)

Как же нам реализовать ​​функцию printTuple?

Это первая часть, где мы обсуждаем только основы. Во второй части мы поговорим об обобщениях, std::apply и многом другом.

Основы

std::tuple (кортеж) — это коллекция фиксированного размера, содержащая разнородные значения.

Для сравнения, его младший брат — std::pair (пара) — принимает два шаблонных параметра, .

std::pair intDouble < 10, 42.42 >; // или with CTAD, C++17: std::pair deducedIntDouble < 10, 42.42 >; // выведение типов!

std::tuple принимает переменное количество аргументов. Так что это обобщение std::pair , поскольку оно может принимать любое количество аргументов/значений.

std::tuple tup ; // или with CTAD, C++17: std::tuple deducedTup ; // выведение типов

Если вам нужно получить доступ к элементу пары, вы можете просто запросить .first или .second элемент:

std::pair intDouble < 10, 42.42 >; intDouble.first *= 10;

Однако поскольку у кортежа размер может быть разный, у него нет ни .first , ни .second , ни .third . вы можете получить доступ к его элементам только через std::get :

#include #include int main() < std::tuple tp ; // по индексу: std::get(tp) *= 100; std::cout << std::get(tp) (tp) (tp)
Так как по нему итерировать?

Ну что ж, теперь, когда мы разобрались с основами, мы можем попытаться написать код, который будет выполнять перебор всех элементов кортежа.

Как видите, значения/типы устанавливаются во время компиляции. Это отличает кортеж от обычного контейнера, такого как std::vector , куда мы обычно помещаем значения во время выполнения.

Чтобы итерировать по кортежу, нам нужно преобразовать этот “желанный” код:

// как бы мы хотели: std::tuple tp ; for (auto& elem : tp) std::cout 
std::tuple tp ; std::cout (tp) (tp) (tp) 

Другими словами, нам нужно разложить кортеж в ряд вызовов std::get для доступа к элементу по его позиции (id). Позже мы можем передать полученные таким образом элементы в std::cout или любой другой вызываемый объект (для их обработки).

К сожалению, язык не поддерживает такие циклы времени компиляции… по крайней мере пока (дополнительную информацию по этому вопросу вы можете посмотреть внизу).

Чтобы добиться подобного эффекта, нам нужно прибегнуть к шаблонной магии.

Базовая структура

Сначала мы можем попробовать задействовать шаблон функции, который принимает список индексов, которые мы хотим вывести:

template void printElem(const T& x) < std::cout ; template void printTupleManual(const TupleT& tp) < (printElem(std::get(tp)), . ); >

И тогда наша искомая функция примет следующую форму:

std::tuple tp < 10, 20, "hello">; printTupleManual(tp);

Что здесь происходит?

Помимо типа самого кортежа наша функция printTupleManual принимает нетиповые шаблонные аргументы — variadic список из size_t значений.

Для определения типа tp этом вызове я использовал decltype . В качестве альтернативы мы могли бы вызвать ее следующим образом:

std::tuple tp < 10, 20, "hello">; printTupleManual, 0, 1, 2>(tp);

Как видите, decltype экономит нам много времени. Подробнее об этом смотрите на Cppreference - decltype.

Внутри самой функции мы используем свертку параметров шаблона (доступную начиная с C++17), чтобы расширить этот variadic набор параметров с помощью оператора запятой.

Другими словами, наша функция тождественна следующей форме:

void printTupleManual, 0, 1, 2> (const std::tuple & tp) < printElem(get(tp)), (printElem(get(tp)), printElem(get(tp))); >

Мы можем проиллюстрировать это “расширение” с помощью CppInsights — смотрите демо по этой ссылке.

Благодаря сверткам (fold expressions) мы стали на шаг ближе к искомой нами форме цикла времени компиляции!

Поприветствуйте index_sequence

В предыдущем примере нам нужно было передавать список индексов вручную. Создание такого списка аргументов не вяжется с масштабируемостью и подвержено ошибкам. Можем ли мы автоматически вывести этот список на основе размера кортежа?

Все, что нам сейчас нужно, так это сгенерировать следующие индексы:

// для кортежа размера N сгенерировать список 0, 1, . N-1

Эта проблема очень распространена в программировании с шаблонами, и, начиная с C++14, мы можем использовать index_sequence . Это вспомогательный шаблонный класс, который содержит индексы в виде нетиповых параметров шаблона:

template < class T, T. Ints >class integer_sequence; template using index_sequence = std::integer_sequence;

Стандартная библиотека C++ определяет std::integer_sequence , а std::index_sequence — это просто integer_sequence по size_t . Смотрите @cppreference.com.

Таким образом мы можем преобразовать наш код в такую форму:

template void printElem(const T& x) < std::cout ; template void printTupleManual(const TupleT& tp, std::index_sequence) < (printElem(std::get(tp)), . ); >

И вызывать его следующим образом:

std::tuple tp < 10, 20, "hello">; printTupleManual(tp, std::index_sequence<>);

Мы также можем воспользоваться вспомогательной функцией make_index_sequence :

printTupleManual(tp, std::make_index_sequence<>);

И заключительная часть — получаем размер кортежа:

printTupleManual(tp, std::make_index_sequence::value><>);

Также у нас есть вспомогательной шаблон переменной — tuple_size_v , который может сделать нашу строку немного короче:

printTupleManual(tp, std::make_index_sequence><>);

Объединим все это в нашей функции:

template void printElem(const T& x) < std::cout ; template void printTupleManual(const TupleT& tp, std::index_sequence) < (printElem(std::get(tp)), . ); > template > void printTupleGetSize(const TupleT& tp) < printTupleManual(tp, std::make_index_sequence<>); >

Теперь ее вызов выглядит очень лаконично:

std::tuple tp < 10, 20, "hello">; printTupleGetSize(tp);

Вы также можете посмотреть полностью “развернутый” код в C++ Insights, перейдя по этой ссылке.

Например, наш вызов разворачивается в:

void printTupleGetSize, 3> (const std::tuple & tp) < printTupleManual(tp, std::integer_sequence<>); >

Как видите, make_index_sequence был элегентно развернут в std::integer_sequence<> .

Вывод std::tuple

Теперь, когда нас есть все необходимые функции для итерации по кортежу, мы можем попробовать заключить их в окончательную функцию вывода в консоль.

template void printTupleImp(const TupleT& tp, std::index_sequence) < size_t index = 0; auto printElem = [&index](const auto& x) < if (index++ >0) std::cout ; std::cout (tp)), . ); std::cout template > void printTuple(const TupleT& tp) < printTupleImp(tp, std::make_index_sequence<>); >

Как видите, внутри printTupleImp я преобразовал printElem в лямбду. Этот шаг является вспомогательным, так как он позволяет мне легко перенести некоторые дополнительные состояния в функцию вывода в консоль. Мне нужно проверить, должен ли я ставить разделитель или нет.

Теперь мы можем запустить ее:

std::tuple tp < 10, 20, "hello">; printTuple(tp);

Оператор

Все, что нам нужно сделать, это задействовать нашу вспомогательную функцию и передать дополнительный объект ostream :

#include #include #include template std::ostream& printTupleImp(std::ostream& os, const TupleT& tp, std::index_sequence) < size_t index = 0; auto printElem = [&index, &os](const auto& x) < if (index++ >0) os ; os (tp)), . ); os template ::value> std::ostream& operator <<(std::ostream& os, const TupleT& tp) < return printTupleImp(os, tp, std::make_index_sequence<>); > int main() < std::tuple tp < 10, 20, "hello">; std::cout

Добавление индексов

Поскольку у нас уже есть список индексов, почему бы нам не использовать его?

#include #include #include template std::ostream& printTupleImp(std::ostream& os, const TupleT& tp, std::index_sequence) < auto printElem = [&os](const auto& x, size_t id) < if (id >0) os ; os (tp), Is), . ); os template ::value> std::ostream& operator <<(std::ostream& os, const TupleT& tp) < return printTupleImp(os, tp, std::make_index_sequence<>); > int main() < std::tuple tp < 10, 20, "hello">; std::cout

Теперь вместо отдельной переменной index я просто передаю текущий индекс элемента из сверточного выражения. Мы также можем использовать его в выводе в качестве префикса для каждого элемента.

Тогда мы получим:

(0: 10, 1: 20, 2: hello)

Если вы хотите посмотреть на аналогичный код, который работает с std::format , вы можете почитать мою статью: Как форматировать пары и кортежи с помощью std::format (~1450 слов), которая доступна для патронов C++ Stories Premium на патреоне. Оценить все преимущества Premium патронажа можно здесь.

Заключение и следующая часть

Это был интересный эксперимент!

За несколько шагов этого руководства мы прошли путь от основ кортежей к итерации по списку индексов, а затем и с std::index_sequence . Благодаря сверточным выражениям, доступным с C++17, мы можем расширить наш список аргументов времени компиляции и реализовать поверх него функцию.

Мы сосредоточились на функции вывода, так как она относительно проста для понимания и более-менее полезна. В следующий раз я постараюсь сделать нашу функцию для итерации более общей, чтобы можно было выполнять преобразования значений. Мы также рассмотрим удобную функцию std::apply , которая даст нам дополнительные возможности.

Возвращаясь к вам

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

Обязательно делитесь вашими мыслями в комментариях под статьей.

for времени компиляции

Я упомянул, что C++ не предлагает нам цикла времени компиляции, но есть одно предложение P1306 — “Expansion statements”. В настоящее время оно находится в рассмотрении, но вряд ли появится в C++23.

Это позволит нам писать что-то вроде:

auto tup = std::make_tuple(0, ‘a’, 3.14); for. (auto elem : tup) std::cout 

Посмотреть статус этого предложения можно на @Github/cpp/papers.

Примечания

  • Effective Modern C++ by Scott Meyers.
  • C++ Templates: The Complete Guide (2nd Edition) by David Vandevoorde, Nicolai M. Josuttis, Douglas Gregor.

Статьи и ссылки:

  • C++ Fold Expressions 101 - Fluent C++
  • Mundane std::tuple tricks: Getting started - The Old New Thing
  • std::index_sequence and its Improvement in C++20 - Fluent C++
  • c++ - Is there a tuple for_each() that returns a tuple of all values returned from the functions invoked? - Stack Overflow

Сегодня в OTUS пройдет demo-урок по основам C++, на котором обсудим:

  • Что есть суть «современный C++»?
  • Какие есть отличительные черты у этого языка?
  • В чем его сильные и слабые стороны?

Приглашаем всех заинтересованных начинающих, регистрация по ссылке.

  • c++
  • шаблоны c++
  • основы програмирования

Подробное знакомство с кортежами в C#

Я отчетливо помню времена до кортежей: множество проблем с поиском оптимального способа возвращения из метода нескольких значений. Как только в C# добавили этот функционал, я сразу увидел множество случаев, в которых они могут быть полезны.

Затем постепенно, с каждой новой версией языка, кортежи дорабатывали. Если вы не пользовались ими с тех времен, когда они только появились, то сегодня вполне справедливо можете их не узнать. Изменились кортежи основательно, но к лучшему.

В этой статье мы вспомним, как начиналась история кортежей, и познакомимся с вариантами их использования в последних версиях языка.

Появление кортежей в C#4

В 2010 году вышла новая версия .NET и C#, в которой и появились первые кортежи.

Задача этого нового принципа (нового для C#, конечно) состояла в том, чтобы упростить работу с небольшим числом значений. До этого для сохранения этих значений приходилось либо создавать собственный класс/структуру, либо использовать набор параметров ref — что в обоих случаях было не идеальным решением.

Итак, мы получили новый класс, вполне ожидаемо названный Tuple , который могли использовать для хранения нескольких элементов данных в одной структуре.

private Tuple GetData()
return new Tuple(true, 47);
>

Вроде все просто. Используемые внутри кортежа типы данных можно определять с помощью обобщенных типов, создавая кортеж для их передачи в виде единой структуры.

Затем значения из кортежа можно извлекать:

Tuple tuple = new Tuple(true, 47);

bool boolValue = tuple.Item1; // равно 'true'
int intValue = tuple.Item2; // равно '47'

Ничего сложного, но если вы мыслите как я, то свойства Item1 и Item2 оказываются как бы лишенными смысла. Если брать их отдельно, то не получится понять, что именно их значения представляют.

Так что хоть начальная проблема и была решена, разработчикам еще было над чем потрудиться.

Улучшенные кортежи в C# 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 при попытке извлечь из этой структуры данные:

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

Здесь у нас два варианта:

  • использовать вместо 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;

Он может выглядеть немного странно, но возможность определять переменные напрямую из кортежа очень удобна. Остальной части кода неважно, что они взяты из кортежа, и вам не нужно определять или инстанцировать другие локальные переменные самостоятельно.

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

А как быть, если некоторые возвращаемые кортежем элементы вас не интересуют? Обязательно ли придется сталкиваться с этими лишними переменными, которые вы все равно игнорируете? Нет!

Для любого элемента можно использовать отмену (discard), и соответствующий фрагмент кода весьма прост:

(bool isSuccessful, _) = GetData();

Вместо определения int totaltime мы просто используем нижнее подчеркивание. Таким образом мы сообщаем компилятору, что не будем использовать этот элемент, и он не будет загромождать им оставшуюся часть кода.

Что можно хранить в кортеже?

Хранить в кортеже можно практически все. Если сильно захочется, то можно даже создавать кортежи функций. Вот один из способов:

private (Func function1, Func function2) GetFunctionTuples()
return (() => true, () => false);
>

Сколько элементов вмещает кортеж?

Если использовать старинный класс Tuple , то он ограничен восемью элементами для одного кортежа. Если же вам нужно больше, то последний элемент можно сделать кортежем. Смысл в том, что так можно умещать необходимое количество данных вложенным образом.

Однако при использовании современного синтаксиса таких ограничений нет. В этом случае кортеж вместит все, что вы в нем пропишете.

Интересно отметить, что метод .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);
>

При выполнении тест проваливается, и мы получает такой ответ:

Очевидно, что имена элементов здесь отсутствуют — указаны лишь значения в том порядке, в каком они прописаны в кортеже.

При сравнении кортежей нужно учитывать и многие другие моменты, о которых лучше всего прочесть в документации Microsoft.

Выводы

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

Элементы кортежа можно без проблем именовать, чтобы всегда быть в курсе, к каким данным они относятся. При этом с помощью несложного синтаксиса можно легко извлекать эти элементы с целью последующего использования в коде.

Кортежи также можно сравнивать, но тут важно помнить, что в сравнении учитывается порядок их элементов, а не имена.

  • Создание расширяющих методов на C#
  • 5 простейших приемов работы на C#
  • Новейшие возможности C# 9

Читайте нас в Telegram, VK и Дзен

Массивы и кортежи

В C++ есть несколько способов объединить группу переменных фиксированного размера в одну переменную.

#Массивы в C

В языке C есть три основных способа определить массив:

Получившиеся переменные функционально идентичные, но немного отличаются:
  • Определенный глобально массив a будет лежать в заранее выделенной области памяти на протяжении всего времени исполнения программы. Все элементы изначально заполнены своим значением по умолчанию (для int , нулём).
  • Определенный внутри функции массив b будет лежать на стеке — специальной области памяти для временных переменных — и будет удален сразу когда функция (или любой другой блок вроде тела цикла или if -а) завершится. Так как размер стека исполнения ограничен, большие массивы ($>10^6$) выделять так нельзя. Изначально он заполнен чем-то случайным, что лежало на тот момент в памяти — чтобы заполнить нулями, можно написать int x[100] = <> . Чтобы заполнить все элементы заданными значениями, можно написать int y[5] = .
  • Определенный через оператор new массив c выделен динамически. Он существует, пока его специально не удалили через оператор del[] . Он также заполнен тем, что на тот момент лежало в памяти. В отличие от предыдущих двух вариантов, он может быть любого размера, даже неизвестного заранее.

Важно. В первых двух вариантах размер массива должен быть известной на момент компиляции константой. Компилятор GCC может скомпилировать выражение вида int a[n] , и действительно выделится массив не-константного размера; IDE поэтому может и не подчеркнуть его, хотя это не является частью стандарта.

Все элементы массива хранятся последовательно в памяти, а сами переменные a , b и c на самом деле являются указателями на первый элемент массива. Скобочки — это просто «синтаксический сахар»:

Для инициализации и копирования в C есть две полезные функции, memset и memcpy соответственно.

Первая берет указатель «куда», указатель «откуда» и количество байт, которые нужно перекопировать:

Вторая берет указатель «куда» и один байт — значение, которое нужно раскопировать по всему массиву.
Важно. memset работает именно с сырыми байтами, а не типами вроде int или float . Поэтому через memset массивы целочисленных типов можно заполнять только «периодичными» значениями, вроде $0$ и $-1$ (отрицательная единица в двоичной записи выглядит как 111..111 ).

Также важно помнить, что последний аргумент в обоих функциях — это число байтов, а не количество элементов. В случае с массивами не-константного размера можно домножить размер типа на размер массива:

Здесь sizeof(int) = 4 . Вместо просто четверки так пишут для самокомментируемости.

#std::array

В C++11 добавили свой класс для массивов константного размера:

; Все операции с ним работают по аналогии с сишными. Основное отличие — он является контейнером STL, то есть у него есть итераторы и с ним работают все алгоритмы из стандартной библиотеки.
С обычными массивами, впрочем, тоже — указатели автоматически приводятся к итераторам:
Массивы из STL, как и обычные массивы константного размера, поддерживают итерирование:
Также можно изменять элементы в массиве во время итерирования следующим синтаксисом:
В STL также есть более ошибкоустойчивая альтернатива memset — std::fill :
Она уже работает с полными типами, хотя и немного медленнее.

#std::pair и std::tuple

Тип pair хранит пару из переменных не обязательно одинаковых типов:

Первый элемент доступен через поле .first , а второй через .second .

Его обобщение, tuple , хранит кортеж из произвольного количества переменных:

Вместо .first , .second , .third и так далее с tuple нужно использовать индексы.

Пары и тюплы удобно возвращать из функций:

Также по массивам из них удобно итерироваться:

#struct

Очень рекомендуется по возможности вместо пар и тюплов объявлять структуры.

Вместо возни с .first и .second или индексами вы получаете именованные поля, а также возможность определять свои методы и перегружать операторы:
Единственный минус структур в том, что по для пар и тюплов будут определены функции сравнения и хеширования, и поэтому их можно сразу в таком виде класть в качестве ключа в структуры из STL вроде set или unordered_set , а для структур их нужно писать отдельно.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *