Ref返回C#7.0中的限制

时间:2017-03-18 13:46:53

标签: c# c#-7.0

我试图了解以下关于C#7.0中与ref返回相关的新功能的官方博客文章的摘录。

  
      
  1. 你只能返回“安全返回”的引用:那些是   传递给你,指向对象中的字段。

  2.   
  3. 参考本地化初始化为某个存储位置,无法变异指向另一个存储位置。

  4.   

不幸的是,博客文章没有提供任何代码示例。如果有人能够通过实际例子和解释更加清楚地突出以粗体突出显示的限制,那将非常感激。

提前致谢。

4 个答案:

答案 0 :(得分:17)

你有一些答案可以澄清限制,但不是限制背后的推理。

限制背后的原因是我们绝不允许别名为死变量。如果你在普通方法中有一个普通的本地,并且你返回一个ref,那么当使用ref时,本地就已经死了。

现在,有人可能会指出ref返回的本地可以被提升到闭包类的字段。是的,这可以解决问题。但该功能的重点是允许开发人员编写高性能的贴近机器的低成本机制,并自动提升到关闭 - 然后承担收集压力的负担等等 - 工作反对这个目标。

事情可能会变得有点棘手。考虑:

ref int X(ref int y) { return ref y; }
ref int Z( )
{
  int z = 123;
  return ref X(ref z);
}

这里我们以偷偷摸摸的方式返回本地z的引用!这也必须是非法的。但现在考虑一下:

ref double X(ref int y) { return ref whatever; }
ref double Z( )
{
  int z = 123;
  return ref X(ref z);
}

现在我们知道返回的ref不是z的引用。那么,如果传入的引用类型与返回的引用类型不同,那么我们可以说这是合法的吗?

这个怎么样?

struct S { public int s; }
ref int X(ref S y) { return ref y.s; }
ref int Z( )
{
  S z = default(S);
  return ref X(ref z);
}

现在我们又一次将ref返回到一个死变量。

当我们第一次设计此功能时(2010年IIRC),有许多复杂的建议来处理这些情况,但我最喜欢的建议只是“让所有这些都非法”。你不会返回一个ref-returns方法返回的引用,即使它不可能已经死了。

我不知道C#7团队最终实施了什么规则。

答案 1 :(得分:6)

要通过引用传递内容,必须将其归类为变量。 C#规范(§5变量)定义了七类变量:静态变量,实例变量,数组元素,值参数,引用参数,输出参数和局部变量。

class ClassName {
    public static int StaticField;
    public int InstanceField;
}
void Method(ref int i) { }
void Test1(int valueParameter, ref int referenceParameter, out int outParameter) {
    ClassName instance = new ClassName();
    int[] array = new int[1];
    outParameter=0;
    int localVariable = 0;
    Method(ref ClassName.StaticField);  //Static variable
    Method(ref instance.InstanceField); //Instance variable
    Method(ref array[0]);               //Array element
    Method(ref valueParameter);         //Value parameter
    Method(ref referenceParameter);     //Reference parameter
    Method(ref outParameter);           //Output parameter
    Method(ref localVariable);          //Local variable
}

第一点实际上是说你可以返回归类为参考参数,输出参数,静态变量实例变量的变量。

ref int Test2(int valueParameter, ref int referenceParameter, out int outParameter) {
    ClassName instance = new ClassName();
    int[] array = new int[1];
    outParameter=0;
    int localVariable = 0;
    return ref ClassName.StaticField;  //OK, "ones that point into fields in objects"
    return ref instance.InstanceField; //OK, "ones that point into fields in objects"
    return ref array[0];               //OK, array elements are also "safe to return" by reference
    return ref valueParameter;         //Error
    return ref referenceParameter;     //OK, "ones that were passed to you"
    return ref outParameter;           //OK, "ones that were passed to you"
    return ref localVariable;          //Error
}

请注意,对于值类型的实例字段,您应该考虑封闭变量的“安全返回”状态。并不总是允许,例如引用类型的字段:

struct StructName {
    public int InstacneField;
}
ref int Test3() {
    StructName[] array = new StructName[1];
    StructName localVariable = new StructName();
    return ref array[0].InstacneField;      //OK, array[0] is "safe to return"
    return ref localVariable.InstacneField; //Error, localVariable is not "safe to return"
}

