为什么在C#中引用和退出?


Why ref and out in C#?

当使用关键字ref时,调用代码需要初始化传递的参数,但对于关键字out我们不需要这样做。

  • 为什么我们不到处使用out
  • 两者的确切区别是什么?
  • 请举例说明我们需要使用ref而不能使用out的情况。


答案在这个msdn文章中给出。从那个帖子:

The two parameter passing modes
addressed by out and ref are subtly
different, however they are both very
common. The subtle difference between
these modes leads to some very common
programming errors. These include:

  • not assigning a value to an out
    parameter in all control flow paths
  • not assigning a value to variable
    which is used as a ref parameter
  • Because the C# language assigns
    different definite assignment rules to
    these different parameter passing
    modes, these common coding errors are
    caught by the compiler as being
    incorrect C# code.

    The crux of the decision to include
    both ref and out parameter passing
    modes was that allowing the compiler
    to detect these common coding errors
    was worth the additional complexity of
    having both ref and out parameter
    passing modes in the language.


    outref的一种特殊形式,其中引用的内存在调用前不应初始化。

    在这种情况下,C编译器强制在方法返回之前分配out变量,并且在分配之前不使用该变量。

    out不起作用,但ref起作用的两个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    void NoOp(out int value) // value must be assigned before method returns
    {
    }

    void Increment(out int value) // value cannot be used before it has been assigned
    {
        value = value + 1;
    }


    这些答案都不能让我满意,所以我来看看我对refout的看法。

    我的答案是以下两页的摘要:

  • 参考(C参考)
  • 输出(C参考)
  • 比较

  • 方法定义和调用方法都必须显式使用ref/out关键字
  • 这两个关键字都会导致参数被引用传递(甚至是值类型)
  • 但是,当通过引用传递值类型时没有装箱
  • 属性不能通过outref传递,因为属性实际上是方法
  • ref/out在编译时不被认为是方法签名的一部分,因此,如果方法之间的唯一区别是其中一个方法采用ref参数,而另一个方法采用out参数,则不能重载方法。
  • 对比度

    ref

  • 必须在传递之前初始化
  • 可用于将值传递给方法
  • out

  • 在传递之前不必初始化
  • 在方法返回之前,需要调用方法来分配值
  • 无法使用向方法传递值
  • 实例

    不会编译,因为方法签名的唯一区别是ref/out

    1
    2
    public void Add(out int i) { }
    public void Add(ref int i) { }

    使用ref关键字:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void PrintNames(List<string> names)
    {
      int index = 0; // initialize first (#1)

      foreach(string name in names)
      {
        IncrementIndex(ref index);
        Console.WriteLine(index.ToString() +"." + name);
      }
    }

    public void IncrementIndex(ref int index)
    {
      index++; // initial value was passed in (#2)
    }

    使用out关键字:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void PrintNames(List<string> names)
    {
      foreach(string name in names)
      {
        int index; // not initialized (#1)

        GetIndex(out index);
        Console.WriteLine(index.ToString() +"." + name);
      }
    }

    public void GetIndex(out int index)
    {
      index = IndexHelper.GetLatestIndex(); // needs to be assigned a value (#2 & #3)
    }

    作者的随意评论

  • 在我看来,使用out关键字的概念类似于使用parameterDirection的Output枚举值在ado.net中声明输出参数。
  • C中的数组是通过引用传递的,但是为了重新分配数组引用以影响调用代码中的引用,必须使用ref关键字
  • 例子:

    1
    2
    3
    4
    5
    public void ReassignArray(ref int[] array)
    {
      array = new int[10]; // now the array in the calling code
                           // will point to this new object
    }

    有关引用类型与值类型的详细信息,请参阅传递引用类型参数(C编程指南)


    ref关键字允许您更改参数的值。正在调用的方法可以是调用链中的中间链接。使用out关键字的方法只能在调用链的开头使用。

    另一个优点是现有的值可以在方法的逻辑中使用,并且仍然保留返回值。

    在Oracle中,函数有显式的in/out和out参数(默认值以及如果不设置方向会得到什么)。等效值为正常值(仅为参数)、ref[参数]和out[参数]。


    当您需要使用ref而不是out时,一个人为的示例如下:

    1
    2
    3
    4
    5
    6
    7
    public void SquareThisNumber(ref int number)
    {
       number = number * number;
    }

    int number = 4;
    SquareThisNumber(ref number);

    这里我们希望number是一个进出变量,所以我们使用ref。如果我们使用out,编译器会给出一个错误,说明我们在使用之前初始化了out参数。


    当我们在调用以out关键字为前缀的方法时传递该值,它会完全不同地对待它,就像我们没有将它传递给方法一样。相反,我们实际上是将out变量的值从方法的定义部分收集(路由)到调用该方法的方法out变量参数。

    所以out variable是方法定义中处理的输出,这就是为什么我们需要创建它,初始化它,并且只在定义中修改它的原因。

    当需要从特定方法返回多个值时,使用out变量。

    而对于ref变量,我们需要首先初始化它,因为它的内存位置被作为参数传递到方法定义。想想如果我们在传递之前不初始化它会发生什么?


    编译器知道out变量不应该在调用之前设置。这允许在使用前声明它们。然而,它知道必须在返回中使用的函数之前设置它。