Является ли использование .NET 4.0 кортежей в моем коде C # плохим дизайнерским решением?

166

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

Итак, это:

public class ResultType
{
    public string StringValue { get; set; }
    public int IntValue { get; set; }
}

public ResultType GetAClassedValue()
{
    //..Do Some Stuff
    ResultType result = new ResultType { StringValue = "A String", IntValue = 2 };
    return result;
}

Это эквивалентно этому:

public Tuple<string, int> GetATupledValue()
{
    //...Do Some stuff
    Tuple<string, int> result = new Tuple<string, int>("A String", 2);
    return result;
}

Итак, если оставить в стороне возможность того, что я упускаю точку зрения Tuples, является ли пример с Tuple плохим выбором дизайна? Мне это кажется менее беспорядочным, но не настолько самодокументированным и чистым. Это означает, что с типом ResultTypeпозже станет ясно, что означает каждая часть класса, но у вас есть дополнительный код для поддержки. С помощью Tuple<string, int>вам нужно будет посмотреть и выяснить, что каждый из них Itemпредставляет, но вы пишете и поддерживаете меньше кода.

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

Джейсон Уэбб
источник
16
@Boltbait: потому что кортеж из теории множеств en.wikipedia.org/wiki/Tuple
Мэтью Уайтед
16
@BoltBait: «кортеж» - это упорядоченный набор значений, который не обязательно имеет отношение к строкам в базе данных.
FrustratedWithFormsDesigner
19
Кортежи в c # были бы еще лучше, если бы было какое-то хорошее действие по распаковке кортежей :)
Джастин
4
@John Saunders Мой вопрос конкретно о проектных решениях в c #. Я не использую другие языки .net, поэтому я не уверен, как кортежи влияют на них. Поэтому я отметил это c #. Мне показалось разумным, но я думаю, что и переход на .net тоже подойдет.
Джейсон Уэбб
13
@John Saunders: Я уверен, что он спрашивает о проектных решениях, касающихся использования или не использования Tuples в приложении, написанном исключительно на C #. Я не верю, что он спрашивает о дизайнерских решениях, которые были приняты при создании языка C #.
Бретт Видмайер

Ответы:

163

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

Однако в общедоступном API они менее эффективны. Потребитель (а не вы) должен либо угадывать, либо искать документацию, особенно для таких вещей, как Tuple<int, int>.

Я хотел бы использовать их для частных / внутренних членов, но использовать классы результатов для открытых / защищенных членов.

Этот ответ также имеет некоторую информацию.

Брайан Уоттс
источник
19
Разница между публичным и частным очень важна, спасибо за то, что подняли ее. Возвращение кортежей в ваш публичный API - это действительно плохая идея, но внутри, где все может быть тесно связано (но это нормально ), это нормально.
Клинтон Пирс
36
Плотно связаны или тесно связаны? ;)
contactmatt
Разве соответствующая документация не может быть полезна здесь? Например, у Scala StringOps(в основном это класс «методов расширения» для Java String) есть метод parition(Char => Boolean): (String, String)(принимает предикат, возвращает кортеж из 2 строк). Очевидно, что вывод (очевидно, что это будут символы, для которых предикат был истинным, а другой - для которых он был ложным, но не ясно, какой именно). Это документация, которая проясняет это (и сопоставление с образцом может быть использовано, чтобы прояснить при использовании, какой именно).
Кат
@Mike: из-за этого трудно понять, как правильно, а что нет - отличительный признак API, который кому-то где-то причинит боль :-)
Брайан Уоттс
79

На мой взгляд, Tuple - это ярлык для написания результирующего класса (я уверен, что есть и другие варианты использования).

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

Вот пример разумного использования для Tuple<>:

var opponents = new Tuple<Player,Player>( playerBob, playerSam );

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

var pokerHand = Tuple.Create( card1, card2, card3, card4, card5 );

Покерную комбинацию можно рассматривать как набор карт - и кортеж (может быть) является разумным способом выражения этой концепции.

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

