【问题标题】:When to use in vs ref vs out何时使用 in vs ref vs out
【发布时间】:2010-12-03 18:21:15
【问题描述】:

前几天有人问我什么时候应该使用参数关键字out 而不是ref。虽然我(我认为)理解 refout 关键字(一直是 asked before)之间的区别,最好的解释似乎是 ref == inout,什么是一些(假设的或代码)示例,我应该始终使用out 而不是ref

既然ref 更通用,你为什么要使用out?只是语法糖吗?

【问题讨论】:

  • 使用out 传入的变量在分配之前无法读取。 ref 没有这个限制。就是这样。
  • 简而言之,ref 用于输入/输出,而out 是仅输出参数。
  • 你到底没有得到什么?
  • 还有out变量必须在函数中赋值。
  • 谢谢科里。但我已经不是那样了。我的观点是,这样做有什么好处。实际上,我需要一个示例来展示我们可以使用 ref 参数来实现使用 out 参数无法实现的功能的场景,反之亦然。

标签: c#


【解决方案1】:

你应该使用out,除非你需要ref

当需要编组数据时,它会产生很大的不同,例如到另一个过程,这可能是昂贵的。因此,当方法不使用初始值时,您要避免编组初始值。

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

作为一个小区别,out 参数不需要初始化。

out 的示例:

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

其中 GetBothNames 是一种以原子方式检索两个值的方法,无论 a 和 b 是什么,该方法都不会改变行为。如果调用到夏威夷的服务器,将初始值从这里复制到夏威夷是浪费带宽。使用 ref 的类似 sn-p:

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);

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

【讨论】:

  • “事实并非如此。” - 你能更好地解释一下你的意思吗?
  • 您不想使用ref 作为默认值。
  • 对于后代:另一个区别似乎没有其他人提到过,如here所述;对于out 参数,调用方法需要在方法返回之前分配一个值。 - 你没有要做任何带有 ref 参数的东西。
  • @brichins 请参考您提到的link 中的“cmets(社区添加)”部分。这是一个错误,已在 VS 2008 文档中得到纠正。
  • @brichins 调用 方法需要分配一个值,而不是调用方法。 zverev.eugene 这是 VS 2008 文档中更正的内容。
【解决方案2】:

用out表示该参数没有被使用,只被设置。这有助于调用者了解您始终在初始化参数。

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

【讨论】:

  • +1 我不知道它也可以用于引用类型,很好的明确答案,谢谢
  • @brichins:不,你不能。 out 参数在进入函数时被视为未分配。在您首先明确地分配了某个值之前,您将无法检查它们的值——根本无法使用调用函数时参数所具有的值。
  • 是的,您无法在内部分配之前访问该值。我指的是参数本身可以稍后在方法中使用 - 它没有被锁定。这是否应该实际完成是一个不同的讨论(关于设计);我只是想指出这是可能的。感谢您的澄清。
  • @ดาว:它可以与引用类型一起使用,因为当您传入引用类型参数时,您传递的是引用的值而不是对象本身。所以它仍然是按值传递。
【解决方案3】:

你是对的,从语义上讲,ref 提供“输入”和“输出”功能,而 out 仅提供“输出”功能。有一些事情需要考虑:

  1. out 要求接受参数的方法必须在返回之前的某个时刻为变量赋值。您可以在 Dictionary<K,V> 等一些键/值数据存储类中找到这种模式,其中您具有 TryGetValue 等功能。此函数采用out 参数,该参数保存检索到的值。调用者将值传递给这个函数是没有意义的,所以out 用于保证调用后变量中会有一些值,即使它不是“真实”数据(在不存在密钥的 TryGetValue 的情况下)。
  2. 在处理互操作代码时,outref 参数的编组方式不同

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

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

【讨论】:

  • 请注意,被调用方法为 out 参数赋值的要求是由 c# 编译器强制执行的,而不是由底层 IL 强制执行的。因此,用 VB.NET 编写的库可能不符合该约定。
  • 听起来 ref 实际上相当于 C++ (*) 中的取消引用符号。 C# 中的传递引用必须等同于 C/C++ 所指的双指针(指向指针的指针),因此 ref 必须取消对第一个指针的引用,从而允许被调用的方法访问上下文中实际对象的内存位置。
  • 我实际上会建议正确的TryGetValue 在找不到密钥的情况下明确使用ref 而不是out
【解决方案4】:

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

