何时应该使用输出参数?

61

我不理解何时应该使用输出参数,如果我需要返回多个类型的结果,我会将其包裹在一个新的类型中,我发现这比 out 更容易处理。

我看到过像这样的方法:

   public void Do(int arg1, int arg2, out int result)

是否有任何情况下实际上这样做是有意义的?

TryParse呢?为什么不返回一个ParseResult类型呢?或者在较新的框架中返回可空类型呢?


1
如果你不喜欢TryParse,你可以写一个包装器。 - tuinstoel
顺便提一下,还有一个Parse方法,它没有输出参数,只返回一个值。如果字符串无法转换为该类型,则会抛出异常。 - devuxer
3
使用单个输出参数并具有void返回类型是没有意义的,不推荐。但是,可以参考一些答案中提到的良好用例。 - Thorarin
1
即使是微软公司自己也建议不要使用“out”参数。https://msdn.microsoft.com/en-us/library/ms182131.aspx 从本质上讲,“out”是一种返回多个值而不仅仅是一个值的方法。但是,对于一个函数来说,返回一个包含需要返回的所有内容的结构体或类更加清晰和模块化。 - levininja
10个回答

33

如果你有一个名为TryNNN的函数,并且明确了即使该函数不成功,输出参数也将始终被设置,那么使用 out 关键字是很好的选择。这允许你信赖你声明的局部变量将被设置,而不必在代码后面放置对 null 的检查(下面的评论指出该参数可能会被设置为null,因此你可能需要验证你调用的函数的文档是否确实如此)。它使代码更清晰易读。另一种情况是当你需要返回一些数据以及方法条件的状态时:

public bool DoSomething(int arg1, out string result);
在这种情况下,返回值可以表明函数是否成功,并且结果存储在输出参数中。诚然,这个例子有些牵强,因为你可以设计一种方式,使函数简单地返回一个字符串,但是你可以理解这个想法。
缺点是你必须声明一个本地变量来使用它们:
string result;
if (DoSomething(5, out result))
    UpdateWithResult(result);

改为:

UpdateWithResult(DoSomething(5));

不过,这甚至可能不是一个缺点,这取决于你想要的设计。在 DateTime 的情况下,提供了两种方法(Parse 和 TryParse)。


实际上,相反的情况更为真实,即在期望调用“失败”后保持输出参数不变方面。在大多数情况下,在将输出参数传递到函数之前不应对其进行初始化。 - Samuel
4
如果函数执行失败,输出参数将不会被更改。除非函数抛出异常,否则输出参数将始终被更改。如果具有输出参数的方法存在不分配值给输出参数的执行路径,则该方法将无法编译。因此,在第二个示例中进行初始化“string result =''”是不必要的。 - Joe
1
塞缪尔说的。在退出方法之前设置一个输出参数是必需的。如果您希望某些情况下的值保持不变,请改用ref而不是out。 - Thorarin
out参数只是为了让调用者不必初始化变量而存在的。方法本身仍然可以将其初始化为null或任何默认值,并以此方式返回。 - Mark Cidade
6
现在你无需再声明本地变量了:if (DoSomething(5, out var result)) { ... } - snowflake

6

我知道回答晚了好几年。 如果您不希望您的方法实例化一个新对象返回,则out(和ref)也非常有用。这在高性能系统中非常重要,因为您希望方法的性能达到亚微秒级别。从内存访问的角度来看,实例化相对昂贵。


6

好的,就像大多数事情一样,这取决于情况。 让我们看看选择:

  • 您可以返回函数中想要返回的任何值
  • 如果您想要返回多个值或者函数已经有了返回值,您可以使用 out 参数或创建一个新的复合类型将所有这些值公开为属性。

对于 TryParse,使用 out 参数是高效的——您不需要创建一个新类型,这会产生 16B 的开销(在 32b 机器上)或者在调用后承担它们垃圾回收的性能成本。例如,TryParse 可以从循环内部调用——因此,在这里 out 参数统治。
对于那些不会在循环内部调用的函数(即性能不是主要关注点),返回单个复合对象可能会更加“干净”(这取决于观察者的主观判断)。现在有匿名类型和动态类型,这使得它甚至变得更容易了。

注意:

  1. out 参数有一些需要遵循的规则,即编译器将确保函数在退出之前初始化该值。因此,TryParse 必须将 out 参数设置为某个值,即使解析操作失败。
  2. TryXXX 模式是使用 out 参数的一个很好的例子——Int32.TryParse 是由于人们抱怨捕获异常以知道解析是否失败而产生的性能损失。另外,在情况解析成功时,您最有可能要做的事情是获取解析后的值——使用 out 参数意味着您不必再调用 Parse 来获取。

我认为没有简单的方法来返回匿名类型,对吗? - chikak
在C#4.0之前,问题在于使用这些匿名类型返回值。方法可以将匿名对象作为“object”返回,但是调用者无法执行returnValue.FirstEmbeddedValue,即使它存在于对象中。需要动态方法解析才能使其正常工作。 - Gishu
@Gishu - 复合类型和类是同一件事吗? - Radmation
@RadleyAnaya - composite => 由多个事物/值组成。可以是C#类或结构体。 - Gishu