Возвращение строго типизированных Tuple<>экземпляров как части открытого API для открытого типа редко является хорошей идеей. Как вы сами понимаете, кортежи требуют, чтобы вовлеченные стороны (автор библиотеки, пользователь библиотеки) заранее договорились о цели и интерпретации используемых типов кортежей. Достаточно сложно создать интуитивно понятные и понятные API-интерфейсы, которые Tuple<>публично скрывают только намерения и поведение API.

Анонимные типы также являются своего рода кортежем, однако они строго типизированы и позволяют указывать понятные, информативные имена для свойств, принадлежащих типу. Но анонимные типы сложно использовать в разных методах - они были в основном добавлены для поддержки таких технологий, как LINQ, где проекции будут создавать типы, которым мы обычно не хотим назначать имена. (Да, я знаю, что анонимные типы с одинаковыми типами и именованными свойствами объединяются компилятором).

Мое эмпирическое правило таково: если вы вернете его из открытого интерфейса, сделайте его именованным типом .

Мое другое практическое правило использования кортежей: аргументы метода имени и переменные типа localc Tuple<>как можно более понятны - заставьте имя представлять смысл отношений между элементами кортежа. Подумай о моем var opponents = ...примере.

Вот пример реального случая, когда я использовал, Tuple<>чтобы избежать объявления типа «только данные» для использования только в моей собственной сборке . Ситуация связана с тем, что при использовании общих словарей, содержащих анонимные типы, становится трудно использовать TryGetValue()метод для поиска элементов в словаре, поскольку для метода требуется outпараметр, который не может быть назван:

public static class DictionaryExt 
{
    // helper method that allows compiler to provide type inference
    // when attempting to locate optionally existent items in a dictionary
    public static Tuple<TValue,bool> Find<TKey,TValue>( 
        this IDictionary<TKey,TValue> dict, TKey keyToFind ) 
    {
        TValue foundValue = default(TValue);
        bool wasFound = dict.TryGetValue( keyToFind, out foundValue );
        return Tuple.Create( foundValue, wasFound );
    }
}

public class Program
{
    public static void Main()
    {
        var people = new[] { new { LastName = "Smith", FirstName = "Joe" },
                             new { LastName = "Sanders", FirstName = "Bob" } };

        var peopleDict = people.ToDictionary( d => d.LastName );

        // ??? foundItem <= what type would you put here?
        // peopleDict.TryGetValue( "Smith", out ??? );

        // so instead, we use our Find() extension:
        var result = peopleDict.Find( "Smith" );
        if( result.First )
        {
            Console.WriteLine( result.Second );
        }
    }
}

PS Существует еще один (более простой) способ обойти проблемы, возникающие из-за анонимных типов в словарях, и заключается в использовании varключевого слова, чтобы позволить компилятору «вывести» тип за вас. Вот эта версия:

var foundItem = peopleDict.FirstOrDefault().Value;
if( peopleDict.TryGetValue( "Smith", out foundItem ) )
{
   // use foundItem...
}
LBushkin
источник
5
@stakx: Да, я согласен. Но имейте в виду - этот пример в основном предназначен для иллюстрации случаев, когда использование a Tuple<> может иметь смысл. Всегда есть альтернативы ...
Л.Бушкин
1
Я думаю, что замена параметров на Tuple, как в TryGetValue или TryParse, является одним из немногих случаев, когда имеет смысл иметь открытый API, возвращающий Tuple. На самом деле, по крайней мере, один язык .NET автоматически делает это изменение API для вас.
Джоэл Мюллер
1
@stakx: кортежи также неизменны, а массивы - нет.
Роберт Полсон
2
Я понял, что кортежи полезны только как неизменяемые объекты игрока в покер.
Геннадий Ванин Геннадий Ванин
2
+1 Отличный ответ - мне нравятся ваши первые два примера, которые на самом деле немного изменили мое мнение. Я никогда не видел примеров, где кортеж был бы по крайней мере таким же подходящим, как пользовательская структура или тип. Тем не менее, ваш последний «реальный пример» для меня, кажется, больше не добавляет ценности. Первые два примера идеальны и просты. Последнее немного сложно понять, и ваш тип «PS» делает его бесполезным, поскольку ваш альтернативный подход намного проще и не требует кортежей.
Чиккодоро
18

