何时使用in、ref和out?

415

有人问我,什么情况下应该使用out参数关键字而不是ref。虽然我(我想)理解了refout关键字之间的区别(在之前已被问过),最好的解释似乎是ref == inout,但什么情况下我应该始终使用out而不是ref呢?请提供一些(假设或代码)示例。

由于ref更为通用,那么为什么还要使用out呢?这只是一种语法糖吗?


20
使用 out 传递的变量在分配之前无法读取,而 ref 没有此限制。就是这样。 - Corey Ogburn
20
简而言之,ref 是用于输入/输出参数,而 out 则是仅输出参数。 - Tim S.
3
你到底哪一部分不理解? - tnw
4
函数中的 out 变量必须被赋值。 - Corey Ogburn
显示剩余7条评论
17个回答

416

如果你不需要ref,应该使用out

当数据需要进行编组(例如传递到另一个进程)时,使用out和使用ref会有很大的区别,这可能是代价高昂的。因此,当方法不使用初始值时,您应该避免编组初始值。

除此之外,它还可以向声明或调用的读者显示初始值是否相关(并且可能被保留)或被丢弃。

作为一个次要的差异,out参数不需要初始化。

out的示例:

string a, b;
person.GetBothNames(out a, out b);

GetBothNames是一个同时检索两个值的方法,无论a和b是什么,该方法都不会改变其行为。如果调用需要到夏威夷的服务器,将初始值从此处复制到夏威夷将浪费带宽。使用ref的类似代码段:

string a = String.Empty, b = String.Empty;
person.GetBothNames(ref a, ref b);

这可能会使读者困惑,因为它看起来好像a和b的初始值是相关的(尽管方法名表明它们并不相关)。

ref的示例:

string name = textbox.Text;
bool didModify = validator.SuggestValidName(ref name);

这里的初始值与方法相关。


5
"那并不完全是事实。" - 请问您能更好地解释一下您的意思吗? - peterchen
3
不要在默认值中使用 ref 关键字。 - C.Evenhuis
168
为了后人留存记录:另外一个没有被其他人提到的区别,正如在这里所述;对于一个out参数,“调用方法必须在方法返回之前分配一个值” - 你不必对引用参数做任何事情。 - brichins
3
请参考您提供的链接中的“注释(社区补充)”部分。这是VS 2008文档中已经修正的错误。 - Bharat Ram V
14
@brichins所说的是在called方法中需要指定数值,而不是在调用方法中。zverev.eugene在VS 2008文档中做了修正。 - Segfault
显示剩余6条评论

78

使用 "out" 关键字表示该参数未被使用,仅被设置。这有助于调用者理解您始终在初始化参数。

另外,“ref”和“out”不仅适用于值类型。它们还允许您在方法内重置引用类型所引用的对象。


3
+1 我不知道它也可以用于引用类型,回答清晰明了,谢谢。 - Dale
@brichins:不行。out参数在进入函数时被视为未赋值。在您首先明确分配某个值之前,您将无法检查它们的值 - 没有任何方法可以使用函数调用时参数所具有的值。 - Ben Voigt
真的,您无法在内部分配之前访问该值。我指的是参数本身可以稍后在方法中使用-它没有被锁定。是否实际应该这样做是另一种讨论(关于设计); 我只是想指出它是可能的。感谢澄清。 - brichins
2
@ดาว:它可以与引用类型一起使用,因为当您传递引用类型参数时,传递的是引用的值而不是对象本身。因此仍然是按值传递。 - Tarik

41

你说得对,语义上,ref 提供了“输入”和“输出”功能,而 out 只提供了“输出”功能。需要考虑以下几点:

  1. out 要求接受参数的方法必须在返回之前某个时刻为变量赋值。您会在一些键/值数据存储类(如 Dictionary<K,V>)中找到此模式,其中有像 TryGetValue 这样的函数。该函数将使用 out 参数来保存检索到的值。调用者传递一个值进入这个函数是没有意义的,因此使用 out 来保证在调用后某个时刻变量中将有一些值,即使它不是“真实”的数据(在 TryGetValue 情况下,键不存在)。
  2. outref 参数在处理互操作代码时进行不同的编排。
