Возвращает два значения: Tuple vs 'out' vs '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
источник
Строка не является типом значения. Я думаю, вы хотели сказать «рассмотрите функцию, которая возвращает два значения».
Эрик Липперт
@ Эрик: Ты прав. Я имел в виду неизменяемые типы.
Xaqron
а что не так с классом?
Лукаш Мадон
1
@lukas: Ничего, но, конечно, это не лучший опыт. Это легкое значение (<16 KB) и если я собирающийся добавить пользовательский код, я пойду с , structкак Ericупоминались.
Xaqron
1
Я бы сказал, используйте только тогда, когда вам нужно возвращаемое значение, чтобы решить, следует ли вообще обрабатывать возвращаемые данные, как в TryParse, в противном случае вы всегда должны возвращать структурированный объект, поскольку если структурированный объект должен быть типом значения или ссылкой Тип зависит от того, какое дополнительное использование данных вы используете
MikeT

Ответы:

93

У каждого из них есть свои плюсы и минусы.

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

Кортежи создают давление при сборке мусора и не являются самодокументированными. "Item1" не очень информативен.

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

Я был бы склонен к индивидуальному структурному решению при прочих равных условиях. Еще лучше создать функцию, которая возвращает только одно значение . Почему вы вообще возвращаете два значения?

ОБНОВЛЕНИЕ: обратите внимание, что кортежи в 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
мы знаем, как производительность этого сравнивается с использованием параметров out?
SpaceMonkey
20

Я думаю, что ответ зависит от семантики того, что делает функция, и отношения между двумя значениями.

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

Если, однако, ваша функция возвращает координаты X / Y некоторого объекта на экране, тогда два значения семантически принадлежат друг другу, и было бы лучше использовать struct.

Лично я бы избегал использовать a tupleдля всего, что будет видно внешнему коду из-за неудобного синтаксиса для получения членов.

Эндрю Купер
источник
+1 за семантику. Ваш ответ будет более подходящим с ссылочными типами, когда мы можем оставить outпараметр null. Есть несколько неизменяемых типов, допускающих значение NULL.
Xaqron
3
Фактически, два значения в TryParse в значительной степени связаны друг с другом, гораздо больше, чем подразумевается, имея одно в качестве возвращаемого значения, а другое в качестве параметра ByRef. Во многих отношениях логичным будет возвращать тип, допускающий значение NULL. В некоторых случаях шаблон TryParse работает хорошо, а в некоторых - неудобно (приятно, что его можно использовать в операторе «if», но есть много случаев, когда возвращается значение, допускающее значение NULL, или когда можно указать значение по умолчанию. было бы удобнее).
supercat
@supercat Я бы согласился с Эндрю, они не принадлежат друг другу. хотя они связаны, return сообщает вам, нужно ли вам вообще беспокоиться о значении, а не о том, что нужно обрабатывать в тандеме. поэтому после того, как вы обработали возврат, он больше не требуется для какой-либо другой обработки, относящейся к выходному значению, это отличается от возврата KeyValuePair из словаря, где есть четкая и постоянная связь между ключом и значением. хотя я согласен, что если бы типы, допускающие значение NULL, были в .Net 1.1, они, вероятно, использовали бы их, поскольку null был бы правильным способом
пометить
@MikeT: Я считаю исключительно неудачным, что Microsoft предложила использовать структуры только для вещей, представляющих одно значение, тогда как на самом деле структура открытого поля является идеальной средой для передачи вместе группы независимых переменных, связанных вместе изолентой. . Индикатор успеха и значение значимы вместе в момент их возврата , даже если после этого они были бы более полезны как отдельные переменные. Поля структуры открытых полей, хранящиеся в переменной, сами по себе могут использоваться как отдельные переменные. В любом случае ...
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. Хотя, если вы хотите вернуть несколько значений в классе кортежей (что на самом деле не может быть достигнуто путем простого возврата одного параметра), я выберу второй подход.

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

Вы не упомянули еще один вариант, который имеет собственный класс вместо структуры. Если данные имеют связанную с ними семантику, с которой могут работать функции, или если размер экземпляра достаточно велик (как правило,> 16 байт), предпочтительным может быть специальный класс. Использование out не рекомендуется в общедоступном API, поскольку оно связано с указателями и требует понимания того, как работают ссылочные типы.

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

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

ЖанП
источник
1
Если тип существует исключительно с целью возврата агрегирования значений, я бы сказал, что простой тип значения открытого поля лучше всего подходит для этой семантики. Если в типе нет ничего, кроме его полей, не возникнет вопросов о том, какие виды проверки данных он выполняет (очевидно, нет), представляет ли он захваченное или живое представление (структура с открытым полем не может действовать. в режиме реального времени) и т. д. Неизменяемые классы менее удобны для работы и дают преимущество в производительности только в том случае, если экземпляры могут передаваться несколько раз.
supercat
0

Не существует «лучшей практики». Это то, что вам удобно и что лучше всего работает в вашей ситуации. Пока вы согласны с этим, нет никаких проблем ни с одним из опубликованных вами решений.

хитрый
источник
3
Конечно, все они работают. Если нет технических преимуществ, мне все еще любопытно узнать, что в основном используется экспертами.
Xaqron