ref return方法的结果被认为是“安全返回”,如果这种方法不带任何“安全返回”参数:

ref int ReturnFirst(ref int i, ref int ignore) => ref i;
ref int Test4() {
    int[] array = new int[1];
    int localVariable = 0;
    return ref ReturnFirst(ref array[0], ref array[0]);      //OK, array[0] is "safe to return"
    return ref ReturnFirst(ref array[0], ref localVariable); //Error, localVariable is not "safe to return"
}

虽然我们知道ReturnFirst(ref array[0], ref localVariable)将返回“安全返回”引用(ref array[0]),但编译器无法通过单独分析Test4方法来推断它。因此,在这种情况下,ReturnFirst方法的结果被认为不是“安全返回”。

第二点说,ref局部变量声明必须包含初始化器:

int localVariable = 0;
ref int refLocal1;                     //Error, no initializer
ref int refLocal2 = ref localVariable; //OK

此外,无法重新分配ref本地变量以指向其他存储位置:

int localVariable1 = 0;
int localVariable2 = 0;
ref int refLocal = ref localVariable1;
ref refLocal = ref localVariable2;     //Error
refLocal = ref localVariable2;         //Error

实际上没有有效的语法来重新分配ref本地变量。

答案 2 :(得分:4)

您可以在GitHub - Proposal: Ref Returns and Locals找到关于此功能的精彩讨论。

  

1。你只能返回“安全返回”的引用:那些是   传递给你,以及指向对象中的字段的那些。

以下示例显示了安全引用的返回,因为它来自调用者:

public static ref TValue Choose<TValue>(ref TValue val)
{
    return ref val;
}

相反,此示例的非安全版本将返回对本地的引用(此代码无法编译):

public static ref TValue Choose<TValue>()
{
    TValue val = default(TValue);
    return ref val;
}
  

2。 Ref locals初始化为某个存储位置,不能变异指向另一个存储位置。

限制意味着您需要始终在声明时初始化本地引用。像

这样的声明
ref double aReference;

不会编译。您也无法为现有的引用分配新引用,如

aReference = ref anOtherValue;

答案 3 :(得分:0)

此页面上的其他答案是完整且有用的,但我想添加一个额外的点,即out参数,您的函数需要完全初始化,计为&#34 ;出于 ref return 的目的,可安全返回&#34;

有趣的是,将此事实与另一个新的 C#7 功能inline declaration of 'out' variables相结合,可以模拟局部变量的通用 内联声明 能力:

辅助功能:

public static class _myglobals
{
    /// <summary> Helper function for declaring local variables inline. </summary>
    public static ref T local<T>(out T t)
    {
        t = default(T);
        return ref t;
    }
};

使用此帮助程序,调用者指定&#34;内联局部变量&#34;的初始化。通过 分配帮助者的ref-return

为了演示帮助器,接下来是一个简单的两级比较函数的例子,这对于(例如)MyObj.IComparable<MyObj>.Compare实现来说是典型的。虽然非常简单,但这种类型的表达式无法绕过需要单个局部变量 - 没有重复工作,即。现在通常情况下,需要一个本地会阻止使用expression-bodied member,这是我们在这里要做的事情,但问题很容易用上面的帮助器解决:

public int CompareTo(MyObj x) =>
                       (local(out int d) = offs - x.offs) == 0 ? size - x.size : d;

演练: 本地变量d是&#34;内联声明,&#34;并根据 offs 字段初始化计算第一级比较的结果。如果此结果不确定,我们将回退到返回第二级排序(基于 size 字段)。但在替代方案中,我们仍然可以返回第一级结果,因为它已保存在本地d中。

请注意,上述内容也可以在没有辅助功能的情况下完成,通过 C#7 pattern matching

public int CompareTo(MyObj other) => 
                       (offs - x.offs) is int d && d == 0 ? size - x.size : d;

包含在源文件的顶部:

using System;
using /* etc... */
using System.Xml;
using Microsoft.Win32;

using static _myglobals;    //  <-- puts function 'local(...)' into global name scope

