Повертаючи два значення, Tuple проти 'out' проти 'struct'


86

Розглянемо функцію, яка повертає два значення. Ми можемо написати:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

Яка з них є найкращою практикою і чому?


Рядок не є типом значення. Я думаю, ви хотіли сказати "розглянемо функцію, яка повертає два значення".
Ерік Ліпперт,

@ Ерік: Ви праві. Я мав на увазі незмінні типи.
Xaqron,

а що поганого в класі?
Лукаш Мадон,

1
@lukas: Нічого, але, звичайно, це не найкраща практика. Це полегшене значення (<16 КБ), і якщо я хочу додати власний код, я піду, structяк Ericуже згадувалося.
Xaqron,

1
Я б сказав, що використовуйте лише тоді, коли вам потрібно значення повернення, щоб вирішити, чи варто взагалі обробляти дані повернення, як у TryParse, інакше ви завжди повинні повертати структурований об'єкт, наприклад, якщо структурований об'єкт повинен мати тип значення або посилання тип залежить від того, яке додаткове використання даних ви використовуєте
MikeT

Відповіді:


93

У кожного з них є свої плюси і мінуси.

Вихідні параметри швидкі та дешеві, але вимагають, щоб ви передали змінну та покладалися на мутацію. Правильно використовувати параметр out із LINQ практично неможливо.

Кортежі чинять тиск на збір сміття і не самодокументуються. "Елемент1" не дуже описовий.

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

Я був би схильний до спеціального рішення структури за інших рівних умов. Ще краще, хоча це зробити функцію, яка повертає лише одне значення . Чому ви спочатку повертаєте два значення?

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


2
Повернення двох значень часто замінює відсутність типів опцій або ADT.
Антон Тихий,

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

23
@Xaqron: Якщо ви виявите, що ідея "даних із таймаутом" є загальноприйнятою у вашій програмі, тоді ви можете розглянути загальний тип "TimeLimited <T>", щоб ваш метод міг повернути TimeLimited <string> або TimeLimited <Uri> або що завгодно. Клас TimeLimited <T> тоді може мати допоміжні методи, які повідомляють вам "скільки часу нам залишилось?" або "термін дії закінчився?" або що завгодно. Спробуйте захопити таку цікаву семантику, як ця, у системі типів.
Ерік Ліпперт,

3
Абсолютно, я ніколи не використовував би Tuple як частину загальнодоступного інтерфейсу. Але навіть для "приватного" коду я отримую величезну читабельність від належного типу, замість використання Tuple (особливо наскільки легко створити приватний внутрішній тип за допомогою Auto Properties).
SolutionYogi

2
Що означає тиск на збір?
котиться

25

Додаючи до попередніх відповідей, C # 7 пропонує кортежі типу значення, на відміну від того, System.Tupleщо є посилальним типом, а також пропонує покращену семантику.

Ви все ще можете залишити їх без імені та використовувати .Item*синтаксис:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Але що насправді є потужним у цій новій функції - це здатність називати кортежі. Тож ми могли б переписати вищенаведене так:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

Також підтримується деструктуризація:

(string firstName, string lastName, int age) = getPerson()


2
Чи правильно я вважаю, що це повертає в основному структуру з посиланнями як членами під капотом?
Austin_Anderson

4
чи знаємо ми, як ефективність цього порівняння з використанням параметрів?
SpaceMonkey

20

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

Наприклад, TryParseметоди беруть outпараметр для прийняття проаналізованого значення та повертають a, boolщоб вказати, чи пройшов синтаксичний аналіз. Ці два значення насправді не належать разом, тому, семантично, це має більше сенсу, а намір коду легше читати, використовувати outпараметр.

Якщо, однак, ваша функція повертає координати X / Y якогось об’єкта на екрані, тоді ці два значення семантично належать разом, і було б краще використовувати a struct.

Я особисто уникаю використання а tupleдля будь-чого, що буде видно зовнішньому коду через незручний синтаксис для отримання членів.


+1 для семантики. Ваша відповідь є більш доречною з посиланням на типи, коли ми можемо залишити outпараметр null. Існує кілька незмінних незмінних типів.
Xaqron,

3
Насправді, два значення в TryParse дуже багато належать разом, набагато більше, ніж передбачається, коли одне є повернутим значенням, а друге - параметром ByRef. Багато в чому логічним, що повертається, буде нульовий тип. Є деякі випадки, коли шаблон TryParse працює приємно, а в деяких випадках це болісно (приємно, що його можна використовувати в операторі "якщо", але є багато випадків, коли повертається значення, що допускає значення, що обнуляється, або можливість вказати значення за замовчуванням було б зручніше).
supercat

@supercat, я погодився б з andrew, вони не належать разом. хоча вони пов’язані, повернення повідомляє, чи потрібно взагалі турбуватися про значення, а не про те, що потрібно обробляти в тандемі. отже, після того, як ви обробили повернення, яке більше не потрібно для будь-якої іншої обробки, що стосується значення out, це відрізняється від вимоги повернення KeyValuePair зі словника, де існує чіткий і постійний зв'язок між ключем і значенням. хоча я погоджуюсь, якщо типи, що мають дозвіл, були в .Net 1.1, вони, мабуть, використовували б їх, оскільки null був би правильним способом позначити відсутність значення
MikeT

@MikeT: Я вважаю надзвичайно прикро, що корпорація Майкрософт запропонувала використовувати структури лише для речей, що представляють одне значення, хоча насправді структура відкритого поля є ідеальним середовищем для передачі групи незалежних змінних, пов'язаних між собою скотчем . Показник успіху та значення мають значення разом із моментом їх повернення , навіть якщо після цього вони були б більш корисними як окремі змінні. Поля структури відкритого поля, що зберігаються у змінній, самі по собі використовуються як окремі змінні. У будь-якому випадку ...
supercat

@MikeT: Через те, як коваріація підтримується і не підтримується в рамках, єдиним tryшаблоном, який працює з коваріантними інтерфейсами, є T TryGetValue(whatever, out bool success); цей підхід дозволив би інтерфейси IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>і дозволив коду, який хоче зіставити екземпляри з Animalекземплярами, Carщоб прийняти Dictionary<Cat, ToyotaCar>[using TryGetValue<TKey>(TKey key, out bool success). Таке відхилення неможливе, якщо TValueвоно використовується як refпараметр.
supercat

2

Я піду на підхід до використання параметра Out, оскільки при другому підході вам потрібно буде створити і об’єкт класу Tuple, а потім додати до нього значення, що, на мою думку, є дорогою операцією порівняно з поверненням значення у параметрі out. Хоча, якщо ви хочете повернути кілька значень у класі Tuple (що фактично неможливо досягти, просто повернувши один параметр out), тоді я піду на другий підхід.


Я згоден out. Крім того, є paramsключове слово, про яке я не згадував, щоб зберегти відповідь на запитання.
Xaqron

2

Ви не згадали ще один варіант, який має власний клас замість struct. Якщо дані мають семантику, пов’язану з ними, яка може працювати з функціями, або розмір екземпляра досить великий (> 16 байт, як правило), може бути кращим спеціальний клас. Використання "out" не рекомендується в загальнодоступному API через його прив'язку до покажчиків і вимагає розуміння того, як працюють посилальні типи.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Кортеж добре підходить для внутрішнього використання, але використання його в публічному API незручне. Отже, я голосую між struct та class для загальнодоступного API.


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

0

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


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