Кортежи могут быть полезны ... но они могут быть болью позже. Если у вас есть метод, который возвращает, Tuple<int,string,string,int>откуда вы знаете, что эти значения позже. Были они ID, FirstName, LastName, Ageили были они UnitNumber, Street, City, ZipCode.

Мэтью Уайтед
источник
9

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

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

Анонимные классы в C # уже обеспечивают превосходное решение для наиболее частого частного использования кортежей, и они дают значимые имена элементам, так что они действительно превосходят в этом смысле. Единственная проблема заключается в том, что они не могут вытекать из именованных методов. Я бы предпочел, чтобы это ограничение было снято (возможно, только для частных методов), чем иметь особую поддержку кортежей в C #:

private var GetDesserts()
{
    return _icecreams.Select(
        i => new { icecream = i, topping = new Topping(i) }
    );
}

public void Eat()
{
    foreach (var dessert in GetDesserts())
    {
        dessert.icecream.AddTopping(dessert.topping);
        dessert.Eat();
    }
}
Дэниел Уорвикер
источник
В целом, я хотел бы, чтобы .net определил стандарт для нескольких специальных типов анонимных типов, например struct var SimpleStruct {int x, int y;}, для определения структуры с именем, которое начинается с guid, связанного с простыми структурами, и {System.Int16 x}{System.Int16 y}добавлено что-то вроде ; любое имя, начинающееся с этого GUID, должно представлять структуру, содержащую только эти элементы и конкретное указанное содержимое; если несколько определений для одного и того же имени находятся в области видимости, и они совпадают, все будут рассматриваться как один и тот же тип.
суперкат
Такая вещь была бы полезна для нескольких типов типов, включая изменяемые и неизменяемые классы и структуры, а также интерфейсы и делегаты для использования в ситуациях, когда согласованная структура должна рассматриваться как подразумевающая сопоставление семантики. Хотя, безусловно, есть причины, по которым может быть полезно иметь два типа делегатов с одинаковыми параметрами, которые нельзя считать взаимозаменяемыми, было бы также полезно, если бы можно было указать, что метод требует делегата, который принимает некоторую комбинацию параметров, но за Мне все равно, что это за делегат.
суперкат
К сожалению, если два разных человека хотят писать функции, которые принимают в качестве входных делегатов, которым нужен один refпараметр, единственный способ получить один делегат, который может быть передан обеим функциям, будет, если один человек создаст и скомпилирует Тип делегата и дает его другому, чтобы построить против. Никто не может указать, что эти два типа следует считать синонимами.
суперкат
У вас есть более конкретные примеры использования анонимных классов Tuple? Я просто просмотрел 50 мест в моей кодовой базе, где я использую кортежи, и все они либо являются возвращаемыми значениями (как правило yield return), либо являются элементами коллекций, которые используются где-то еще, поэтому не стоит использовать анонимные классы.
2
@JonofAllTrades - мнения, вероятно, расходятся по этому вопросу, но я стараюсь разрабатывать общедоступные методы так, как будто они будут использоваться кем-то другим, даже если это кто-то я, поэтому я предоставляю себе хорошее обслуживание клиентов! Вы склонны вызывать функцию больше раз, чем пишете. :)
Даниэль Уорвикер
8

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

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

// one possible use of tuple within a private context. would never
// return an opaque non-descript instance as a result, but useful
// when scope is known [ie private] and implementation intimacy is
// expected
public class WorkflowHost
{
    // a map of uri's to a workflow service definition 
    // and workflow service instance. By convention, first
    // element of tuple is definition, second element is
    // instance
    private Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> _map = 
        new Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> ();
}
Джонни г
источник
2
Основные языки "RAD" (Java - лучший пример) всегда выбирали безопасность и избегали стрельбы по сценариям типа стопы. Я рад, что Microsoft рискует с C # и дает языку некоторую хорошую мощь, даже если она может быть использована не по назначению.
Мэтт Грир
4
Ключевое слово var нельзя злоупотреблять. Возможно, вы имели в виду динамический?
Крис Марисик
@Chris Marisic, просто чтобы уточнить, что я не имел в виду тот же сценарий - вы абсолютно правы, вас varнельзя передать назад или существовать за пределами заявленной области. однако, все еще возможно использовать это сверх его предназначенного предназначения и быть "оскорбленным"
Джонни г
5
Я до сих пор стою на том, что вар нельзя злоупотреблять периодом. Это просто ключевое слово для более короткого определения переменной. Ваш аргумент, возможно, касается анонимных типов, но даже этими действительно нельзя злоупотреблять, потому что они все еще являются обычными статически связанными типами, а теперь динамические - это совсем другая история.
Крис Марисик
4
Можно злоупотреблять var в том смысле, что если он используется слишком часто, это может вызвать проблемы у человека, читающего код. Особенно, если вы не в Visual Studio и вам не хватает intellisense.
Мэтт Грир
5

