何时使用in vs ref vs out


383

前几天有人问我应该使用parameter关键字out而不是ref。尽管我(我认为)了解了refout关键字(之前已经问过)之间的区别,并且最好的解释似乎是ref== inout,但是我应该始终使用out而不是一些(假设的或代码的)示例ref

由于ref更笼统,您为什么要使用out?只是语法糖吗?


18
使用中传递的变量在out分配给它之前无法读取。ref没有这个限制。就是这样。
Corey Ogburn 2013年

17
简而言之,ref是输入/输出,而out参数是仅输出。
Tim S.

3
你到底没有得到什么?
tnw

4
此外,out变量必须在函数中分配。
科里·奥格本

谢谢科里。但是我已经不是那样了。我的意思是,这样做有什么好处。实际上,我需要一个示例来说明一个场景,其中我们可以使用ref参数来实现使用out参数无法实现的功能,反之亦然。
拉吉尔·辛格

Answers:


399

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

155
对于后代:正如这里所说,似乎没有其他人提到过另一个差异;为out参数,在主叫方法需要该方法返回到之前分配一个值。-你不具备做一个ref参数什么。
brichins 2011年

3
@brichins请参阅您提到的链接中的“评论(社区添加)”部分。VS 2008文档中已纠正此错误。
巴拉特·拉姆

13
@brichins要求被调用的方法分配一个值,而不是调用方法。zverev.eugene,这是VS 2008文档中已更正的内容。
Segfault

72

用完表示该参数未使用,仅设置。这有助于调用者了解您始终在初始化参数。

同样,ref和out不仅适用于值类型。它们还允许您重置方法中引用类型所引用的对象。


3
+1我也不知道它也可以用于引用类型,很好的明确答案,谢谢
Dale

@brichins:不,你不能。 out参数在进入函数时被视为未分配。在您首先明确分配了一些值之前,您将无法检查它们的值-根本无法使用调用函数时参数所具有的值。
Ben Voigt 2013年

是的,您不能在内部分配之前访问该值。我指的是参数本身可以在以后的方法中使用的事实-它未锁定。是否真的应该这样做(在设计上)是另外一个讨论。我只想指出这是可能的。感谢您的澄清。
brichins 2013年

2
@ดาว:它可以与引用类型一起使用,因为当您传递引用类型参数时,所传递的是引用的值而不是对象本身。因此它仍然是按值传递。
塔里克

38

在语义上,您同时ref提供“输入”和“输出”功能,而out仅提供“输出”功能,这是正确的。有一些事情要考虑:

  1. out要求接受参数的方法务必在返回之前的某个时候为变量分配一个值。您可以在某些键/值数据存储类(如Dictionary<K,V>)中找到此模式,其中您具有的功能TryGetValue。该函数采用一个out参数,该参数保存了检索到的值。这是没有意义的调用者传递一个值进入此功能,所以out被用来保证一定的价值会在通话结束后的变量,即使它是不是“真正”的数据(在的情况下TryGetValue,其中密钥不存在)。
  2. outref处理互操作代码时,参数和参数的编组方式有所不同

另外,重要的是要注意,尽管引用类型和值类型在其值的性质上有所不同,但应用程序中的每个变量都指向一个保存值的内存位置,即使对于引用类型也是如此。碰巧的是,对于引用类型,包含在该内存位置中的值是另一个内存位置。当您将值传递给函数(或执行任何其他变量赋值)时,该变量的值将被复制到另一个变量中。对于值类型,这意味着将复制该类型的全部内容。对于引用类型,这意味着将复制内存位置。无论哪种方式,它都会创建变量中包含的数据的副本。它所保持的唯一真正相关性涉及赋值语义。在分配变量或按值传递(默认值)时,对原始(或新)变量进行新赋值时,不会影响其他变量。对于引用类型,是的,对实例进行的更改两侧都有可用,但这是因为实际变量只是指向另一个内存位置的指针;变量的内容(内存位置)实际上并没有改变。

传递ref关键字表示原始变量函数参数实际上都指向相同的内存位置。同样,这仅影响分配语义。如果将新值分配给其中一个变量,则由于其他变量指向相同的存储位置,因此新值将反映在另一侧。


1
请注意,由c#编译器而不是基础IL强制执行被调用方法向out参数分配值的要求。因此,用VB.NET编写的库可能不符合该约定。
jmoreno 2014年

听起来ref实际上等效于C ++(*)中的取消引用符号。C#中的传递引用必须等效于C / C ++称为双指针(指向指针的指针),因此ref必须取消引用第一个指针,从而允许被调用方法访问上下文中实际对象的存储位置。
ComeIn14年

实际上,我建议使用正确的TryGetValue方法,ref而不是out在找不到密钥的情况下不明确使用。
NetMage

27