namespace MyNamespace
{
   // ...

以下示例显示声明内联本地变量,并在 C#7 中初始化。如果未提供初始化,则会获得由default(T)帮助程序函数指定的local<T>(out T t)。现在只有ref return功能才能实现此功能,因为ref return方法是唯一可用作ℓ-value的方法。

示例1:

var s = "abc" + (local(out int i) = 2) + "xyz";   //   <-- inline declaration of local 'i'
i++;
Console.WriteLine(s + i);   //   -->  abc2xyz3

示例2:

if ((local(out OpenFileDialog dlg) = new OpenFileDialog       // <--- inline local 'dlg'
    {
        InitialDirectory = Environment.CurrentDirectory,
        Title = "Pick a file",
    }).ShowDialog() == true)
{
    MessageBox.Show(dlg.FileName);
}

第一个例子从一个整数文字中简单地分配。在第二个示例中,内联本地dlg是从构造函数(new表达式)分配的,然后整个赋值表达式用于其解析值以调用实例方法(ShowDialog )在新创建的实例上。为了清楚地作为一个独立的示例,它通过显示dlg的引用实例确实需要被命名为变量来完成,以便获取其中一个属性。

[编辑:] 关于......

  

2。 Ref locals初始化为某个存储位置,不能变异指向另一个存储位置。

...拥有一个带有可变引用的ref变量肯定会很好,因为这有助于避免在循环体内进行昂贵的索引边界检查。当然,这也正是为什么它不被允许的原因。你可能无法解决这个问题(即ref到一个数组访问表达式,索引包含ref不会起作用;它会在初始化时永久解析为引用位置的元素)但如果​​有帮助,请注意您可以ref带到指针,其中包括 ref local

int i = 5, j = 6;

int* pi = &i;
ref int* rpi = ref pi;

Console.WriteLine(i + " " + *pi + " " + *rpi);      //   "5 5 5"

pi = &j;

Console.WriteLine(i + " " + *pi + " " + *rpi);      //   "5 6 6"

这个毫无意义的示例代码的要点是,虽然我们没有以任何方式改变 ref本地变量rpi本身(因为&#39;你可以&# 39; t),它 现在有一个不同的(终极)指示物。

更严重的是, ref local 现在允许的,就像在数组索引循环体中收紧IL一样,我称之为 值类型标记。 这允许循环体中有效的IL,它需要访问值类型数组中每个元素的多个字段。通常,这需要在外部初始化(newobj / initobj)之后进行权衡,然后是单个索引访问与原位非初始化,但需要花费冗余多运行时索引。

但是,使用值类型标记,现在我们可以完全避免每个元素initobj / newobj IL指令,并且在运行时只有一个索引计算。我将首先展示该示例,然后在下面详细描述该技术。

/// <summary>
/// Returns a new array of (int,T) where each element of 'src' is paired with its index.
/// </summary>
public static (int Index, T Item)[] TagWithIndex<T>(this T[] src)
{
    if (src.Length == 0)
        return new (int, T)[0];

    var dst = new (int Index, T Item)[src.Length];     // i.e, ValueTuple<int,T>[]
    ref var p = ref dst[0];      //  <--  co-opt element 0 of target for 'T' staging

    ref int i = ref p.Index;  //  <-- index field in target will also control loop
    i = src.Length;    

    while (true)
    {
        p.Item = src[--i];
        if (i == 0)
            return dst;
        dst[i] = p;
    }
}

该示例显示了值型冲压技术的简洁而极端的使用;如果你感兴趣的话,你可以自己辨别它的扭曲(在评论中发表)。在下文中,我将更一般地讨论价值型冲压技术。

首先,准备参考本地,直接引用value-type的登台实例中的相关字段。这可以在堆栈上,或者如示例中所示,从目标数组本身的最后处理元素中选择。对整个登台实例也有ref可能很有价值,特别是如果使用合作选择技术。

循环体的每次迭代都可以非常有效地准备登台实例,并且作为准备好的最后一步,&#34;盖章&#34;它只用一个索引操作批量进入数组。当然,如果数组的最后一个元素被选为staging实例,那么你也可以稍早地保留循环。