Использование класса вроде бы ResultTypeпонятнее. Вы можете дать значимые имена полям в классе (тогда как с помощью кортежа они будут называться Item1и Item2). Это еще более важно, если типы двух полей одинаковы: имя четко различает их.

Ричард Ферн
источник
Одно небольшое дополнительное преимущество: вы можете безопасно изменить порядок параметров для класса. Выполнение этого для кортежа нарушит код, и если поля имеют схожие типы, это может быть невозможно обнаружить во время компиляции.
Я согласен, кортежи используются, когда разработчику не хватает энергии, чтобы думать о значимом имени класса, объединяющем значимые имена свойств. Программирование - это общение. Кортежи являются антипаттерном к общению. Я бы предпочел, чтобы Microsoft оставила это вне языка, чтобы заставить разработчиков принимать правильные дизайнерские решения.
Майк
5

Как насчет использования кортежей в шаблоне decorate-sort-undecorate? Преобразование Шварца для людей Perl. Вот, конечно, надуманный пример, но Tuples - хороший способ справиться с подобными вещами:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("C:\\Windows")
                    .Select(x => new Tuple<string, string>(x, FirstLine(x)))
                    .OrderBy(x => x.Item2)
                    .Select(x => x.Item1).ToArray();
        }
        static string FirstLine(string path)
        {
            using (TextReader tr = new StreamReader(
                        File.Open(path, FileMode.Open)))
            {
                return tr.ReadLine();
            }
        }
    }
}

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

Клинтон Пирс
источник
3
Вы можете использовать Tuple.Create вместо конструктора. Это короче.
Кугель,
анонимный тип будет намного более читабельным; как если бы использовались реальные имена переменных вместо 'x'
Дейв Кузино
Это было бы сейчас . Это ТАКАЯ проблема, ответы, опубликованные много лет назад, никогда не очищаются и не обновляются для новых технологий.
Клинтон Пирс,
4

IMO, эти "кортежи" - это в основном все анонимные structтипы с открытым доступом с неназванными участниками .

Только место , где я хотел бы использовать кортеж , когда вам нужно быстро BLOb вместе некоторые данные, в очень ограниченном объеме. Семантика данных должна быть очевидна , поэтому код не сложен для чтения. Поэтому использование tuple ( int, int) для (row, col) кажется разумным. Но мне трудно найти преимущество передstruct именованными членами (так что никаких ошибок не делается, и строка / столбец не случайно меняются местами)

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

Возьмите простой пример:

struct Color{ float r,g,b,a ; }
public void setColor( Color color )
{
}

Версия кортежа

public void setColor( Tuple<float,float,float,float> color )
{
  // why?
}

Я не вижу никакого преимущества в использовании кортежа вместо структуры с именованными членами. Использование неназванных членов - это шаг назад для читабельности и понятности вашего кода.

Tuple кажется мне ленивым способом избежать создания structс настоящими именованными участниками. Чрезмерное использование кортежа, когда вы действительно чувствуете, что вам / или кому-то другому, сталкивающемуся с вашим кодом, понадобятся именованные участники - A Bad Thing ™, если я когда-либо видел его.

bobobobo
источник
3

Не судите меня, я не эксперт, но теперь с новыми кортежами в C # 7.x вы можете вернуть что-то вроде:

return (string Name1, int Name2)

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

Денис Гордин
источник
2

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

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