outref 都表示通过引用传递的变量,但 ref 要求在传递之前初始化变量,这在 Marshaling 的上下文中可能是一个重要的区别(互操作: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: 分支到目标....

(参见:List of CIL instructions

【讨论】:

    【解决方案5】:

    以下是我从 C# Out Vs Ref 上的这篇 codeproject 文章中提取的一些注释

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

    如果你是一个视觉型的人,那么请看这个 yourtube 视频,它实际上展示了不同之处https://www.youtube.com/watch?v=lYdcY5zulXA

    下图更直观地显示了差异

    【讨论】:

    • one-way , two-way 术语可能在这里被滥用。它们实际上都是双向的,但是它们的概念行为在参数的引用和值上有所不同
    【解决方案6】:

    如果您打算读取和写入参数,则需要使用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 更合适的情况。您完全关心 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 的回复。但是你能解释一下为什么我不能用 out 来读写参数吗?
    • @RajbirSingh,因为 out 参数不一定已经初始化,所以编译器不会让你从 out 参数中读取,直到你写了一些东西。
    • zneak,我同意你的看法。但在下面的例子中,一个 out 参数可以用作读写: string name = "myName"; private void OutMethod(out string nameOut) { if(nameOut=="myName") { nameOut = "Rajbir Singh in out 方法"; } }
    • @RajbirSingh,您的示例无法编译。您无法在 if 语句中读取 nameOut,因为它之前没有分配任何内容。
    • 谢谢@zneak。你是绝对正确的。它不编译。非常感谢我的帮助,现在对我来说很有意义:)
    【解决方案7】:

    outref 的更多约束版本。

    在方法体中,你需要在离开方法之前分配给所有out参数。 此外,分配给 out 参数的值也会被忽略,而 ref 则需要分配它们。

    所以out 允许你这样做:

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

    ref 需要分配 a 和 b。

    【讨论】:

    • 如果有的话,out 是约束较少的版本。 ref 有“前置条件:变量肯定赋值,后置条件:变量肯定赋值”,而out 只有“后置条件:变量肯定赋值”。(正如预期的那样,具有较少前置条件的函数实现需要更多)
    • @BenVoigt:猜猜这取决于你在看什么方向 :) 我想我的意思是编码灵活性方面的限制(?)。
    【解决方案8】:

    听起来如何:

    out = 只初始化/填充一个参数(参数必须为空) return it out plain

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

    【讨论】:

    • out 参数变量可以在传递给方法之前获取一个值。
    【解决方案9】:

    如何在C#中使用inoutref

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

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

    • 异步方法,您使用 async 修饰符定义。
    • 迭代器方法,其中包括 yield returnyield break 语句。

    【讨论】:

      【解决方案10】:

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

      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 参数传递。

      有关传递数组的信息,请参阅使用 refout 传递数组(C# 编程指南)。

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

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

      【讨论】:

        【解决方案11】:

        还是觉得有必要好好总结一下,这是我想出来的。

        总结,

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

        in = R

        out = W 必须在 R 之前

        ref = R+W


        解释,

        in

        函数只能READ那个变量。

        out

        不能先初始化变量,因为,
        必须在 READ 之前向其写入函数。

        ref

        函数可以读/写到那个变量。


        为什么这样命名?

        关注数据被修改的地方,

        in

        数据只能在进入 (in) 函数之前设置。

        out

        数据只能在离开(退出)函数之前设置。

        ref

        必须在进入(in)函数之前设置数据。
        在离开(退出)功能之前可以设置数据。

        【讨论】:

        • 也许 (in/out/ref) 应该重命名为 (r/wr/rw)。也许不是,进/出是一个更好的比喻。
        【解决方案12】:

        只是为了澄清 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
        

        【讨论】:

          【解决方案13】:

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

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

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

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

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

          您可以将给定的引用更改为不同的实例。

          你知道吗?

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

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

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

          【讨论】:

            【解决方案14】:

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

            【讨论】:

              【解决方案15】:

              你为什么想用完?

              为了让其他人知道变量从被调用的方法返回时会被初始化!

              如上所述: "对于out参数,调用方法需要在方法返回前赋值。"

              示例:

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

              【讨论】:

                【解决方案16】:

                关于 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})");
                }
                

                来源:What's new in C# 7.

                【讨论】:

                  【解决方案17】:

                  需要注意的是inC# ver 7.2的有效关键字:

                  in 参数修饰符在 C# 7.2 及更高版本中可用。以前的版本会生成编译器错误 CS8107(“C# 7.0 中没有‘只读引用’功能。请使用语言版本 7.2 或更高版本。”)要配置编译器语言版本,请参阅选择 C# 语言版本。

                  ...

                  in 关键字导致参数通过引用传递。它使形参成为实参的别名,实参必须是变量。换句话说,对参数的任何操作都是在参数上进行的。它类似于 ref 或 out 关键字,只是 in 参数不能被调用的方法修改。虽然 ref 参数可以修改,但 out 参数必须由被调用的方法修改,并且这些修改在调用上下文中是可观察的。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-06-27
                    • 2016-08-06
                    • 2014-06-10
                    • 1970-01-01
                    • 2011-02-20
                    • 2011-03-22
                    • 1970-01-01
                    相关资源
                    最近更新 更多