Практичний приклад, де Tuple можна використовувати в .Net 4.0?


97

Я бачив кортеж, представлений в .Net 4, але я не в змозі уявити, де його можна використовувати. Ми завжди можемо скласти спеціальний клас або Struct.


13
Можливо, це добре місце, щоб зауважити, що ця тема дуже стара. Подивіться, що сталося в C # 7 !
maf-soft

Відповіді:


83

У цьому справа - зручніше не робити спеціальний клас чи структурувати весь час. Це вдосконалення на кшталт Actionабо Func... ви можете зробити ці типи самостійно, але зручно, що вони існують у рамках.


5
Напевно, варто зазначити з MSDN : "Будь-які публічні статичні члени цього типу є безпечними для потоків. Будь-які члени екземплярів не гарантують безпеку потоку ".
Метт Борджа

Ну, може, мовні дизайнери повинні були спростити створення власних типів на льоту? Отже, ми могли б зберегти той самий синтаксис, а не вводити ще один?
Томас Ейд

@ThomasEyde - саме те, що вони робили останні 15 років. Ось так додалися тілесні члени виразів і тепер цінують кортежі. Класи Запис в стилі ледь не обрізаний в серпні (9 місяців до цього коментаря) для цієї версії C # 7, і, ймовірно , вийде в C # 8. Крім того, зверніть увагу , що значення кортежі пропонують рівність значень , де набили оскому класи не . Якщо все це було представлено ще в 2002 році, це потребує
президентства

що ви маєте в виду convenient not to make a custom class. Ви маєте на увазі створення примірника класу користувача, використовуючи =new..()?
Алекс

75

За допомогою кортежів ви можете легко реалізувати двовимірний словник (або n-мірний з цього приводу). Наприклад, ви можете використовувати такий словник для здійснення відображення обміну валют:

var forex = new Dictionary<Tuple<string, string>, decimal>();
forex.Add(Tuple.Create("USD", "EUR"), 0.74850m); // 1 USD = 0.74850 EUR
forex.Add(Tuple.Create("USD", "GBP"), 0.64128m);
forex.Add(Tuple.Create("EUR", "USD"), 1.33635m);
forex.Add(Tuple.Create("EUR", "GBP"), 0.85677m);
forex.Add(Tuple.Create("GBP", "USD"), 1.55938m);
forex.Add(Tuple.Create("GBP", "EUR"), 1.16717m);
forex.Add(Tuple.Create("USD", "USD"), 1.00000m);
forex.Add(Tuple.Create("EUR", "EUR"), 1.00000m);
forex.Add(Tuple.Create("GBP", "GBP"), 1.00000m);

decimal result;
result = 35.0m * forex[Tuple.Create("USD", "EUR")]; // USD 35.00 = EUR 26.20
result = 35.0m * forex[Tuple.Create("EUR", "GBP")]; // EUR 35.00 = GBP 29.99
result = 35.0m * forex[Tuple.Create("GBP", "USD")]; // GBP 35.00 = USD 54.58

Я б скоріше використав Enum для скорочень країн. Це можливо?
Зак

1
Звичайно, це повинно працювати без проблем, оскільки перерахунки - це ціннісні типи.
MarioVW

@MarioVW Це також можна досягти за допомогою багатовимірного масиву. Як кортеж має будь-яке значення?
Олексій

26

У журналі MSDN є відмінна стаття, яка розповідає про болі в животі та дизайнерські міркування, що входили в додавання Tuple до BCL. Вибір між типом значення та типом посилання є особливо цікавим.

Як зрозуміло в статті, рушійною силою, що стояла за Tuple, було стільки груп всередині Microsoft, які мали для цього користь, команда F # вперед. Хоча не згадується, я вважаю, що нове "динамічне" ключове слово в C # (і VB.NET) мало щось спільне з ним, а кортежі дуже поширені в динамічних мовах.

Інакше це не особливо перевершує створення власного poco, принаймні, ви можете дати членам краще ім'я.


ОНОВЛЕННЯ: завдяки великій редакції версії C # 7, тепер ви отримаєте набагато більше синтаксичного кохання. Попереднє оголошення в цій публікації в блозі .


23

Ось невеликий приклад - скажімо, у вас є метод, який потребує пошуку ручки користувача та адреси електронної пошти з урахуванням ідентифікатора користувача. Ви завжди можете створити спеціальний клас, який містить ці дані, або використовувати параметр ref / out для цих даних, або ви можете просто повернути Tuple і мати гарний підпис методу, не створюючи нового POCO.

