Використовує .NET 4.0 кортежі в моєму коді C # погане рішення про дизайн?


166

З додаванням класу Tuple в .net 4 я намагався вирішити, чи використовувати їх у моєму дизайні поганий вибір чи ні. Як я це бачу, кортеж може бути ярликом до написання результатного класу (я впевнений, що є й інші напрями).

Отже це:

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, де проекції створювали б типи, яким ми зазвичай не хотіли б присвоювати імена. (Так, я знаю, що анонімні типи з однаковими типами та названими властивостями консолідуються компілятором).

Моє правило: якщо ви повернете його з вашого загальнодоступного інтерфейсу - зробіть його іменованим типом .

Інше моє правило щодо використання кортежів полягає в тому, щоб: назви аргументів методу імен і локальних змінних типу було 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...
}

5
@stakx: Так, я згоден. Але майте на увазі - приклад багато в чому призначений для ілюстрування випадків, коли використання Tuple<> могло мати сенс. Завжди є альтернативи ...
Л.Бушкін

1
Я думаю, що заміна параметрів Tuple, як у TryGetValue або TryParse, - один з небагатьох випадків, коли може мати сенс, щоб публічний API повертав Tuple. Насправді, принаймні одна мова .NET автоматично змінює цей API для вас.
Джоель Мюллер

1
@stakx: кортежі також незмінні, тоді як масиви - ні.
Роберт Поулсон

2
Я зрозумів, що кортежі корисні лише як незмінні об’єкти гравця в покер.
Геннадій Ванін Геннадій Ванін

2
+1 Чудова відповідь - я люблю ваші два початкові приклади, які насправді трохи змінили мою думку. Я ніколи не бачив прикладів, коли кортеж був принаймні настільки ж підходящим, як спеціальна структура чи тип. Однак ваш останній "приклад із реального життя", здається, більше не додає великої цінності. Перші два приклади ідеальні та прості. Останнє трохи важко зрозуміти, і ваш тип "PS" робить його марним, оскільки ваш альтернативний підхід набагато простіший і не вимагає кортежів.
chiccodoro

18

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


9

Кортежі є досить корисним доповненням до CLR з точки зору програміста C #. Якщо у вас є колекція елементів, яка відрізняється за довжиною, вам не потрібно, щоб вони мали унікальні статичні назви під час компіляції.

Але якщо у вас колекція постійної довжини, це означає, що фіксовані місця у колекції мають певне заздалегідь визначене значення. І це завжди краще , щоб дати їм відповідні імена статичних в цьому випадку, замість того , щоб згадати значення 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;}, визначав би структуру з іменем, що починається з настанови, пов'язаної з простими структурами та має щось на зразок {System.Int16 x}{System.Int16 y}доданого; будь-яке ім'я, що починається з цього GUID, повинно було представляти структуру, що містить саме ці елементи та певний визначений вміст; якщо декілька визначень для одного і того ж імені знаходяться в області дії і вони відповідають, всі вважатимуться одним типом.
суперкар

Така річ була б корисною для декількох типів, включаючи змінні та незмінні класи та структури, а також інтерфейси та делегати для використання в ситуаціях, коли відповідна структура повинна вважати такою, що передбачає відповідність семантики. Хоча, безумовно, є причини, чому може бути корисним два типи делегатов, які мають однакові параметри, не вважатись взаємозамінними, також було б корисно, якби можна було вказати, що метод хоче делегата, який приймає якусь комбінацію параметрів, але виходить за межі цього Мені все одно, який це делегат.
supercat

Прикро, що якщо двоє різних людей хочуть записати функції, які приймають вхідних делегатів, яким потрібен єдиний refпараметр, єдиний спосіб отримати один делегат, який може бути переданий обом функціям, буде, якщо одна людина виробляє і компілює тип делегата і надає його іншому для побудови. Немає можливості обом людям вказати, що два типи слід вважати синонімами.
supercat

Чи є у вас конкретніші приклади, де ви б використовували анонімні заняття, а не Tuple? Я просто проглянув 50 місць у своїй кодовій базі даних, де я використовую кортежі, і всі вони або повертаються значення (як правило yield return), або є елементами колекцій, які використовуються в іншому місці, тому не відвідуйте анонімні класи.

2
@JonofAllTrades - думки, ймовірно, залежать від цього, але я намагаюся створити публічні методи так, ніби вони будуть використовуватися кимось іншим, навіть якщо це хтось я, тому я даю собі хороше обслуговування клієнтів! Ви схильні викликати функцію більше разів, ніж ви пишете її. :)
Daniel Earwicker

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не можете бути передані назад або існувати за межами заявленої області. однак, все-таки можна використовувати його понад призначене призначення та його "зловживати"
johnny g

5
Я все ще стою на тому, що не можна зловживати періодом. Це лише ключове слово, щоб зробити коротше визначення змінної. Можливо, ваш аргумент стосується анонімних типів, але навіть тими, хто насправді не може зловживати, тому що вони все ще є нормально статично пов'язаними типами, зараз динамічними, це зовсім інша історія.
Кріс Марісіч

4
вар можна зловживати в тому сенсі, що при надмірному використанні це може іноді викликати проблеми у людини, яка читає код. Особливо, якщо ви не в Visual Studio і вам не вистачає інтелігенції.
Метт Грір

5

Використання класу на зразок ResultTypeзрозуміліше. Ви можете дати значущі імена полям у класі (тоді як з кортежем вони будуть називатися Item1та Item2). Це ще важливіше, якщо типи двох полів однакові: ім’я чітко розмежовує їх.


