Еще не было официального релиза C# 6 и его нового компилятора «Roslyn», а уже становятся известны подробности следующей редакции — C# 7. И она обещает нам много всяких «вкусностей», которые должны облегчить наше с вами существование. Хотя это все пока предварительно, но все равно интересно, чем нас порадует Microsoft в не совсем ближайшем будущем.



Кортежи (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)


  1. js605451
    28.04.2015 23:45
    +5

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

    list.Add("John Doe", 66); // Такое добавление данных в лист не может не радовать
    

    Это как раз хреновый пример, который воспринимается на порядок хуже, чем длинное скучное:
    list.Add(new Person 
    {
      Name = "John Doe",
      Age = 66
    });
    


    1. NeoCode
      29.04.2015 00:13
      +3

      Кортежи — это одна из тех естественных вещей, которая была неочевидна (или недоступна в явном виде) в течение очень долгого времени, и только недавно стала проникать в мейнстрим. В общем, кортеж — это группа имен времени компиляции и не более того. Список аргументов функции — кортеж. Список инициализации в фигурных скобках — это тоже кортеж. Даже список полей и методов класса тоже можно рассматривать как кортеж (хотя и не кортеж объектов, а скорее чистый кортеж имен — это уже к метапрограммированию). В идеале, это способ группировки чего угодно на этапе компиляции — и в общем это более фундаментально чем структура (которая по сути лишь кортеж в обертке типа). И множественный возврат из функции — лишь самое очевидное применение. Как насчет групповых или множественных операций?

      (i, j, k) = (a, b, c);
      (i, j, k) += 100;
      

      Хотя и это мелочи. Думаю, со временем ситуация с кортежами прояснится и раскроется их мощь, а то многие думают что это недоструктура или даже недосписок как в питоне.


      1. js605451
        29.04.2015 00:32
        +3

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


        1. NeoCode
          29.04.2015 09:18
          +7

          Тип номинативно-типизирован, кортеж — структурно-типизирован. Это разные вещи с разными областям применения. Если вам нужна функция, принимающая два параметра — вы пишете функцию с двумя аргументами, а не функцию, принимающую тип с двумя полями. Также и здесь. Бывает, когда нужно вернуть из функции объект (как единое целое) — в этом случае объявляется структура; а если нужно вернуть просто два значения, никак не связанных между собой — применяется кортеж и множественный возврат из функции.


          1. js605451
            29.04.2015 09:26

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

            Если возникает необходимость вернуть 2 значения, никак не связанных между собой, стоит пересмотреть ответственности, возложенные на метод. Предложите пример такого метода — интересно чем вы будете руководствоваться придумывая для него имя.
            Магазину «Котлеты и Ноутбуки» срочно требуется определиться


            1. nickolaym
              29.04.2015 16:44
              -2

              Таких котлет и ноутбуков — вагон и тележка.
              Обычай предписывает использовать для этого out-параметры

              Cutlet GetCutletAndNotebook(out Notebook nb)
              


              В С++ есть функции, возвращающие пару — например, итератор и флажок при вставке в set.
              Предложите, как пересмотреть ответственности для них.


              1. js605451
                29.04.2015 20:13

                Таких котлет и ноутбуков — вагон и тележка.
                Обычай предписывает использовать для этого out-параметры

                out-параметры — это минорная фича, необходимость в которой возникает крайне редко. Я не знаком с историей её возникновения, но предположу, что на 90% мотивация была — байндинги для сишного кода. Очень многие функции WinAPI возвращают «полезный результат» через параметр, а возвращаемое значение используют для кода возврата. Это традиция из культуры C — там нет исключений, поэтому возникает та самая интересная ситуация когда нужно и код возврата сообщить, и полезные данные вернуть. В Java, например, out-параметров вообще нет — проблем с этим не возникает.

                В С++ есть функции, возвращающие пару — например, итератор и флажок при вставке в set.
                Предложите, как пересмотреть ответственности для них.

                У стандартной библиотеки C++ своя уникально-экзотическая философия из которой органично вытекает этот неповторимый извращённый дизайн. Во многих других мейнстримных языках аналогичный метод возвращает bool и сложностей ни у кого не возникает.


                1. nickolaym
                  29.04.2015 21:14

                  Мотивация у out-параметров — чтоб не рожать новые классы на каждый случай возвращения кортежа.
                  Заодно, удачно ложится на сишные интерфейсы и COM.

                  В яве своя культура сложилась, не зря её называют королевством существительных. Там, ещё одним классом больше, классом меньше, не жалко.


            1. exvel
              05.05.2015 16:08

              Зачем предлагать, когда уже и так есть в самом .NET:
              TryParse и его разновидности.


          1. hmspns
            29.04.2015 13:12
            -1

            А чем Tuple<> не угодил?


            1. ApeCoder
              29.04.2015 13:36

              github.com/dotnet/roslyn/issues/347 — см раздел background


          1. brewerof
            29.04.2015 15:41
            +1

            А что заминусовали js605451?

            Он прав, это будет провоцировать сode smells в неумелых ручках.


            1. Danov
              01.05.2015 00:05

              С таким аргументом и болгарку придется заменить ножовкой.


            1. AndrewMayorov
              01.05.2015 14:49

              Это в любом случае не будет пахнуть хуже, чем метод с пятью out-параметрами.


              1. brewerof
                02.05.2015 20:54
                +1

                Не будет, но теперь +1 способ сломать первую буковку из SOLID, при чем довольно удобный. Опять оговариваюсь — в кривых ручках.


      1. impwx
        29.04.2015 14:44

        (i, j, k) += 100;
        

        Тут больше подходит не кортеж, а IEnumerable<T>. В кортеже все типы могут быть разные и абсолютно непонятно, что тогда должен делать такой оператор.


        1. ilammy
          29.04.2015 17:47
          -1

          Добавлять 100 ко всему, что в кортеже, или не компилироваться, если это невозможно.


          1. impwx
            29.04.2015 18:49
            +2

            Это применение из области эзотерики. Для нормальных случаев есть data.Select(x => x + 100).


    1. nsinreal
      29.04.2015 01:25
      +4

      В отсутствии кортежей есть один недостаток. Вот пишешь ты метод, в нем юзаешь анонимный тип. А потом хочешь сделать extract to method — и все, все плюшки закончились. Нужно вводить новый класс, переопределять операции == (хотя можно делать nullable structures, там вроде это автоматом идет), а это на порядки больше времени/места занимает.

      В итоге у вас:
      1) Либо один большой говнометод
      2) Много непонятных data-классов, которые нужны только в пределах двух-трех методов
      3) Либо код будет содержать Tuple/KeyValuePair, что вообще читабельности не добавляет.

      И кортежи решают проблему — у вас появляются анонимные типы (статически типизированные и с адекватными именами свойств), которые легко шарятся между тремя методами. Т.е. можно писать короткие методы (держа каждый метод на едином уровне абстракции) и не писать лишнего говнокода. На всякий случай: количества смысла и адекватности на одну строку кода повышается.

      Не, можно конечно юзать это и во вред проекту. Но в C# есть более опасная фича в виде dynamic, а её никто не спешит юзать без реальной нужды.

      Также вместе с кортежами должны идти pattern matching/destructuring. Это по сути возможность написать foreach ((var id, var name) in dictionary)


      1. js605451
        29.04.2015 08:36
        +1

        1) Либо один большой говнометод
        2) Много непонятных data-классов, которые нужны только в пределах двух-трех методов
        3) Либо код будет содержать Tuple/KeyValuePair, что вообще читабельности не добавляет.

        Всё верно — если программист решая задачу сталкивается с выбором из этих 3 пунктов, очевидно он сам не понял что напрограммировал. Есть большая разница между «кодом, который выдаёт нужные результаты» и «кодом, который решает задачу»: в первом случае просто проходят тесты, а во втором — в явном виде описываются намерения: вводятся термины, связи между ними, и уже в контексте этого небольшого «под-домена» описывается решение задачи.

        Обратите внимание, ваше описание решаемой проблемы начинается с фразы «Вот пишешь ты метод, в нем юзаешь анонимный тип» — проблема появляется уже здесь, а не в момент «extract method»: вы посчитали, что «вот эта сущность» недостаточно важна, чтобы вводить её в явном виде, и ввели неявно. На следующем шаге вы об этом начинаете горько сожалеть, т.к. выясняется, что именно эта сущность связывает несколько частей решения задачи.


        1. nsinreal
          29.04.2015 13:33

          вы посчитали, что «вот эта сущность» недостаточно важна, чтобы вводить её в явном виде, и ввели неявно

          О, мы нашли точку непонимания. Вы считаете, что если сущность шарится между методами, то она важна. Это не так. Возможно у вас не встречалось задач, в которых есть сущность настолько мелкая, что писать на неё лишний тип и придумывать осознанное адекватное имя нецелесообразно. Да, она связывает несколько «частей» решения задачи, и что?

          У меня каждая часть решения состоит из двух-четырех строк — у меня в этой задачи главное методы, а не данные. Вы правда мне предлагаете выделять новый тип данных? Завести отдельный файлик под структуру, перечислить требуемые неймспейсы, указать неймспейс сущности, указать что за сущность, указать два свойства сущности, разделив их пробелами по гайдлайнам. У меня есть код в 10 строк, вы предлагаете добавить еще 10 строк и один файл, запутав остальных в проекте самим существованием левой модели?

          Я предпологаю, что у вас нету понимания, что некоторые задачи нужно решать, продумывая типы данных (ООП и прочее); а в некоторых задачах нужно писать код (функции). А если я начну городить больше кода, больше типов, больше классов, то он станет нечитабельным (концентрация смысла на строку резко упадет, а охватить все разумом станет трудно). С другой стороны, если я возьму другую задачу, то мне сначала лучше продумать типы данных, потому что в ином случае мой код будет нечитабельной тыквой. Смесь парадигм несет программисту счастье.


          1. js605451
            29.04.2015 19:21

            Вы считаете, что если сущность шарится между методами, то она важна.

            Т.к. сущность шарится между методами, она является частью внутреннего интерфейса. Интерфейс — важен, будь он внутренний или внешний.

            Вы правда мне предлагаете выделять новый тип данных?

            Да, я предлагаю не мудрствовать, а просто взять и выделить новый тип данных. Не обязательно так громко, как вы описали: если речь идёт про реализацию одного единственного класса, достаточно сделать внутренний класс. Хотя, вообще говоря, и от лишнего файла Земля не остановится.

            А если я начну городить больше кода, больше типов, больше классов, то он станет нечитабельным

            Наоборот. В голове намного проще удержать сущность, если у неё есть имя. Удержать в голове «ту штуку, в которой лежат a, b и c» — намного сложнее, т.к. вы не поднимаетесь выше a, b и c — они как были отдельными компонентами, так и остались.


            1. ApeCoder
              30.04.2015 11:23

              Мне кажется, это более предназначенно для работы с LINQ и лябдами. Если функции неименованные, то могут быть и типы неименованные.


          1. SirEdvin
            30.04.2015 11:49

            Смесь парадигм несет программисту счастье
            И большое проблемы другому программисту, который будет в этом коде разбираться.
            Если у вас задача, где нужно писать код, то почему бы не написать ее на F# или другом функциональном языке, а потом, если нужно, подключить его к C#?


            1. ApeCoder
              30.04.2015 13:01

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


            1. nsinreal
              30.04.2015 13:28

              Потому что смесь языков не несет счастье. Сесь парадигм несет счастье. Почему так? Потому что код един, в одном формате и не нарушается принцип DRY. В случае когда мы мешаем языки, такого не происходит. Если у нас есть C# (бек) и Javascript (фронт), то происходит дублирование, либо отсутствие на каком-то из слоев какой-то полезной логики, либо изобретаются всякие транскомпиляторы. Если у нас есть C# и F#, то даже в пределах их общий shared код — это просто пиздец. Не надо рассказывать про то, что они на .NET и легко интероптятся. Я пробовал, я знаю что это не пашет с легкого пинка.

              А еще под F# нету многих инструментов и фич, которые есть под C#.


            1. nsinreal
              30.04.2015 13:33

              Особенно ржачно выглядит когда человек пытается подключить всякие там IronPython в проект на C#


      1. VladVR
        05.05.2015 21:00

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

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


    1. AxisPod
      29.04.2015 07:41
      -8

      Ну как бы тот же LINQ был бы невозможен в том виде что есть без типа dynamic. А в данном случае кортежи упростят местами код и при этом еще помогут соптимизировать конечный код. И чего в данном случае нарушает статическую типизацию? Какой-нить синтаксис вида Tuple<int, string>(1, «text»); для вас приемлем, здесь есть нарушение статической типизации?


      1. AxisPod
        29.04.2015 08:39
        +1

        Чёт слегка спутал dynamic и анонимные типы.


  1. NeoCode
    29.04.2015 00:18
    +1

    Спасибо! Надеюсь что будет и следующая часть — Pattern matching / Records / algebraic data types (Proposal: #206), там еще интереснее:)


    1. BOBS13 Автор
      29.04.2015 07:40
      +1

      Да, я как раз планировал во второй части описать эту фичу.


  1. eocron
    29.04.2015 01:13

    Единственное место, где я вижу для них перспективу в большинстве проектов — табличные списки разного формата и DataBaseStyle. Остальное просто убьет «сладостью» весь проект, ничего же не понятно будет.

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


  1. nosuchip
    29.04.2015 09:42

    Не увидел — планируется ли разворачивание кортежей в переменные? Вроде такого:

    a, b, c = (1,2,3)


    1. BOBS13 Автор
      29.04.2015 09:54

      Нет, такого не будет. Во всяком случаи пока в планах такого я не увидел.


      1. 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.


        1. BOBS13 Автор
          29.04.2015 10:35

          Спасибо, упустил этот момент, добавил в статью.


  1. KReal
    29.04.2015 13:41

    И кортежи, и паттерн матчинг, и наверняка многое из этого списка есть в F# и используется, как мне кажется, для ограниченного круга задач. Зачем тащить это в C#?


    1. BOBS13 Автор
      29.04.2015 17:52

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


      1. js605451
        30.04.2015 00:06

        Так для этого не нужны кортежи — анонимных типов достаточно.


  1. impwx
    29.04.2015 14:43
    +1

    Очень спорная фича по нескольким причинам. Во-первых, почему решили придумать новый синтаксис для практически того же, что уже делают анонимные типы, а не расширить их возможность? Во-вторых, почему именно структура? Например, следующий код не сработает:

    var list = new List<(int X, int Y)>();
    list.Add((1, 2));
    list[0].X = 2; // ошибка: потеряли lvalue
    


    1. BOBS13 Автор
      29.04.2015 17:41
      +1

      Я не очень понял, почему ошибка можете поянить?


      1. impwx
        29.04.2015 18:41

        Потому, что индексатор List<T> — это присыпанная синтаксическим сахаром функция. Из нее возвращается копия структуры, поэтому лежащее в списке значение не может быть изменено присваиванием. С массивом ситуация обстоит по-другому: там индексатор реализован с помощью отдельной инструкции, позволяющей обратиться непосредственно к объекту по адресу.


  1. SirEdvin
    29.04.2015 20:21

    Я одного не понял. Будут создаваться таки анонимные структуры или классы? Потому что если структуры, фича куда более, чем просто сомнительна.


    1. 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.


      1. SirEdvin
        29.04.2015 22:29
        +1

        То есть у них получилась такая фича, что если не знать ее паттерн, она плавно переходит в баг?


        1. omikad
          30.04.2015 07:57

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