这取决于编译上下文(请参见下面的示例)。

outref两者都表示变量通过引用传递,但ref需要被传递之前被初始化变量,其可以是在编组的上下文中一个重要的区别(互操作:UmanagedToManagedTransition或反之亦然)

MSDN警告

不要将通过引用传递的概念与引用类型的概念混淆。这两个概念并不相同。可以通过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指令列表


23

以下是我从C#Out Vs Ref上的此代码项目文章中获得的一些注意事项

  1. 仅当我们期望函数或方法有多个输出时,才应使用它。对结构的思考也可以是一个不错的选择。
  2. REF和OUT是关键字,指示数据如何从调用方传递到被调用方,反之亦然。
  3. REF中的数据通过两种方式。从呼叫者到被呼叫者,反之亦然。
  4. 入站数据仅从被叫方到呼叫方传递一种方式。在这种情况下,如果呼叫者试图将数据发送给被呼叫者,它将被忽略/拒绝。

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

下图更直观地显示了差异

C#输出与参考


1
one-waytwo-way此处可能会误用术语。它们实际上都是双向的,但是它们的概念行为在参数的引用和值上有所不同
ibubi

17

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返回a 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更为合适。您完全关心x传递给的内容Increment

在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
}

感谢zneak的回复。但是您能解释一下为什么我不能用它来读写参数吗?
拉吉伯·辛格

@RajbirSingh,因为out不一定必须初始化参数,因此编译器将不允许您从out参数中读取内容,除非您已对其进行了写入。
zneak 2013年

zneak,我同意你的看法。但是在下面的示例中,可以使用out参数进行读取和写入:string name =“ myName”; 私有无效OutMethod(out string nameOut){if(nameOut ==“ myName”){nameOut =“ Rajbir Singh in out方法”; }
拉杰比尔·辛格

1
@RajbirSingh,您的示例未编译。您无法阅读nameOut您的if声明,因为之前未分配任何内容。
zneak 2013年

谢谢@zneak。你是绝对正确的。它不会编译。非常感谢我的帮助,现在对我来说很有意义:)
Rajbir Singh 2013年

7

out是的更多约束版本ref

在方法主体中,您需要out在离开方法之前分配给所有参数。同样,分配给out参数的值也将被忽略,而ref要求分配它们。

因此out,您可以执行以下操作:

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

在哪里ref需要分配a和b。


如果有的话,out则是约束较少的版本。 ref具有“先决条件:绝对已分配变量,后置条件:绝对已分配变量”,而out只有“后置条件:绝对已分配变量”。(并且正如预期的那样,具有更少先决条件的函数实现需要更多的资源)
Ben Voigt

@BenVoigt:猜猜这取决于您所看的方向:)我认为我的意思是编码灵活性方面的约束(?)。
leppie

7

怎么听起来:

=仅初始化/填充参数(参数必须是空的)归还普通

REF =基准,标准参数(可能与值),但功能可以modifiy它。


在将参数传递给方法之前,out参数变量可以获取一个值。
BenceVégert18年

6

您可以out在两个上下文中使用contextual关键字(每个上下文都是详细信息的链接),作为参数修饰符或在接口和委托中的泛型类型参数声明中使用。本主题讨论参数修饰符,但您可以参阅此其他主题,以获取有关泛型类型参数声明的信息。

out关键字的原因参数按引用传递。这类似于ref关键字,不同之处在于它ref要求在传递变量之前对其进行初始化。要使用out参数,方法定义和调用方法都必须显式使用out关键字。例如:C#

class OutExample
{
    static void Method(out int i)
    {
        i = 44;
    }
    static void Main()
    {
        int value;
        Method(out value);
        // value is now 44
    }
}

尽管作为out参数传递的变量不必在传递之前进行初始化,但是在方法返回之前,被调用方法需要分配一个值。

尽管refout关键字会导致不同的运行时行为,但在编译时它们不被视为方法签名的一部分。因此,如果唯一的区别是一个方法接受一个ref参数,而另一个方法接受一个参数,则不能重载方法out。例如,以下代码将无法编译:C#

class CS0663_Example
{
    // Compiler error CS0663: "Cannot define overloaded 
    // methods that differ only on ref and out".
    public void SampleMethod(out int i) { }
    public void SampleMethod(ref int i) { }
}

但是,如果一个方法采用refout参数,而另一个方法均不使用,则可以完成重载,例如:C#

class OutOverloadExample
{
    public void SampleMethod(int i) { }
    public void SampleMethod(out int i) { i = 5; }
}

属性不是变量,因此不能作为out参数传递。