public static void Main(string[] args)
{
    int userId = 0;
    Tuple<string, string> userData = GetUserData(userId);
}

public static Tuple<string, string> GetUserData(int userId)
{
    return new Tuple<string, string>("Hello", "World");
}

10
Це хороший приклад, однак не виправдовує використання Tuple.
Амітаб

7
Кортеж - це гідне пристосування, оскільки ви повертаєте чіткі цінності; але кортеж світить більше, коли ви повертаєте кілька значень різних типів .
Марк Rushakoff

21
Ще одним хорошим прикладом може бути int.TryParse, оскільки ви можете усунути вихідний параметр і замість цього використовувати Tuple. Таким чином, ви могли мати Tuple<bool, T> TryParse<T>(string input)і замість того, щоб використовувати вихідний параметр, ви отримуєте обидва значення назад в кортежі.
Tejs

3
насправді саме так відбувається, коли ви викликаєте будь-який метод TryParse з F #.
Джоель Мюллер

23

Я використовував кортеж для вирішення проблеми 11 проекту Euler :

class Grid
{
    public static int[,] Cells = { { 08, 02, 22, // whole grid omitted

    public static IEnumerable<Tuple<int, int, int, int>> ToList()
    {
        // code converts grid to enumeration every possible set of 4 per rules
        // code omitted
    }
}

Тепер я можу вирішити всю проблему за допомогою:

class Program
{
    static void Main(string[] args)
    {
        int product = Grid.ToList().Max(t => t.Item1 * t.Item2 * t.Item3 * t.Item4);
        Console.WriteLine("Maximum product is {0}", product);
    }
}

Я міг би використовувати для цього спеціальний тип, але він виглядав би точно так, як Tuple .


16

Синтаксис кортежу C # 'є смішно громіздким, тому кортежі болісно оголошувати. І у нього немає відповідності шаблонів, тому їх також болісно використовувати.

Але періодично потрібно просто спеціальне групування об'єктів, не створюючи для нього клас. Наприклад, скажімо, що я хотів зібрати список, але хотів два значення замість одного:

// sum and sum of squares at the same time
var x =
    Enumerable.Range(1, 100)
    .Aggregate((acc, x) => Tuple.Create(acc.Item1 + x, acc.Item2 + x * x));

Замість того, щоб поєднувати колекцію значень в єдиний результат, давайте розширимо один результат на колекцію значень. Найпростіший спосіб написати цю функцію:

static IEnumerable<T> Unfold<T, State>(State seed, Func<State, Tuple<T, State>> f)
{
    Tuple<T, State> res;
    while ((res = f(seed)) != null)
    {
        yield return res.Item1;
        seed = res.Item2;
    }
}

fперетворює деякий стан у кортеж. Ми повертаємо перше значення з кортежу і встановлюємо наш новий стан на друге значення. Це дозволяє нам зберігати стан протягом усього обчислення.

Ви використовуєте його як таке:

// return 0, 2, 3, 6, 8
var evens =
    Unfold(0, state => state < 10 ? Tuple.Create(state, state + 2) : null)
    .ToList();

// returns 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
var fibs =
    Unfold(Tuple.Create(0, 1), state => Tuple.Create(state.Item1, Tuple.Create(state.Item2, state.Item1 + state.Item2)))
    .Take(10).ToList();

evensдосить простий, але fibsтрохи розумніший. Це stateнасправді кортеж, який містить фіб (n-2) і fib (n-1) відповідно.


4
+1 Tuple.Create - це зручна стенографія дляnew Tuple<Guid,string,...>
AaronLS

@Juliet Яке використання ключового слова State
irfandar

7

Мені не подобається зловживання ними, оскільки вони виробляють код, який не пояснює сам себе, але вони чудово реалізують сполучені ключі на льоту, оскільки вони реалізують IStructuralEquatable та IStructuralComparable (використовувати як для пошуку, так і замовлення цілей).

І вони поєднують у собі внутрішньо хеш-коди своїх елементів; Наприклад, ось GetHashCode Tuple (взято з ILSpy):

    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer)
    {
        return Tuple.CombineHashCodes(comparer.GetHashCode(this.m_Item1), comparer.GetHashCode(this.m_Item2), comparer.GetHashCode(this.m_Item3));
    }

7

Кортежі чудово підходять для виконання декількох операцій з асинхронічного вводу-виводу одночасно та повернення всіх значень разом. Ось приклади того, як це робити з Tuple та без нього. Кортежі насправді можуть зробити ваш код яснішим!

Без (бридкого гніздування!):

Task.Factory.StartNew(() => data.RetrieveServerNames())
    .ContinueWith(antecedent1 =>
        {
            if (!antecedent1.IsFaulted)
            {
                ServerNames = KeepExistingFilter(ServerNames, antecedent1.Result);
                Task.Factory.StartNew(() => data.RetrieveLogNames())
                    .ContinueWith(antecedent2 =>
                        {
                            if (antecedent2.IsFaulted)
                            {
                                LogNames = KeepExistingFilter(LogNames, antecedent2.Result);
                                Task.Factory.StartNew(() => data.RetrieveEntryTypes())
                                    .ContinueWith(antecedent3 =>
                                        {
                                            if (!antecedent3.IsFaulted)
                                            {
                                                EntryTypes = KeepExistingFilter(EntryTypes, antecedent3.Result);
                                            }
                                        });
                            }
                        });
            }
        });

З кортежем

Task.Factory.StartNew(() =>
    {
        List<string> serverNames = data.RetrieveServerNames();
        List<string> logNames = data.RetrieveLogNames();
        List<string> entryTypes = data.RetrieveEntryTypes();
        return Tuple.Create(serverNames, logNames, entryTypes);
    }).ContinueWith(antecedent =>
        {
            if (!antecedent.IsFaulted)
            {
                ServerNames = KeepExistingFilter(ServerNames, antecedent.Result.Item1);
                LogNames = KeepExistingFilter(LogNames, antecedent.Result.Item2);
                EntryTypes = KeepExistingFilter(EntryTypes, antecedent.Result.Item3);
            }
        });

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


5

Кортежі широко використовуються у функціональних мовах, які можуть робити більше речей з ними, тепер F # є "офіційною" .net мовою, з якою ви хочете взаємодіяти з нею з C # та передавати їх між кодом, написаним двома мовами.


Кортежі також вбудовані у типи для деяких популярних мов скриптів, таких як Python та Ruby (які також мають .Net реалізація для інтероп ... IronPython / Ruby).
MutantNinjaCodeMonkey

5

Я схильний уникати Tupleбільшості сценаріїв, оскільки це шкодить читабельності. Однак Tupleкорисно, коли вам потрібно згрупувати непов'язані дані.

Наприклад, припустимо, у вас є список автомобілів та міст, в яких вони були придбані:

Mercedes, Seattle
Mustang, Denver
Mercedes, Seattle
Porsche, Seattle
Tesla, Seattle
Mercedes, Seattle

Ви хочете зібрати рахунки за кожен автомобіль у кожному місті:

Mercedes, Seattle [3]
Mustang, Denver [1]
Porsche, Seattle [1]
Tesla, Seattle [1]

Для цього ви створюєте Dictionary. У вас є кілька варіантів:

  1. Створіть Dictionary<string, Dictionary<string, int>>.
  2. Створіть Dictionary<CarAndCity, int>.
  3. Створіть Dictionary<Tuple<string, string>, int>.

Читання втрачається при першому варіанті. Це зажадає від вас написати набагато більше коду.

Другий варіант працює і є лаконічним, але машина та місто насправді не пов’язані між собою і, ймовірно, не належать до класу разом.

Третій варіант - лаконічний і чистий. Це добре використовувати Tuple.


4

Кілька прикладів з моєї голови:

  • Місце X та Y (і Z, якщо вам подобається)
  • ширина і висота
  • Все, що вимірюється часом

Наприклад, ви не хочете включати System.Drawing у веб-додаток просто для використання Point / PointF та Size / SizeF.


2
Tupleє настільки ж зручним у критичних ситуаціях, як Pointі SomeVectorможе бути корисним під час виконання графічних операцій. У ній виділяються два значення, дуже пов'язані між собою. Я часто бачу властивості з назвою StartTime та EndTime під час читання коду, але навіть кращий за час та тривалість дати, Tuple змушує вас враховувати обидва значення кожного разу, коли ви працюєте в цій області вашої бізнес-логіки. Або повертаючи "ей, дані змінилися (bool), ось дані (іншого типу)" у важкій обробці, а не через інтенсивні прив'язки.
Леон Пелтьє,

3

Ви повинні бути дуже обережними з використанням Tupleі, ймовірно, подумайте двічі, перш ніж це зробити. З мого попереднього досвіду я з’ясував, що використання Tupleкоду дуже важко читати та підтримувати в майбутньому. Деякий час тому мені довелося виправити якийсь код, де кортежі використовувалися майже скрізь. Замість того, щоб думати про правильні об'єктні моделі, вони просто використовували кортежі. Це був кошмар ... іноді мені хотілося вбити хлопця, який написав код ...

Не хочу говорити, що не варто вживати, Tupleі це зло чи щось таке, і я на сто відсотків впевнений, що є деякі завдання, де Tupleнайкращим кандидатом користується, але, мабуть, вам варто подумати ще раз, чи дійсно вам це потрібно? ?


1

Найкраще застосування для Tuples, яке я знайшов, - коли потрібно повернути більше ніж 1 тип об'єкта з методу, ви знаєте, які типи об'єктів та їх кількість будуть, і це не довгий список.

Інші прості альтернативи можуть використовувати параметр 'out'

private string MyMethod(out object)

або складання словника

Dictionary<objectType1, objectType2>

Однак використання кортежу заощаджує або створення об'єкта 'out', або необхідність ретельного пошуку запису в словнику;


1

Щойно знайшов рішення однієї з моїх проблем у Tuple. Це як декларування класу в області методу, але з ледачим оголошенням назв його полів. Ви оперуєте колекціями кортежів, окремих його примірників, а потім створюєте колекцію анонімного типу з необхідними іменами полів, спираючись на свій кортеж. Це дозволяє уникнути створення нового класу для цієї мети.

Завдання полягає в тому, щоб написати відповідь JSON з LINQ без додаткових класів:

