Кортежи (Tuples)
Когда часто используешь множественный возврат значений из методов, обычно применяешь ключевое слово out в параметрах метода или определение различных дополнительных структур и классов. Обычно они находятся прямо над определением метода или, в случае переменных, их нужно проинициализировать где-то перед вызовом. Это не очень удобно. Итак, какое предполагается улучшение, рассмотрим на примере следующего кода:
public (int sum, int count) Tally(IEnumerable<int> values) { ... }
var t = Tally(myValues);
Console.WriteLine($"Sum: {t.sum}, count: {t.count}");
Здесь мы видим анонимное определение структуры с public полями. Т.е. мы получаем несложный и вполне удобный способ использования возврата множественных значений. Что будет происходить внутри, пока не очень понятно. Вот пример работы с ключевым свойством async:
public async Task<(int sum, int count)> TallyAsync(IEnumerable<int> values) { ... }
var t = await TallyAsync(myValues);
Console.WriteLine($"Sum: {t.sum}, count: {t.count}");
С этим новым синтаксисом появляется много интересных возможностей создания анонимных типов:
var t = new (int sum, int count) { sum = 0, count = 0 };
Данный синтаксис кажется слишком избыточным. Зато создание объекта структуры с помощью литерала кажется очень даже удобным:
public (int sum, int count) Tally(IEnumerable<int> values)
{
var s = 0; var c = 0;
foreach (var value in values) { s += value; c++; }
return (s, c); // создание объекта анонимной структуры
}
Есть еще один метод создания объекта анонимной структуры с помощью литерала:
public (int sum, int count) Tally(IEnumerable<int> values)
{
var res = (sum: 0, count: 0); // Заполнение данных прямо во время создания анонимной структуры
foreach (var value in values) { res.sum += value; res.count++; }
return res;
}
Этот пример очень сильно напомнил создание JSON. И пока не совсем понятно, можно ли будет написать что-то подобное:
var res = (sum: 0, count: 0, option :( sum: 0, count: 0));
Но, как мне кажется, самая «вкусность» — это создание анонимных структур в коллекциях.
var list = List<(string name, int age)>();
list.Add("John Doe", 66); // Такое добавление данных в лист не может не радовать
Радует сегодняшняя открытость разработки языка: каждый может повлиять на разработку и предложить свои идеи. Более подробно о кортежах здесь.
Update: Спасибо, пользователю ApeCoder. Указал отсуствие в статье механизма инциализации переменных с помощью кортежей.
Вот пример:
public (int sum, int count) Tally(IEnumerable<int> values)
{
var res = (sum: 0, count: 0); // infer tuple type from names and values
foreach (var value in values) { res.sum += value; res.count++; }
return res;
}
(var sum, var count) = Tally(myValues); // инициализация перемнных
Console.WriteLine($"Sum: {sum}, count: {count}");
Комментарии (47)
eocron
29.04.2015 01:13Единственное место, где я вижу для них перспективу в большинстве проектов — табличные списки разного формата и DataBaseStyle. Остальное просто убьет «сладостью» весь проект, ничего же не понятно будет.
Задачи с применением такого рода технологии есть в математических фреймворках и цель у нее там несколько иная, нежели удобство, а именно — объединение задач в одну и там не важна читабельность (математика жеж). Например, вычисление одновременно и мат ожидания, и дисперсии, чтобы два раза цикл не гнать, но это такой слабенький пример, есть и покруче.
nosuchip
29.04.2015 09:42Не увидел — планируется ли разворачивание кортежей в переменные? Вроде такого:
a, b, c = (1,2,3)
BOBS13 Автор
29.04.2015 09:54Нет, такого не будет. Во всяком случаи пока в планах такого я не увидел.
ApeCoder
29.04.2015 10:20+2По ссылке
Tuple deconstruction
Since the grouping represented by tuples is most often «accidental», the consumer of a tuple is likely not to want to even think of the tuple as a «thing». Instead they want to immediately get at the components of it. Just like you don't first bundle up the arguments to a method into an object and then send the bundle off, you wouldn't want to first receive a bundle of values back from a call and then pick out the pieces.
Languages with tuple features typically use a deconstruction syntax to receive and «split out» a tuple in one fell swoop:
(var sum, var count) = Tally(myValues); // deconstruct result Console.WriteLine($"Sum: {sum}, count: {count}");
This way there's no evidence in the code that a tuple ever existed.
KReal
29.04.2015 13:41И кортежи, и паттерн матчинг, и наверняка многое из этого списка есть в F# и используется, как мне кажется, для ограниченного круга задач. Зачем тащить это в C#?
BOBS13 Автор
29.04.2015 17:52С паттерн матчинг понятно, его давно хотят перетинуть с C#, сколько самописных библиотек по этому написано. Картежи достаточно спорная фича, я согласен, хотя ее прменение вполне понятно и может быть удобным в случаи каких то еденичных возвратов. Я бы допустим применил ее при выгрузке некоего не большой объекта на веб страницу с сериализацией в JSON.
impwx
29.04.2015 14:43+1Очень спорная фича по нескольким причинам. Во-первых, почему решили придумать новый синтаксис для практически того же, что уже делают анонимные типы, а не расширить их возможность? Во-вторых, почему именно структура? Например, следующий код не сработает:
var list = new List<(int X, int Y)>(); list.Add((1, 2)); list[0].X = 2; // ошибка: потеряли lvalue
BOBS13 Автор
29.04.2015 17:41+1Я не очень понял, почему ошибка можете поянить?
impwx
29.04.2015 18:41Потому, что индексатор
List<T>
— это присыпанная синтаксическим сахаром функция. Из нее возвращается копия структуры, поэтому лежащее в списке значение не может быть изменено присваиванием. С массивом ситуация обстоит по-другому: там индексатор реализован с помощью отдельной инструкции, позволяющей обратиться непосредственно к объекту по адресу.
SirEdvin
29.04.2015 20:21Я одного не понял. Будут создаваться таки анонимные структуры или классы? Потому что если структуры, фича куда более, чем просто сомнительна.
BOBS13 Автор
29.04.2015 22:21Обоснование у создателей на создание анонимных структур, а не классов, вот такое
Struct or class
As mentioned, I propose to make tuple types structs rather than classes, so that no allocation penalty is associated with them. They should be as lightweight as possible.
Arguably, structs can end up being more costly, because assignment copies a bigger value. So if they are assigned a lot more than they are created, then structs would be a bad choice.
In their very motivation, though, tuples are ephemeral. You would use them when the parts are more important than the whole. So the common pattern would be to construct, return and immediately deconstruct them. In this situation structs are clearly preferable.
Structs also have a number of other benefits, which will become obvious in the following.SirEdvin
29.04.2015 22:29+1То есть у них получилась такая фича, что если не знать ее паттерн, она плавно переходит в баг?
omikad
30.04.2015 07:57Структуры тоже себя так ведут — большие структуры плохие в производительности. Если кортеж разрастется, то очевидная рекомендация — сделать из него именованный класс, что и решит проблему копирования
js605451
Кортежи — это очень спорная фича, особенно для языка со статической типизацией. Если раньше не-очень-хорошим-программистам приходилось напрячься, чтобы вводя новую сущность в коде дать ей внятное название, теперь они вообще напрягаться не будут прикрываясь «использованием языка на всю катушку».
Это как раз хреновый пример, который воспринимается на порядок хуже, чем длинное скучное:
NeoCode
Кортежи — это одна из тех естественных вещей, которая была неочевидна (или недоступна в явном виде) в течение очень долгого времени, и только недавно стала проникать в мейнстрим. В общем, кортеж — это группа имен времени компиляции и не более того. Список аргументов функции — кортеж. Список инициализации в фигурных скобках — это тоже кортеж. Даже список полей и методов класса тоже можно рассматривать как кортеж (хотя и не кортеж объектов, а скорее чистый кортеж имен — это уже к метапрограммированию). В идеале, это способ группировки чего угодно на этапе компиляции — и в общем это более фундаментально чем структура (которая по сути лишь кортеж в обертке типа). И множественный возврат из функции — лишь самое очевидное применение. Как насчет групповых или множественных операций?
Хотя и это мелочи. Думаю, со временем ситуация с кортежами прояснится и раскроется их мощь, а то многие думают что это недоструктура или даже недосписок как в питоне.
js605451
Вы бы лучше пример кода привели, чтобы показать где эта конструкция выигрывает по сравнению с явным типом. Что такое кортеж — вполне понятно. Также понятно какие проблемы будут возникать при его использовании. Что непонятно — какие проблемы он решает.
NeoCode
Тип номинативно-типизирован, кортеж — структурно-типизирован. Это разные вещи с разными областям применения. Если вам нужна функция, принимающая два параметра — вы пишете функцию с двумя аргументами, а не функцию, принимающую тип с двумя полями. Также и здесь. Бывает, когда нужно вернуть из функции объект (как единое целое) — в этом случае объявляется структура; а если нужно вернуть просто два значения, никак не связанных между собой — применяется кортеж и множественный возврат из функции.
js605451
Если возникает необходимость вернуть 2 значения, никак не связанных между собой, стоит пересмотреть ответственности, возложенные на метод. Предложите пример такого метода — интересно чем вы будете руководствоваться придумывая для него имя.
nickolaym
Таких котлет и ноутбуков — вагон и тележка.
Обычай предписывает использовать для этого out-параметры
В С++ есть функции, возвращающие пару — например, итератор и флажок при вставке в set.
Предложите, как пересмотреть ответственности для них.
js605451
out-параметры — это минорная фича, необходимость в которой возникает крайне редко. Я не знаком с историей её возникновения, но предположу, что на 90% мотивация была — байндинги для сишного кода. Очень многие функции WinAPI возвращают «полезный результат» через параметр, а возвращаемое значение используют для кода возврата. Это традиция из культуры C — там нет исключений, поэтому возникает та самая интересная ситуация когда нужно и код возврата сообщить, и полезные данные вернуть. В Java, например, out-параметров вообще нет — проблем с этим не возникает.
У стандартной библиотеки C++ своя уникально-экзотическая философия из которой органично вытекает этот неповторимый извращённый дизайн. Во многих других мейнстримных языках аналогичный метод возвращает bool и сложностей ни у кого не возникает.
nickolaym
Мотивация у out-параметров — чтоб не рожать новые классы на каждый случай возвращения кортежа.
Заодно, удачно ложится на сишные интерфейсы и COM.
В яве своя культура сложилась, не зря её называют королевством существительных. Там, ещё одним классом больше, классом меньше, не жалко.
exvel
Зачем предлагать, когда уже и так есть в самом .NET:
TryParse и его разновидности.
hmspns
А чем Tuple<> не угодил?
ApeCoder
github.com/dotnet/roslyn/issues/347 — см раздел background
brewerof
А что заминусовали js605451?
Он прав, это будет провоцировать сode smells в неумелых ручках.
Danov
С таким аргументом и болгарку придется заменить ножовкой.
AndrewMayorov
Это в любом случае не будет пахнуть хуже, чем метод с пятью out-параметрами.
brewerof
Не будет, но теперь +1 способ сломать первую буковку из SOLID, при чем довольно удобный. Опять оговариваюсь — в кривых ручках.
impwx
Тут больше подходит не кортеж, а
IEnumerable<T>
. В кортеже все типы могут быть разные и абсолютно непонятно, что тогда должен делать такой оператор.ilammy
Добавлять 100 ко всему, что в кортеже, или не компилироваться, если это невозможно.
impwx
Это применение из области эзотерики. Для нормальных случаев есть
data.Select(x => x + 100)
.nsinreal
В отсутствии кортежей есть один недостаток. Вот пишешь ты метод, в нем юзаешь анонимный тип. А потом хочешь сделать extract to method — и все, все плюшки закончились. Нужно вводить новый класс, переопределять операции == (хотя можно делать nullable structures, там вроде это автоматом идет), а это на порядки больше времени/места занимает.
В итоге у вас:
1) Либо один большой говнометод
2) Много непонятных data-классов, которые нужны только в пределах двух-трех методов
3) Либо код будет содержать Tuple/KeyValuePair, что вообще читабельности не добавляет.
И кортежи решают проблему — у вас появляются анонимные типы (статически типизированные и с адекватными именами свойств), которые легко шарятся между тремя методами. Т.е. можно писать короткие методы (держа каждый метод на едином уровне абстракции) и не писать лишнего говнокода. На всякий случай: количества смысла и адекватности на одну строку кода повышается.
Не, можно конечно юзать это и во вред проекту. Но в C# есть более опасная фича в виде dynamic, а её никто не спешит юзать без реальной нужды.
Также вместе с кортежами должны идти pattern matching/destructuring. Это по сути возможность написать foreach ((var id, var name) in dictionary)
js605451
Всё верно — если программист решая задачу сталкивается с выбором из этих 3 пунктов, очевидно он сам не понял что напрограммировал. Есть большая разница между «кодом, который выдаёт нужные результаты» и «кодом, который решает задачу»: в первом случае просто проходят тесты, а во втором — в явном виде описываются намерения: вводятся термины, связи между ними, и уже в контексте этого небольшого «под-домена» описывается решение задачи.
Обратите внимание, ваше описание решаемой проблемы начинается с фразы «Вот пишешь ты метод, в нем юзаешь анонимный тип» — проблема появляется уже здесь, а не в момент «extract method»: вы посчитали, что «вот эта сущность» недостаточно важна, чтобы вводить её в явном виде, и ввели неявно. На следующем шаге вы об этом начинаете горько сожалеть, т.к. выясняется, что именно эта сущность связывает несколько частей решения задачи.
nsinreal
О, мы нашли точку непонимания. Вы считаете, что если сущность шарится между методами, то она важна. Это не так. Возможно у вас не встречалось задач, в которых есть сущность настолько мелкая, что писать на неё лишний тип и придумывать осознанное адекватное имя нецелесообразно. Да, она связывает несколько «частей» решения задачи, и что?
У меня каждая часть решения состоит из двух-четырех строк — у меня в этой задачи главное методы, а не данные. Вы правда мне предлагаете выделять новый тип данных? Завести отдельный файлик под структуру, перечислить требуемые неймспейсы, указать неймспейс сущности, указать что за сущность, указать два свойства сущности, разделив их пробелами по гайдлайнам. У меня есть код в 10 строк, вы предлагаете добавить еще 10 строк и один файл, запутав остальных в проекте самим существованием левой модели?
Я предпологаю, что у вас нету понимания, что некоторые задачи нужно решать, продумывая типы данных (ООП и прочее); а в некоторых задачах нужно писать код (функции). А если я начну городить больше кода, больше типов, больше классов, то он станет нечитабельным (концентрация смысла на строку резко упадет, а охватить все разумом станет трудно). С другой стороны, если я возьму другую задачу, то мне сначала лучше продумать типы данных, потому что в ином случае мой код будет нечитабельной тыквой. Смесь парадигм несет программисту счастье.
js605451
Т.к. сущность шарится между методами, она является частью внутреннего интерфейса. Интерфейс — важен, будь он внутренний или внешний.
Да, я предлагаю не мудрствовать, а просто взять и выделить новый тип данных. Не обязательно так громко, как вы описали: если речь идёт про реализацию одного единственного класса, достаточно сделать внутренний класс. Хотя, вообще говоря, и от лишнего файла Земля не остановится.
Наоборот. В голове намного проще удержать сущность, если у неё есть имя. Удержать в голове «ту штуку, в которой лежат a, b и c» — намного сложнее, т.к. вы не поднимаетесь выше a, b и c — они как были отдельными компонентами, так и остались.
ApeCoder
Мне кажется, это более предназначенно для работы с LINQ и лябдами. Если функции неименованные, то могут быть и типы неименованные.
SirEdvin
Смесь парадигм несет программисту счастье
И большое проблемы другому программисту, который будет в этом коде разбираться.
Если у вас задача, где нужно писать код, то почему бы не написать ее на F# или другом функциональном языке, а потом, если нужно, подключить его к C#?
ApeCoder
Ага вместо того, чтобы понять, что такое кортеж, другому программисту придется учить целый другой язык
nsinreal
Потому что смесь языков не несет счастье. Сесь парадигм несет счастье. Почему так? Потому что код един, в одном формате и не нарушается принцип DRY. В случае когда мы мешаем языки, такого не происходит. Если у нас есть C# (бек) и Javascript (фронт), то происходит дублирование, либо отсутствие на каком-то из слоев какой-то полезной логики, либо изобретаются всякие транскомпиляторы. Если у нас есть C# и F#, то даже в пределах их общий shared код — это просто пиздец. Не надо рассказывать про то, что они на .NET и легко интероптятся. Я пробовал, я знаю что это не пашет с легкого пинка.
А еще под F# нету многих инструментов и фич, которые есть под C#.
nsinreal
Особенно ржачно выглядит когда человек пытается подключить всякие там IronPython в проект на C#
VladVR
Кстати, да, сразу пример жизненный приходит на ум. Нередко бывает, что приходится писать огромный EF запрос, и хочется его разбить на несколько методов, иногда и переиспользовать, то ради того, чтоб передать несколько сгруппированых датабазных сущностей приходится заводить класс.
Т.е не сами сущности, конечно, а IQueryable от их группы.
Говнокодеры в этом случае тупо копипастят этот огромный метод, ради того, чтоб в середину еще один .Where() воткнуть, опять же из личной практики. Кортежи, как мне кажется, на способностях лоу-левел разработчиков никак не сыграют.
AxisPod
Ну как бы тот же LINQ был бы невозможен в том виде что есть без типа dynamic. А в данном случае кортежи упростят местами код и при этом еще помогут соптимизировать конечный код. И чего в данном случае нарушает статическую типизацию? Какой-нить синтаксис вида Tuple<int, string>(1, «text»); для вас приемлем, здесь есть нарушение статической типизации?
AxisPod
Чёт слегка спутал dynamic и анонимные типы.