【问题标题】:Ref returns restrictions in C# 7.0Ref 返回 C# 7.0 中的限制
【发布时间】:2017-08-10 01:15:29
【问题描述】:

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

  1. 您只能返回“安全返回”的 ref: 传递给您,以及指向对象中的字段。

  2. Ref locals 被初始化到一个特定的存储位置,并且不能被变异为指向另一个。

很遗憾,博文没有给出任何代码示例。如果有人可以通过实际示例和解释更深入地了解以粗体突出显示的限制,将不胜感激。

提前致谢。

【问题讨论】:

标签: c# c#-7.0


【解决方案1】:

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

1. 您只能返回“安全返回”的 ref: 传递给您,以及指向对象中的字段的那些。

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

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;

【讨论】:

  • 所以没有带有 ref 返回的闭包。
【解决方案2】:

要通过引用传递某些东西,它必须被归类为变量。 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返回变量分类为引用参数、输出参数、静态变量实例变量

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 返回方法的结果被认为是“安全返回”:

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 局部变量。

【讨论】:

  • 不能更改ref local 的引用对象太糟糕了,因为它排除了它们在聚合循环中的使用,这是传播单个预初始化值的常见做法(或如愿以偿,引用到一个值)跨越循环体的相邻迭代。换句话说,每次迭代都会引用前一次迭代来更新聚合值。特别难过,因为这样的循环通常索引数组,这是您希望通过使用 ref local 通过只执行一次偏移计算和边界检查来合并等效数组访问来优化的事情之一。
【解决方案3】:

您已经得到了一些澄清限制的答案,但没有澄清限制背后的原因。

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

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

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

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

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

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

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

这个怎么样?

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

现在我们又一次返回了一个死变量的引用。

当我们第一次设计这个功能时(在 2010 年 IIRC),有许多复杂的提案来处理这些情况,但我最喜欢的提案只是“让所有这些都是非法的”。您无法返回由 ref-returning 方法返回的引用,即使它不可能死。

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

【讨论】:

  • “即使它不可能活着”是指“即使它不可能死”吗?如果没有,我很困惑。并不是说 ref 返回很难……这是我遇到的最令人困惑的功能之一。
  • actual rule they implemented 是:如果作为形式参数传递给该方法的所有引用/输出都可以安全返回,则从另一个方法返回的引用是安全的。 -它更复杂,但这是一个很好的妥协 IMO :)
【解决方案4】:

这个页面上的其他答案是完整和有用的,但我想补充一点,那就是out参数,你的函数需要完全初始化,算作“安全返回” 用于 ref return

有趣的是,将这一事实与另一个新的 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;
    }
};

有了这个助手,调用者通过分配给助手的ref-return来指定“内联局部变量”的初始化 .

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

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

演练: 局部变量 d 是“内联声明的”,并使用基于 的第一级比较计算结果进行初始化关闭 字段。如果这个结果没有定论,我们会退回到返回第二级排序(基于 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&lt;T&gt;(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 local 变量 rpi 本身(因为你不能),它 现在有不同的(最终)参照物。


更严重的是,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;
    }
}

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

首先,准备 ref locals,直接引用值类型的 staging instance 中的相关字段。这可以在堆栈上,或者如示例中所示,从目标数组本身的最后一个要处理的元素中选择。为整个暂存实例设置一个ref 可能也很有价值,尤其是在使用共同选择技术的情况下。

然后,循环体的每次迭代都可以非常有效地准备暂存实例,并且作为准备好后的最后一步,只需一次索引操作就可以将其全部“标记”到数组中。当然,如果数组的最后一个元素被选为暂存实例,那么您也可以稍早离开循环。

【讨论】:

  • 一个小提示...大多数人都知道在定义 properties 时确实可以使用 C# 7 ref return以及 .NET 中的 methods... 但另外需要注意的是它也适用于 indexersclass foo { static ulong ul = 999; public ref ulong this[String key] =&gt; ref ul; }
猜你喜欢
  • 2018-01-27
  • 1970-01-01
  • 2017-06-29
  • 2019-12-23
  • 2018-06-26
  • 2014-12-28
  • 1970-01-01
  • 2013-04-18
  • 1970-01-01
相关资源
最近更新 更多