此外,需要注意的是,尽管引用类型和值类型在其值的本质上有所不同,但是你应用程序中的每个变量都指向保存一个值的内存位置,即使对于引用类型也是如此。只是发生了这样一种情况,对于引用类型,内存位置中包含的值是另一个内存位置。将值传递给函数(或执行任何其他变量赋值)时,该变量的值会被复制到其他变量中。对于值类型,这意味着类型的整个内容都被复制了。对于引用类型,这意味着内存位置被复制了。无论哪种方式,都会创建一个包含变量中所包含数据副本的副本。这仅与赋值语义相关; 当分配变量或按值传递变量(默认情况下)时,当对原始(或新的)变量进行新分配时,它不会影响其他变量。对于引用类型,是的,对实例所做的更改在两边都可用,但这是因为实际变量只是指向另一个内存位置的指针; 变量的内容-内存位置-实际上并没有更改。
使用ref关键字进行传递表示原始变量和函数参数都会指向同一个内存位置。同样,这仅影响赋值语义。如果对其中一个变量赋予新值,那么由于另一个变量指向相同的内存位置,新值将反映在另一侧。

1
请注意,调用方法分配值给 out 参数的要求是由 C# 编译器强制执行的,而不是底层 IL 强制执行的。因此,使用 VB.NET 编写的库可能不符合该约定。 - jmoreno
听起来 ref 实际上相当于 C++ 中的解引用符号 (*)。在 C# 中的按引用传递必须等同于 C/C++ 中所谓的双指针(指向指针),因此 ref 必须对第一个指针进行解引用,允许被调用的方法访问实际对象在上下文中的内存位置。 - ComeIn
实际上,我建议在未找到键的情况下,正确的“TryGetValue”应明确使用“ref”而不是“out”。 - NetMage

28
这取决于编译上下文(见下面的示例)。 outref都表示通过引用传递变量,但ref要求在传递之前对变量进行初始化,这在封送处理的上下文中可能是一个重要的区别(Interop: UmanagedToManagedTransition或反之亦然)。 MSDN warns
不要将按引用传递的概念与引用类型的概念混淆。这两个概念并不相同。方法参数可以通过ref进行修改,无论它是值类型还是引用类型。当通过引用传递时,没有对值类型进行装箱。
来自官方MSDN文档:

out关键字导致参数通过引用传递。这类似于ref关键字,只是ref要求在传递之前必须对变量进行初始化。

ref关键字使参数通过引用而非值传递。通过引用传递的效果是,在方法中对参数进行任何更改都会反映在调用方法中的基础参数变量中。引用参数的值始终与基础参数变量的值相同。

当参数被分配时,我们可以验证out和ref确实相同:

CIL示例

考虑以下示例

static class outRefTest{
    public static int myfunc(int x){x=0; return x; }
    public static void myfuncOut(out int x){x=0;}
    public static void myfuncRef(ref int x){x=0;}
    public static void myfuncRefEmpty(ref int x){}
    // Define other methods and classes here
}

在CIL中,myfuncOutmyfuncRef的指令与预期相同。
outRefTest.myfunc:
IL_0000:  nop         
IL_0001:  ldc.i4.0    
IL_0002:  starg.s     00 
IL_0004:  ldarg.0     
IL_0005:  stloc.0     
IL_0006:  br.s        IL_0008
IL_0008:  ldloc.0     
IL_0009:  ret         

outRefTest.myfuncOut:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldc.i4.0    
IL_0003:  stind.i4    
IL_0004:  ret         

outRefTest.myfuncRef:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldc.i4.0    
IL_0003:  stind.i4    
IL_0004:  ret         

outRefTest.myfuncRefEmpty:
IL_0000:  nop         
IL_0001:  ret         

nop:无操作,ldloc:加载本地变量,stloc:存储本地变量,ldarg:加载参数,bs.s:跳转到目标....

(参见:CIL指令列表)


25
以下是我从这篇CodeProject文章C# Out Vs Ref中摘录的一些注释:
  1. 它仅在我们期望从函数或方法获得多个输出时使用。对于相同的问题,结构思想也可以是一个不错的选择。
  2. REF和OUT是关键字,它们指示数据如何在调用方和被调用方之间传递。
  3. 在REF中,数据双向传递。从调用方到被调用方,反之亦然。
  4. 在Out中,数据仅从被调用方传递给调用方。在这种情况下,如果调用方试图将数据发送到被调用方,则会被忽略/拒绝。

如果您是一个视觉化的人,请查看此YouTube视频,该视频演示了实际差异https://www.youtube.com/watch?v=lYdcY5zulXA

下面的图片更直观地显示了差异

C# Out Vs Ref


1
“one-way”和“two-way”这两个术语在这里可能被误用。实际上,它们都是双向的,但是它们的概念行为在参数的引用和值上有所不同。 - ibubi

19
如果您计划读取和写入参数,则需要使用ref。如果您只打算写入,则需要使用out。实际上,out用于当您需要多个返回值或者不想使用正常的返回机制进行输出时(但这应该很少发生)。
有一些语言机制可以帮助这些用例。在传递给方法之前,必须初始化ref参数(强调它们是可读写的),out参数在分配值之前不能被读取,并且保证在方法结束时已经被写入(强调它们只能写入)。违反这些原则会导致编译时错误。
int x;
Foo(ref x); // error: x is uninitialized