有关传递数组的信息,请参见《使用ref和传递数组》out(《 C#编程指南》)。

您不能将refand out关键字用于以下几种方法:

Async methods, which you define by using the async modifier.

Iterator methods, which include a yield return or yield break statement.

out当您要一个方法返回多个值时,声明一个方法很有用。下面的示例使用out一个方法调用返回三个变量。请注意,第三个参数分配为null。这使方法可以选择返回值。C#

class OutReturnExample
{
    static void Method(out int i, out string s1, out string s2)
    {
        i = 44;
        s1 = "I've been returned";
        s2 = null;
    }
    static void Main()
    {
        int value;
        string str1, str2;
        Method(out value, out str1, out str2);
        // value is now 44
        // str1 is now "I've been returned"
        // str2 is (still) null;
    }
}

6

如何在C#中使用inoutref

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

您不能将in,,refout关键字用于以下几种方法:

  • 异步方法,可以通过使用async修饰符来定义。
  • 迭代器方法,其中包括yield returnor yield break语句。

5

只是为了澄清OP的注释,对ref和out的使用是“对方法外部声明的值类型或结构的引用”,这已经被错误地建立了。

考虑在StringBuilder上使用ref,这是一种引用类型:

private void Nullify(StringBuilder sb, string message)
{
    sb.Append(message);
    sb = null;
}

// -- snip --

StringBuilder sb = new StringBuilder();
string message = "Hi Guy";
Nullify(sb, message);
System.Console.WriteLine(sb.ToString());

// Output
// Hi Guy

为此:

private void Nullify(ref StringBuilder sb, string message)
{
    sb.Append(message);
    sb = null;
}

// -- snip --

StringBuilder sb = new StringBuilder();
string message = "Hi Guy";
Nullify(ref sb, message);
System.Console.WriteLine(sb.ToString());

// Output
// NullReferenceException

4

作为ref传递的参数必须在传递给方法之前进行初始化,而out参数在传递给方法之前无需进行初始化。


4

你为什么要用光?

让其他人知道该变量将从被调用方法返回时将被初始化!

如上所述:“对于out参数,要求调用方法在方法返回之前分配一个值。”

例:

Car car;
SetUpCar(out car);
car.drive();  // You know car is initialized.

4

基本上都refout传递方法之间对象/值

out关键字使参数通过引用传递。这类似于ref关键字,不同之处在于ref要求在传递变量之前先对其进行初始化。

out :参数未初始化,必须在方法中初始化

ref :参数已经初始化,可以在方法中读取和更新。

引用类型的“ ref”有什么用?

您可以将给定的引用更改为其他实例。

你知道吗?

  1. 尽管ref和out关键字导致不同的运行时行为,但在编译时它们不被视为方法签名的一部分。因此,如果唯一的区别是一个方法采用ref参数而另一个方法采用out参数,则方法不能重载。

  2. 您不能将ref和out关键字用于以下几种方法:

    • 通过使用async修饰符定义的异步方法。
    • 迭代器方法,其中包括yield return或yield break语句。
  3. 属性不是变量,因此不能作为输出参数传递。


4

有关C#7的附加说明:
在C#7中,无需使用out预先声明变量。所以这样的代码:

public void PrintCoordinates(Point p)
{
  int x, y; // have to "predeclare"
  p.GetCoordinates(out x, out y);
  WriteLine($"({x}, {y})");
}

可以这样写:

public void PrintCoordinates(Point p)
{
  p.GetCoordinates(out int x, out int y);
  WriteLine($"({x}, {y})");
}

资料来源: C#7的新功能。


4

仍然感到需要一个很好的摘要,这是我想出的。

摘要,

当我们在函数内部时,这就是我们指定变量数据访问控制的方式,

in = R

out =必须在R之前W

ref = R + W


说明,

in

函数只能读取该变量。

out

不得先初始化变量,因为
函数必须READ之前写给它。

ref

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


为什么这样命名呢?

着眼于修改数据的地方,

in

只能在输入(输入)功能之前设置数据。

out

只能在退出(退出)功能之前设置数据。

ref

在输入(输入)功能之前必须设置数据。
可以在退出(退出)功能之前设置数据。


也许(输入/输出/参考)应该重命名为(r / wr / rw)。也许不是,进/出是一个更好的隐喻。
修补匠

0

请注意,inC#ver 7.2开始,它是一个有效的关键字:

in参数修饰符在C#7.2和更高版本中可用。早期版本会生成编译器错误CS8107(“在C#7.0中不提供功能'只读引用'。请使用语言版本7.2或更高版本。”)要配置编译器语言版本,请参阅选择C#语言版本。

...

in关键字使参数通过引用传递。它使形式参数成为参数的别名,该参数必须是变量。换句话说,对参数进行的任何操作都在参数上进行。就像ref或out关键字,不同之处在于in参数不能被调用的方法修改。可以修改ref参数,而out参数必须通过调用的方法进行修改,并且这些修改在调用上下文中是可见的。

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.