 //I select some roles from my ORM my with subrequest and save results to Tuple list
 var rolesWithUsers = (from role in roles
                       select new Tuple<string, int, int>(
                         role.RoleName, 
                         role.RoleId, 
                         usersInRoles.Where(ur => ur.RoleId == role.RoleId).Count()
                      ));

 //Then I add some new element required element to this collection
 var tempResult = rolesWithUsers.ToList();
 tempResult.Add(new Tuple<string, int, int>(
                        "Empty", 
                         -1,
                         emptyRoleUsers.Count()
                      ));

 //And create a new anonimous class collection, based on my Tuple list
 tempResult.Select(item => new
            {
                GroupName = item.Item1,
                GroupId = item.Item2,
                Count = item.Item3
            });


 //And return it in JSON
 return new JavaScriptSerializer().Serialize(rolesWithUsers);

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


1

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

Зразок коду нижче.

...
...
// calling code.
var userDetails = await GetUserDetails(userId);
Console.WriteLine("Username : {0}", userDetails.Item1);
Console.WriteLine("User Region Id : {0}", userDetails.Item2);
...
...

private async Tuple<string,int> GetUserDetails(int userId)
{
    return new Tuple<string,int>("Amogh",105);
    // Note that I can also use the existing helper method (Tuple.Create).
}

Детальніше про Tuple читайте тут . Сподіваюся, це допомагає.


Я припускаю, що ви не хотіли повертати анонімний тип або масив (або динамічний вміст)
ozzy432836

0

Зміна форм об'єктів, коли вам потрібно надіслати їх по дроту або перейти на інший шар програми, і кілька об'єктів об'єднуються в один:

Приклад:

var customerDetails = new Tuple<Customer, List<Address>>(mainCustomer, new List<Address> {mainCustomerAddress}).ToCustomerDetails();

Метод розширення:

public static CustomerDetails ToCustomerDetails(this Tuple<Website.Customer, List<Website.Address>> customerAndAddress)
    {
        var mainAddress = customerAndAddress.Item2 != null ? customerAndAddress.Item2.SingleOrDefault(o => o.Type == "Main") : null;
        var customerDetails = new CustomerDetails
        {
            FirstName = customerAndAddress.Item1.Name,
            LastName = customerAndAddress.Item1.Surname,
            Title = customerAndAddress.Item1.Title,
            Dob = customerAndAddress.Item1.Dob,
            EmailAddress = customerAndAddress.Item1.Email,
            Gender = customerAndAddress.Item1.Gender,
            PrimaryPhoneNo = string.Format("{0}", customerAndAddress.Item1.Phone)
        };

        if (mainAddress != null)
        {
            customerDetails.AddressLine1 =
                !string.IsNullOrWhiteSpace(mainAddress.HouseName)
                    ? mainAddress.HouseName
                    : mainAddress.HouseNumber;
            customerDetails.AddressLine2 =
                !string.IsNullOrWhiteSpace(mainAddress.Street)
                    ? mainAddress.Street
                    : null;
            customerDetails.AddressLine3 =
                !string.IsNullOrWhiteSpace(mainAddress.Town) ? mainAddress.Town : null;
            customerDetails.AddressLine4 =
                !string.IsNullOrWhiteSpace(mainAddress.County)
                    ? mainAddress.County
                    : null;
            customerDetails.PostCode = mainAddress.PostCode;
        }
...
        return customerDetails;
    }

0

Параметр "out" є чудовим, коли є лише кілька значень, які потрібно повернути, але коли ви почнете стикатися з 4, 5, 6 або більше значеннями, які потрібно повернути, вони можуть стати непростими. Іншим варіантом повернення декількох значень є створення та повернення визначеного користувачем класу / структури або використання Tuple для упаковки всіх значень, які потрібно повернути методом.

Перший варіант, що використовує клас / структуру для повернення значень, є простим. Просто створіть тип (у цьому прикладі це структура) так:

public struct Dimensions
{
public int Height;
public int Width;
public int Depth;
}

Другий варіант, використовуючи Tuple, - це ще більш елегантне рішення, ніж використання об'єкта, визначеного користувачем. Кортеж може бути створений для розміщення будь-якої кількості значень різних типів. Крім того, дані, які ви зберігаєте в кортежі, незмінні; щойно ви додаєте дані в Tuple через конструктор або статичний метод Create, ці дані неможливо змінити. Кортежі можуть приймати до восьми окремих значень і включати їх. Якщо вам потрібно повернути більше восьми значень, вам потрібно буде використовувати спеціальний клас Tuple: Класс Tuple При створенні кортежу з більш ніж вісьмома значеннями ви не можете використовувати статичний метод Create - замість цього потрібно використовувати конструктор класу. Ось як би ви створили кортеж з 10 цілих значень:

var values = new Tuple<int, int, int, int, int, int, int, Tuple<int, int, int>> (
1, 2, 3, 4, 5, 6, 7, new Tuple<int, int, int> (8, 9, 10));

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


0

Тільки для прототипування - кортежі безглузді. Їм зручно користуватися, але це лише ярлик! Для прототипів - прекрасно. Просто не забудьте видалити цей код пізніше.

Це легко писати, важко читати. Він не має видимих ​​переваг перед класами, внутрішніми класами, анонімними класами тощо.


0

Ну, я спробував 3 способи вирішити ту саму проблему в C # 7, і я знайшов випадок використання для Tuples.

Робота з динамічними даними у веб-проектах іноді може створювати біль при картографуванні тощо.

Мені подобається, як Tuple просто автоматично нанесла на item1, item2, itemN, який здається мені більш надійним, ніж використання індексів масиву, де ви можете потрапити на предмет індексу або використовувати анонімний тип, де ви можете неправильно написати ім’я власності.

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

using System;

namespace Playground
{
    class Program
    {
        static void Main(string[] args)
        {
            var tuple = GetTuple();
            Console.WriteLine(tuple.Item1);
            Console.WriteLine(tuple.Item2);
            Console.WriteLine(tuple.Item3);
            Console.WriteLine(tuple);

            Console.WriteLine("---");

            var dyn = GetDynamic();
            Console.WriteLine(dyn.First);
            Console.WriteLine(dyn.Last);
            Console.WriteLine(dyn.Age);
            Console.WriteLine(dyn);

            Console.WriteLine("---");

            var arr = GetArray();
            Console.WriteLine(arr[0]);
            Console.WriteLine(arr[1]);
            Console.WriteLine(arr[2]);
            Console.WriteLine(arr);

            Console.Read();

            (string, string, int) GetTuple()
            {
                return ("John", "Connor", 1);
            }

            dynamic GetDynamic()
            {
                return new { First = "John", Last = "Connor", Age = 1 };
            }

            dynamic[] GetArray()
            {
                return new dynamic[] { "John", "Connor", 1 };
            }
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.