void Bar(out int x) {}  // error: x was not written to

例如,int.TryParse 返回一个 bool 值,并接受一个 out int 参数:
int value;
if (int.TryParse(numericString, out value))
{
    /* numericString was parsed into value, now do stuff */
}
else
{
    /* numericString couldn't be parsed */
}

这是一个需要输出两个值的明显例子:数值结果和转换是否成功。CLR的作者选择使用out,因为他们不在意之前可能存在的int值。
对于ref,您可以参考Interlocked.Increment
int x = 4;
Interlocked.Increment(ref x);

Interlocked.Increment原子地增加x的值。由于需要读取x才能将其增加,这种情况下使用ref更为适合。你完全关心在传递给Increment之前x的值是多少。

在C#的下一个版本中,甚至可以在out参数中声明变量,进一步强调它们只用于输出:

if (int.TryParse(numericString, out int value))
{
    // 'value' exists and was declared in the `if` statement
}
else
{
    // conversion didn't work, 'value' doesn't exist here
}

1
@RajbirSingh,因为 out 参数并没有被初始化,所以编译器不会让你在向其写入内容之前读取它。 - zneak
zneak,我同意你的观点。但在下面的例子中,out参数可以用作读写:string name = "myName"; private void OutMethod(out string nameOut) { if(nameOut == "myName") { nameOut = "Rajbir Singh in out method"; } } - Rajbir Singh
1
@RajbirSingh,你的示例无法编译。因为在if语句中没有给nameOut赋值,所以你无法读取它。 - zneak
感谢@zneak。您说得完全正确。它不能编译。非常感谢您的帮助,现在我终于明白了 :) - Rajbir Singh
FYI:从http://stackoverflow.com/questions/20789153/ref-vs-out-parameters-in-c-sharp合并到这里 - Shog9
显示剩余2条评论

11

如何在C#中使用inoutref

  • C#中的所有关键字都具有相同的功能,但存在一些限制
  • in参数不能被调用方法修改。
  • ref参数可以被修改。
  • ref必须在被调用前被初始化,它可以在方法中被读取和更新。
  • out参数必须由调用者进行修改。
  • out参数必须在方法中进行初始化。
  • 作为in参数传递的变量在传递到方法调用之前必须进行初始化。但是,被调用的方法可能不会分配值或修改该参数。

您不能将inrefout关键字用于以下类型的方法:

  • 异步方法,这是通过使用async修饰符定义的方法。
  • 迭代器方法,包括yield returnyield break语句。

“输出参数必须由调用者修改。”- 我认为这并不完全正确。请参阅 https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier。它只指定被调用的方法需要向输出变量分配一个值。 - emilaz

10

如果你还需要一个好的总结,这就是我想到的。

总结

当我们在函数内部时,这是如何指定变量数据访问控制的:

in = R

out = 必须先写入再读取

ref = R+W


解释

in

函数只能读取该变量。

out

变量不能先初始化,因为函数必须先写入它,然后才能读取它。

ref

函数可以对该变量进行读/写操作。


为什么会有这个名称?

关注数据何时被修改:

in

数据必须在进入(“in”)函数之前设置。

out

数据必须在离开(“out”)函数之前设置。

ref

数据必须在进入(“in”)函数之前设置。
数据可以在离开(“out”)函数之前设置。


也许(in/out/ref)应该改名为(r/wr/rw)。或者,也许不用改,in/out是一个更好的隐喻。 - tinker

7

outref 的更严格版本。

在方法体中,你需要在离开方法之前为所有的 out 参数赋值。 同时,对于赋给 out 参数的值是被忽略的,而 ref 要求它们被赋值。

因此,out 允许你做到:

int a, b, c = foo(out a, out b);

在这里,ref需要a和b被分配。


如果说有什么区别的话,“out”是更不受限制的版本。ref有“前置条件:变量一定被分配,后置条件:变量一定被分配”,而out只有“后置条件:变量一定被分配”。(并且如预期的那样,具有较少前置条件的函数实现需要更多的要求) - Ben Voigt
@BenVoigt:我猜这取决于你看问题的角度 :) 我想我是指编码灵活性方面的约束(?)。 - leppie

7

听起来是这样的:

out = 仅初始化/填充参数(参数必须为空),并返回一个 out 明文

ref = 引用,标准参数(可能带有值),但函数可以修改它。


输出参数变量可以在传递给方法之前获得值。 - Bence Végert

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