6

我认为out关键字在需要返回布尔值和数值的情况下很有用,比如TryParse方法。但是如果编译器允许以下代码就更好了:

bool isValid = int.TryParse("100", out int result = 0);

我喜欢内联声明,我从未想过那样做。 - Chris S
10
现在可以在C# 7中使用。 - ChaseAucoin

3

当你需要一个方法返回多个值时,out参数就是为此而设计的,在你所发布的示例中就是这种情况:

public void Do(int arg1, int arg2, out int result)

使用输出参数并不是很有意义,因为您只返回一个值,如果删除输出参数并将其替换为 int 返回值,则可以更好地使用该方法:

public int Do(int arg1, int arg2)

关于out参数,有一些好处:

  1. 输出参数最初被视为未赋值状态。
    • 每个out参数在方法返回之前必须被明确赋值,如果您遗漏了赋值,代码将无法编译。

总的来说,在我的私有API中,我基本上尝试使用out参数来避免创建包含多个返回值的单独类型,在我的公共API中,我只在与TryParse模式匹配的方法中使用它们。


1

是的,这是有道理的。以这个为例。

String strNum = "-1";
Int32 outNum;

if (Int32.TryParse(strNum, out outNum)) {
    // success
}
else {
    // fail
}

如果在具有返回值的正常函数中操作失败,你可以返回什么?你肯定不能返回 -1 来表示失败,因为这样就无法区分失败返回值和最初解析的实际值。这就是为什么我们返回一个布尔值来查看是否成功,如果成功,那么我们已经安全地分配了我们的“返回”值。

你可以返回一个不存在的类型 ParsedResult<T>:{ bool Success {get;},T Result {get;} }。 - Rune FS
1
真的,但这需要额外的开销来创建返回值类型,而实际上并不需要。TryParse已经具备了您在此场景中所需的所有功能。 - David Anderson

0

如果您总是创建一个类型,那么您的应用程序中可能会有很多混乱。

正如在这里所说,一个典型的用例是一个TrySomething方法,您希望返回一个布尔值作为成功的指示器,然后是实际值。我也发现在if语句中更加清晰 - 所有三个选项大致具有相同的LOC。

int myoutvalue;
if(int.TryParse("213",out myoutvalue){
    DoSomethingWith(myoutvalue);
}

vs.

ParseResult<int> myoutvalue = int.TryParse("213");
if ( myoutvalue.Success ) {
    DoSomethingWith(myoutvalue.Value);
}

vs.

int? myoutvalue = int.TryParse("213");
if(myoutvalue.HasValue){
    DoSomethingWith(myoutvalue.Value);
}

关于“为什么不返回可空类型”:TryParse 存在自 Framework 1.x,而可空类型从 2.0 开始引入(因为它们需要泛型)。所以为什么要不必要地破坏兼容性或开始在某些类型的 TryParse 中引入不一致性呢?你可以编写自己的扩展方法来复制已经存在的功能(请参见一个涉及到做/不做某些事情的无关主题的 Eric Lipperts 文章)。
另一个用例是如果你必须返回多个无关的值,即使这样做可能会触发警报,表示你的方法可能做了太多的事情。另一方面,如果你的方法类似于昂贵的数据库或 Web 服务调用,而你想要缓存结果,那么这可能是有意义的。当然,你可以创建一个类型,但这意味着你的应用程序中又多了一个类型。

0

专门为返回值创建一个类型对我来说有点痛苦 :-) 首先,我必须创建一个用于返回值的类型,然后在调用方法中,我必须将返回类型中的值分配给需要它的实际变量。

输出参数使用起来更简单。


0

我很烦恼的是,我不能将null传递给TryParse函数的out参数。

尽管如此,在某些情况下,我更喜欢它返回一个具有两个数据部分的新类型。特别是当它们大多数情况下不相关或其中一个部分仅在一段时间后需要进行单个操作时。当我确实需要保存TryParse函数的结果值时,我真的很喜欢使用out参数,而不是一些随机的ResultAndValue类来处理。


-2
有时我会使用输出参数来提高可读性,尤其是对于那些除了返回结果之外还执行命令的方法,阅读方法名称比阅读方法输出更为重要。
StatusInfo a, b, c;

Initialize(out a);
Validate(a, out b);
Process(b, out c);

对比

StatusInfo a = Initialize();
StatusInfo b = Validate(a);
StatusInfo c = Process(b);

对我来说,当我在扫描代码时,我会非常注重每行的前几个字符。在第一个示例中,我可以很容易地看出声明了一些“StatusInfo”变量后发生了什么。而在第二个示例中,我首先看到的是检索了一堆StatusInfo。我必须再次扫描才能看到这些方法可能产生的影响。

6
实际上,我认为第一个看起来更差! - kay.one
我将名称更改为更接近真实世界代码的内容。如果这仍然不好,我可以找到一个实际的例子。 - Mark Cidade
2
在第二个中,编译器将验证您不会按错误顺序执行它们。但是在第一个中则不然。 - Mitch Blevins
如果是这种情况,那么这是一个糟糕的使用 out 参数的例子...我认为第二个更容易阅读和理解。虽然第一个打字较少。 - Radmation

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接