Разумеется, при использовании в качестве модераторов кортежи могут привести к получению более краткого и удобочитаемого кода. Вы можете посмотреть на C ++, STL и Boost, чтобы найти примеры того, как кортежи используются в других языках, но в итоге нам всем придется поэкспериментировать, чтобы найти, как они лучше всего подходят для среды .NET.

Панагиотис Канавос
источник
1

Кортежи - это бесполезная фреймворк в .NET 4. Я думаю, что в C # 4.0 была упущена отличная возможность. Я хотел бы иметь кортежи с именованными членами, чтобы вы могли получить доступ к различным полям кортежа по имени вместо Value1 , Value2 и т. Д.

Это потребовало бы изменения языка (синтаксиса), но это было бы очень полезно.

Филипп Лейбер
источник
Как кортежи являются «языковой функцией»? Это класс, доступный для всех языков .net, не так ли?
Джейсон Уэбб
11
Относительно бесполезно для C #, возможно. Но System.Tuple не был добавлен в платформу из-за C #. Он был добавлен для облегчения взаимодействия между F # и другими языками.
Джоэл Мюллер
@Jason: я не говорил, что это была языковая функция (я сделал это неправильно, но я исправил это, прежде чем вы сделали этот комментарий).
Филипп Лейберт
2
@Jason - языки с прямой поддержкой кортежей имеют неявную поддержку деконструкции кортежей в значения их компонентов. Код, написанный на этих языках, обычно никогда не обращается к свойствам Item1, Item2. let success, value = MethodThatReturnsATuple()
Джоэл Мюллер
@Joel: этот вопрос помечен как C #, так что это как-то о C #. Я все еще думаю, что они могли бы превратить это в первоклассную функцию в языке.
Филипп Лейберт
1

Лично я бы никогда не использовал Tuple в качестве возвращаемого типа, потому что нет никаких указаний на то, что представляют значения. У кортежей есть несколько полезных применений, потому что в отличие от объектов они являются типами значений и, следовательно, понимают равенство. Из-за этого я буду использовать их в качестве ключей словаря, если мне нужен многокомпонентный ключ, или в качестве ключа для предложения GroupBy, если я хочу группировать по нескольким переменным и не хочу вложенных группировок (Кто когда-либо хочет вложенные группировки?). Чтобы преодолеть проблему с чрезвычайным многословием, вы можете создать их с помощью вспомогательного метода. Имейте в виду, что если вы часто обращаетесь к членам (через Item1, Item2 и т. Д.), Вам, вероятно, следует использовать другую конструкцию, такую ​​как struct или анонимный класс.

Сет Полсон
источник
0

Я использовал кортежи, как Tupleи новые ValueTuple, в ряде различных сценариев и пришел к следующему выводу: не использовать .

Каждый раз я сталкивался со следующими проблемами:

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

Мое мнение, что кортежи - это вред, а не особенность C #.

У меня несколько похожая, но гораздо менее резкая критика Func<>и Action<>. Они полезны во многих ситуациях, особенно в простых Actionи Func<type>вариантах, но, помимо всего прочего, я обнаружил, что создание типа делегата является более элегантным, читаемым, обслуживаемым и дает вам больше возможностей, таких как ref/ outпараметры.

Мистер ТА
источник
Я упомянул именованные кортежи - ValueTupleи они мне тоже не нравятся. Во-первых, наименование не является сильным, это скорее предложение, очень легко испортить, потому что оно может быть назначено неявно одному Tupleили ValueTupleс тем же числом и типами элементов. Во-вторых, отсутствие наследования и необходимость копировать вставить все определение с места на место по-прежнему вредны.
г-н Т.А.
Я согласен с этими пунктами. Я стараюсь использовать кортежи только тогда, когда я контролирую производителя и потребителя, и если они не слишком «далеко». Значение «производитель» и «потребитель» в одном и том же методе = «близко», тогда как «производитель» и «потребитель» в разных сборках = «световые годы далеко». Итак, если я создаю кортежи в начале метода и использую их в конце? Конечно, я в порядке с этим. Я также документирую условия и тому подобное. Но да, я согласен, что в целом это не правильный выбор.
Майк Кристиансен