返回两个值,元组与“输出”与“结构”


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)

哪个是最佳做法,为什么?


字符串不是值类型。我认为您的意思是说“考虑一个返回两个值的函数”。
埃里克·利珀特

@Eric:对。我的意思是不可变的类型。
Xaqron 2011年

上课怎么了?
卢卡斯·马顿

1
@lukas:没什么,但是肯定不是最佳实践。这是一个轻量级的值(<16 KB),如果我要添加自定义代码,我将按照structEric提到的去处理。
Xaqron 2011年

1
我会说仅在需要返回值来决定是否应完全处理返回数据时用完,例如在TryParse中,否则应该始终返回结构化对象,就像结构化对象应该是值类型还是引用类型取决于您对数据有何额外使用
MikeT

Answers:


93

他们每个人都有自己的优点和缺点。

输出参数既快速又便宜,但是需要您传入一个变量,并依赖突变。几乎不可能在LINQ中正确使用out参数。

元组造成垃圾收集压力,并且无法自我记录。“ Item1”不是很描述。

如果自定义结构很大,则复制起来可能会很慢,但如果它们很小,则可以自我记录,并且效率很高。但是,定义一堆用于琐碎用途的自定义结构也很麻烦。

在其他所有条件相同的情况下,我倾向于定制结构解决方案。更好的是制作一个仅返回一个值的函数。为什么首先要返回两个值?

更新:请注意,在撰写本文后六年,C#7中的元组是值类型,因此不太可能产生收集压力。


2
返回两个值通常可以替代不具有选项类型或ADT。
2011年

2
根据我使用其他语言的经验,我会说通常将元组用于项目的快速和肮脏分组。通常最好仅创建类或结构,因为它允许您命名每个项目。使用元组时,很难确定每个值的含义。但这确实使您免于花费时间来创建类/结构,如果该类/结构不会在其他地方使用,则可能会显得过分杀伤。
凯文·卡斯卡特

23
@Xaqron:如果您发现程序中普遍存在“超时数据”的概念,则可以考虑使用通用类型“ TimeLimited <T>”,这样您的方法就可以返回TimeLimited <string>或TimeLimited <Uri>或其他。然后,TimeLimited <T>类可以具有帮助程序方法,该方法告诉您“我们还剩下多久?” 或“已过期?” 管他呢。尝试在类型系统中捕获类似这样的有趣语义。
埃里克·利珀特

3
绝对,我永远不会将Tuple用作公共接口的一部分。但是即使对于“私有”代码,我也可以通过使用适当的类型而不是使用Tuple来获得巨大的可读性(尤其是使用“自动属性”创建私有内部类型是多么容易)。
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参数来接受已解析的值,并返回abool来指示解析是否成功。这两个值并不真正属于同一类,因此,从语义上讲,这更有意义,并且使用out参数更易于阅读代码的意图。

但是,如果您的函数返回屏幕上某个对象的X / Y坐标,则这两个值在语义上属于同一类,因此最好使用struct

我个人会避免使用atuple来对外部代码可见的任何东西,因为检索成员的语法很笨拙。


+1表示语义。当我们可以保留out参数时,您的答案是更合适的引用类型null。有一些可为空的不可变类型。
Xaqron 2011年

3
实际上,TryParse中的两个值确实很在一起,远远超过了以一个作为返回值而另一个作为ByRef参数所隐含的含义。在许多方面,要返回的逻辑对象将是可为空的类型。在某些情况下,TryParse模式可以很好地工作,而在某些情况下则很痛苦(可以在“ if”语句中使用它很好,但是在许多情况下,返回可为空的值或可以指定默认值)会更方便)。
supercat

@supercat我同意安德鲁(Andrew)的观点,他们不属于一起。尽管它们是相关的,但返回值会告诉您是否根本不需要理会值,而无需一前一后地进行处理。因此,在您处理完返回值后,就不再需要其他与​​out值相关的其他处理,这与说从字典中返回KeyValuePair有所不同,在字典中,键和值之间存在清晰且持续的联系。尽管我同意.net 1.1中是否包含可为空的类型,但他们可能会使用它们,因为null将是标记无值的一种正确方法
MikeT 2013年

@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参数,则不可能有这种差异。
2013年

2

我将使用使用Out参数的方法,因为在第二种方法中,您将需要创建和对象Tuple类,然后向其添加值,与返回out参数的值相比,我认为这是一项昂贵的操作。尽管如果您想在Tuple类中返回多个值(实际上,仅返回一个out参数就无法实现),那么我将采用第二种方法。


我同意out。此外,还有一个params我没有提到的关键字来使问题简单明了。
Xaqron 2011年

2

您没有提到另一个选项,该选项具有自定义类而不是struct。如果数据具有可以通过函数进行操作的语义,或者实例大小足够大(通常大于16个字节),则最好使用自定义类。在公共API中不建议使用“ out”,因为它与指针关联并且需要了解引用类型的工作方式。

https://msdn.microsoft.com/zh-CN/library/ms182131.aspx

元组适合内部使用,但在公共API中使用却很尴尬。因此,对于公共API,我的投票是在struct和class之间。


1
如果纯粹出于返回值集合的目的而存在一个类型,那么我想说一个简单的暴露字段值类型最适合这些语义。如果类型除了其字段之外没有其他内容,那么它将不会执行任何类型的数据验证(显然没有),无论它代表捕获的视图还是实时视图(开放字段结构都无法发挥作用)不可变的类使用起来不太方便,并且只有在实例可以多次传递的情况下才能提供性能上的好处。
超级猫

0

没有“最佳实践”。这是您感到满意的,并且在您的情况下最有效。只要您对此保持一致,您发布的任何解决方案都不会出现问题。


3
当然,它们都可以工作。如果没有技术优势,我仍然很想知道专家最常使用的是什么。
Xaqron 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.