Ще одна невелика додаткова перевага: ви можете сміливо упорядкувати параметри для класу. Якщо це зробити для кортежу, код буде порушений, і якщо поля мають подібні типи, це може виявитись не під час компіляції.

Я згоден, кортежі використовуються тоді, коли розробнику не вистачає енергії, щоб придумати змістовне ім’я класу, групуючи значущі імена властивостей. Програмування стосується спілкування. Кортежі - антипатерн комунікації. Я хотів би, щоб Microsoft не залишила його мови, щоб змусити розробників приймати хороші дизайнерські рішення.
мік золото

5

Як щодо використання кортежів у декор-сортуванні-неповторному малюнку? (Трансформація Шварца для людей Перла). Ось надуманий приклад, напевно, але кортежі, здається, є хорошим способом впоратися з подібними речами:

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();
            }
        }
    }
}

Тепер я міг би використовувати Об’єкт [] з двох елементів або в цьому конкретному прикладі рядок [] з двох елементів. Справа в тому, що я міг би використати що-небудь як другий елемент в кортежі, який використовується внутрішньо і досить легко читається.


3
Ви можете використовувати Tuple.Create замість конструктора. Це коротше.
Кугель

анонімний тип буде набагато читабельнішим; як би використовували справжні імена змінних замість "x"
Дейв Кузіно

Це було б зараз . Це проблема ТО, відповіді, опубліковані роки та роки тому, ніколи не очищаються та не оновлюються для нових технологій.
Клінтон Пірс,

4

IMO ці "кортежі" - це, в основному, всі анонімні structтипи доступу з публічним доступом з неназваними членами .

Тільки місце , де я хотів би використовувати кортеж , коли вам потрібно швидко BLOb разом деякі дані, в дуже обмеженому обсязі. Семантика даних повинна бути очевидною , тому код не важко прочитати. Тож використання кортежа ( int, int) для (рядок, стовпчик) здається розумним. Але мені важко знайти перевагу над structз названими членами (тому помилок не робиться, а рядки / стовпці випадково не замінені)

Якщо ви надсилаєте дані абоненту або приймаєте дані від абонента, вам дійсно слід скористатись structіменованими членами.

Візьмемо простий приклад:

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

Версія кортежу

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

Я не бачу жодної переваги в застосуванні кортежу замість структури з названими членами. Використання неназваних членів - це крок назад для читабельності та зрозумілості вашого коду.

Кортеж вважає мене ледачим способом уникнути створення structдійсних членів. Надмірне використання кортежу, де вам справді здається, що ви / або хтось інший, що стикається з вашим кодом, потребують названих членів, - це погана річ ™, якщо я коли-небудь бачив його.


3

Не судіть мене, я не експерт, але з новими Tuples в C # 7.x зараз ви можете повернути щось на кшталт:

return (string Name1, int Name2)

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


2

Це залежить, звичайно! Як ви вже говорили, кортеж може заощадити код та час, коли ви хочете згрупувати деякі предмети разом для місцевого споживання. Ви також можете використовувати їх для створення більш загальних алгоритмів обробки, ніж ви можете, якщо ви проходите конкретний клас навколо. Я не можу згадати, скільки разів мені хотілося, щоб у мене було щось за межами KeyValuePair або DataRow, щоб швидко передати якусь дату з одного методу на інший.

З іншого боку, цілком можна перестаратися і пройти навколо кортежів, де можна лише здогадуватися, що вони містять. Якщо ви збираєтесь використовувати кортеж у різних класах, можливо, було б краще створити один конкретний клас.

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


1

Кортежі - це марна рамкова функція. Я хотів би мати кортежі з названими членами, щоб ви могли отримати доступ до різних полів кортежу по імені замість 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 тощо), то, ймовірно, ви повинні використовувати іншу конструкцію, таку як структура або анонімний клас.


0

Я використовував кортежі, як Tupleнові , так і нові ValueTuple, у багатьох різних сценаріях і дійшов наступного висновку: не використовувати .

Кожного разу я стикався з такими проблемами:

  • код став нечитабельним через відсутність чітких імен;
  • не в змозі використовувати функції ієрархії класів, такі як DTO базового класу та DTO класу тощо;
  • якщо вони використовуються в більш ніж одному місці, ви закінчите копіювати та вставляти ці потворні визначення замість чистого імені класу.

На мою думку, кортежі - це шкода, а не особливість C #.

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


Я згадав названі кортежі - ValueTuple- і вони мені теж не подобаються. По-перше, найменування не є сильним, це скоріше пропозиція, дуже легко зіпсувати, оскільки його можна призначати неявно Tupleабо ValueTupleз однаковою кількістю та типами елементів. По-друге, відсутність спадщини та необхідність копіювання вставляти ціле визначення з місця на місце все ще згубні.
Містер ТА

Я погоджуюся з тими моментами. Я намагаюся використовувати кортежі лише тоді, коли я контролюю виробника та споживача, і якщо вони не надто "далеко". Значення, виробник і споживач в одному і тому ж методі = «близький», тоді як виробник і споживач в різних складах = «світловий рік». Отже, якщо я створюю кортежі на початку методу і використовую їх у кінці? Звичайно, я з цим добре. Я також документую умови і таке. Але так, я згоден, що зазвичай вони не є правильним вибором.
Майк Крістіансен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.