只读引用Readonly references

  • [x] 建议[x] Proposed
  • [x] 原型[x] Prototype
  • [x] 实现:已启动[x] Implementation: Started
  • [] 规范:未启动[ ] Specification: Not Started

总结Summary

"只读引用" 功能实际上是一组功能,这些功能利用通过引用传递变量的效率,但不会将数据公开给修改:The "readonly references" feature is actually a group of features that leverage the efficiency of passing variables by reference, but without exposing the data to modifications:

  • in parametersin parameters
  • ref readonly 返回ref readonly returns
  • readonly 结构readonly structs
  • ref/in 扩展方法ref/in extension methods
  • ref readonly &ref readonly locals
  • ref 条件表达式ref conditional expressions

作为 readonly 引用传递参数。Passing arguments as readonly references.

有一个现有建议,它将本主题 https://github.com/dotnet/roslyn/issues/115 视为只读参数的特例,而不会有许多详细信息。There is an existing proposal that touches this topic https://github.com/dotnet/roslyn/issues/115 as a special case of readonly parameters without going into many details. 在这里,我只是要承认自己的想法并不是很新的。Here I just want to acknowledge that the idea by itself is not very new.

动机Motivation

在此功能之前,c # 没有一种有效的方法来表示将结构变量传递到方法调用以实现 readonly,而不需要进行修改。Prior to this feature C# did not have an efficient way of expressing a desire to pass struct variables into method calls for readonly purposes with no intention of modifying. 标准的按值参数传递意味着复制,这会增加不必要的成本。Regular by-value argument passing implies copying, which adds unnecessary costs. 这会使用户使用按引用参数传递,并依赖注释/文档来指示数据不应由被调用方转变。That drives users to use by-ref argument passing and rely on comments/documentation to indicate that the data is not supposed to be mutated by the callee. 这不是一个很好的解决方案,原因很多。It is not a good solution for many reasons.
这些示例是图形库中的大量矢量/矩阵数学运算符, 例如, 由于性能方面的考虑,它会知道,只需使用 ref 操作数。The examples are numerous - vector/matrix math operators in graphics libraries like XNA are known to have ref operands purely because of performance considerations. Roslyn 编译器本身有一些代码,它使用结构来避免分配,并按引用传递它们,以避免复制成本。There is code in Roslyn compiler itself that uses structs to avoid allocations and then passes them by reference to avoid copying costs.

解决方案 (in 参数) Solution (in parameters)

与参数类似 outin 参数作为托管引用传递,并具有来自被调用方的其他保证。Similarly to the out parameters, in parameters are passed as managed references with additional guarantees from the callee.
与在 out 任何其他使用之前 必须 由被调用方分配的参数不同, in 参数无法由被调用方分配。Unlike out parameters which must be assigned by the callee before any other use, in parameters cannot be assigned by the callee at all.

作为结果 in 参数,可以实现间接参数传递的有效性,而不会将参数公开给被调用方的突变。As a result in parameters allow for effectiveness of indirect argument passing without exposing arguments to mutations by the callee.

声明 in 参数Declaring in parameters

in 使用 in 关键字作为参数签名中的修饰符声明参数。in parameters are declared by using in keyword as a modifier in the parameter signature.

对于所有用途,该 in 参数将被视为 readonly 变量。For all purposes the in parameter is treated as a readonly variable. 在方法内部使用参数的大部分限制 in 都与 readonly 字段相同。Most of the restrictions on the use of in parameters inside the method are the same as with readonly fields.

确实有一个 in 参数可以表示 readonly 字段。Indeed an in parameter may represent a readonly field. 限制的相似性并不是一种巧合。Similarity of restrictions is not a coincidence.

例如, in 具有结构类型的参数的字段以递归方式分类为 readonly 变量。For example fields of an in parameter which has a struct type are all recursively classified as readonly variables .

static Vector3 Add (in Vector3 v1, in Vector3 v2)
{
    // not OK!!
    v1 = default(Vector3);

    // not OK!!
    v1.X = 0;

    // not OK!!
    foo(ref v1.X);

    // OK
    return new Vector3(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
  • in 允许使用普通 byval 参数的任何位置都允许使用参数。in parameters are allowed anywhere where ordinary byval parameters are allowed. 这包括索引器、运算符 (包括转换) 、委托、lambda、本地函数。This includes indexers, operators (including conversions), delegates, lambdas, local functions.
 (in int x) => x                                                     // lambda expression  
 TValue this[in TKey index];                                         // indexer
 public static Vector3 operator +(in Vector3 x, in Vector3 y) => ... // operator
  • in 不允许将与或不 out 结合的任何内容结合使用 outin is not allowed in combination with out or with anything that out does not combine with.

  • 不允许重载 ref / out / in 差异。It is not permitted to overload on ref/out/in differences.

  • 允许重载普通 byval 和 in 差异。It is permitted to overload on ordinary byval and in differences.

  • 出于 OHI 的目的 (重载、隐藏、实现) , in 行为类似于 out 参数。For the purpose of OHI (Overloading, Hiding, Implementing), in behaves similarly to an out parameter. 所有相同的规则都适用。All the same rules apply. 例如,重写方法将必须与 in 具有 in 可转换类型的参数的参数匹配。For example the overriding method will have to match in parameters with in parameters of an identity-convertible type.

  • 出于委托/lambda/方法组转换的目的, in 行为与 out 参数类似。For the purpose of delegate/lambda/method group conversions, in behaves similarly to an out parameter. Lambda 和适用的方法组转换候选项必须 in 与具有可 in 转换类型的参数的目标委托的参数匹配。Lambdas and applicable method group conversion candidates will have to match in parameters of the target delegate with in parameters of an identity-convertible type.

  • 出于泛型方差的目的, in 参数是 nonvariant 的。For the purpose of generic variance, in parameters are nonvariant.

注意: in 对于具有引用类型或基元类型的参数,不会出现警告。NOTE: There are no warnings on in parameters that have reference or primitives types. 通常,它可能是毫无意义的,但在某些情况下,用户必须/希望将基元作为传递 inIt may be pointless in general, but in some cases user must/want to pass primitives as in. 示例-重写泛型方法(如 Method(in T param)T 将替换为时 int )或使用方法(如) Volatile.Read(in int location)Examples - overriding a generic method like Method(in T param) when T was substituted to be int, or when having methods like Volatile.Read(in int location)

有一个分析器可以在使用效率较低的情况下发出警告 in ,但这种分析的规则会过于模糊,无法成为语言规范的一部分。It is conceivable to have an analyzer that warns in cases of inefficient use of in parameters, but the rules for such analysis would be too fuzzy to be a part of a language specification.

in在调用站点使用。Use of in at call sites. (in 参数) (in arguments)

可以通过两种方式将参数传递给 in 参数。There are two ways to pass arguments to in parameters.

in 参数可以匹配 in 参数:in arguments can match in parameters:

in调用站点上带有修饰符的参数可与参数匹配 inAn argument with an in modifier at the call site can match in parameters.

int x = 1;

void M1<T>(in T x)
{
  // . . .
}

var x = M1(in x);  // in argument to a method

class D
{
    public string this[in Guid index];
}

D dictionary = . . . ;
var y = dictionary[in Guid.Empty]; // in argument to an indexer
  • in 参数必须是 可读 的左值 ( * ) 。in argument must be a readable LValue(*). 示例: M1(in 42) 无效Example: M1(in 42) is invalid

(\ * ) LValue/RValue 的概念在不同语言之间有所不同。(*) The notion of LValue/RValue vary between languages.
此处,按左值 I 表示一个表达式,该表达式表示可以直接引用的位置。Here, by LValue I mean an expression that represent a location that can be referred to directly. 和 RValue 表示一个表达式,该表达式产生的临时结果不会自行保存。And RValue means an expression that yields a temporary result which does not persist on its own.

  • 具体而言,将 readonly 字段、 in 参数或其他 readonly 形式变量作为参数传递是有效的 inIn particular it is valid to pass readonly fields, in parameters or other formally readonly variables as in arguments. 示例: dictionary[in Guid.Empty] 是合法的。Example: dictionary[in Guid.Empty] is legal. Guid.Empty 为静态只读字段。Guid.Empty is a static readonly field.

  • in 参数的类型必须能够 转换 为参数的类型。in argument must have type identity-convertible to the type of the parameter. 示例: M1<object>(in Guid.Empty) 无效。Example: M1<object>(in Guid.Empty) is invalid. Guid.Empty 不能 转换objectGuid.Empty is not identity-convertible to object

上述规则的动机是 in 参数保证参数变量的 别名The motivation for the above rules is that in arguments guarantee aliasing of the argument variable. 被调用方始终接收对由参数表示的同一位置的直接引用。The callee always receives a direct reference to the same location as represented by the argument.

  • 在极少数情况下 in ,如果自变量用作相同调用的操作数,则参数必须为堆栈溢出 await ,则行为与 with out 和 arguments 相同 ref -如果变量无法以引用透明方式溢出,则会报告错误。in rare situations when in arguments must be stack-spilled due to await expressions used as operands of the same call, the behavior is the same as with out and ref arguments - if the variable cannot be spilled in referentially-transparent manner, an error is reported.

示例:Examples:

  1. M1(in staticField, await SomethingAsync()) 有效。M1(in staticField, await SomethingAsync()) is valid. staticField 是一个静态字段,可在不具有明显副作用的情况下多次访问。staticField is a static field which can be accessed more than once without observable side effects. 因此,可以提供副作用和别名要求的顺序。Therefore both the order of side effects and aliasing requirements can be provided.
  2. M1(in RefReturningMethod(), await SomethingAsync()) 将产生错误。M1(in RefReturningMethod(), await SomethingAsync()) will produce an error. RefReturningMethod()ref 返回方法。RefReturningMethod() is a ref returning method. 方法调用可能具有可观察到的副作用,因此必须在操作数之前计算 SomethingAsync()A method call may have observable side effects, therefore it must be evaluated before the SomethingAsync() operand. 但是,调用的结果是不能跨挂起点保留的引用,这不会导致 await 直接引用要求。However the result of the invocation is a reference that cannot be preserved across the await suspension point which make the direct reference requirement impossible.

注意: stack 溢出错误被视为特定于实现的限制。NOTE: the stack spilling errors are considered to be implementation-specific limitations. 因此,它们不会影响重载决策或 lambda 推理。Therefore they do not have effect on overload resolution or lambda inference.

普通 byval 参数可以匹配 in 参数:Ordinary byval arguments can match in parameters:

不带修饰符的普通参数可以与 in 参数匹配。Regular arguments without modifiers can match in parameters. 在这种情况下,参数具有的宽松约束与普通 byval 参数相同。In such case the arguments have the same relaxed constraints as an ordinary byval arguments would have.

此方案的动机是, in 当不能将参数传递为直接引用时,api 中的参数可能会导致不便用户,例如:文本、计算 await 结果或需要更具体的类型的参数。The motivation for this scenario is that in parameters in APIs may result in inconveniences for the user when arguments cannot be passed as a direct reference - ex: literals, computed or await-ed results or arguments that happen to have more specific types.
所有这些情况都有一个简单的解决方案,将参数值存储在适当类型的临时本地,并将该本地作为 in 参数传递。All these cases have a trivial solution of storing the argument value in a temporary local of appropriate type and passing that local as an in argument.
若要减少此类样板代码的需求,可在需要时执行同一转换(如果需要) inTo reduce the need for such boilerplate code compiler can perform the same transformation, if needed, when in modifier is not present at the call site.

此外,在某些情况下,例如运算符或扩展方法的调用,根本不需要 in 指定语法方式 inIn addition, in some cases, such as invocation of operators, or in extension methods, there is no syntactical way to specify in at all. 只需在与参数匹配时指定普通 byval 参数的行为 inThat alone requires specifying the behavior of ordinary byval arguments when they match in parameters.

具体而言:In particular:

  • 传递右是有效的。it is valid to pass RValues. 在这种情况下,将传递对临时的引用。A reference to a temporary is passed in such case. 示例:Example:
Print("hello");      // not an error.

void Print<T>(in T x)
{
  //. . .
}
  • 允许使用隐式转换。implicit conversions are allowed.

这实际上是传递 RValue 的一种特殊情况This is actually a special case of passing an RValue

在这种情况下,将传递对临时保存转换值的引用。A reference to a temporary holding converted value is passed in such case. 示例:Example:

Print<int>(Short.MaxValue)     // not an error.
  • 在扩展方法接收方的情况下 in (ref ,而不是扩展方法) 、右或隐式 这种参数转换in a case of a receiver of an in extension method (as opposed to ref extension methods), RValues or implicit this-argument-conversions are allowed. 在这种情况下,将传递对临时保存转换值的引用。A reference to a temporary holding converted value is passed in such case. 示例:Example:
public static IEnumerable<T> Concat<T>(in this (IEnumerable<T>, IEnumerable<T>) arg)  => . . .;

("aa", "bb").Concat<char>()    // not an error.

有关扩展方法的详细信息 ref / in ,请在本文档中进一步了解。More information on ref/in extension methods is provided further in this document.

  • 由于操作数的原因 await ,参数溢出可能会溢出 "按值"。argument spilling due to await operands could spill "by-value", if necessary. 在提供对自变量值的直接引用的情况下,不可能由于干扰参数值的副本而产生这种情况 awaitIn scenarios where providing a direct reference to the argument is not possible due to intervening await a copy of the argument's value is spilled instead.
    示例:Example:
M1(RefReturningMethod(), await SomethingAsync())   // not an error.

由于有副作用的调用的结果是不能在挂起期间保留的引用 await ,因此将保留包含实际值的临时,而不是在) 普通 byval 参数的情况下 (。Since the result of a side-effecting invocation is a reference that cannot be preserved across await suspension, a temporary containing the actual value will be preserved instead (as it would in an ordinary byval parameter case).

省略的可选参数Omitted optional arguments

允许 in 参数指定默认值。It is permitted for an in parameter to specify a default value. 这使相应参数成为可选参数。That makes the corresponding argument optional.

在调用站点省略可选参数会导致通过临时传递默认值。Omitting optional argument at the call site results in passing the default value via a temporary.

Print("hello");      // not an error, same as
Print("hello", c: Color.Black);

void Print(string s, in Color c = Color.Black)
{
    // . . .
}

别名行为概述Aliasing behavior in general

refout 变量一样, in 变量也是现有位置的引用/别名。Just like ref and out variables, in variables are references/aliases to existing locations.

当被调用方不能写入它们时,读取 in 参数可能会将不同的值视为其他计算的副作用。While callee is not allowed to write into them, reading an in parameter can observe different values as a side effect of other evaluations.

示例:Example:

static Vector3 v = Vector3.UnitY;

static void Main()
{
    Test(v);
}

static void Test(in Vector3 v1)
{
    Debug.Assert(v1 == Vector3.UnitY);
    // changes v1 deterministically (no races required)
    ChangeV();
    Debug.Assert(v1 == Vector3.UnitX);
}

static void ChangeV()
{
    v = Vector3.UnitX;
}

in 局部变量的参数和捕获。in parameters and capturing of local variables.

出于 lambda/async 捕获参数的目的, in 其行为与 out 和参数相同 refFor the purpose of lambda/async capturing in parameters behave the same as out and ref parameters.

  • in 无法在闭包中捕获参数in parameters cannot be captured in a closure
  • in 迭代器方法中不允许使用参数in parameters are not allowed in iterator methods
  • in 异步方法中不允许使用参数in parameters are not allowed in async methods

临时变量。Temporary variables.

参数传递的某些用途 in 可能需要间接使用临时本地变量:Some uses of in parameter passing may require indirect use of a temporary local variable:

  • in 调用站点使用时,参数始终作为直接别名传递 inin arguments are always passed as direct aliases when call-site uses in. 在这种情况下,永远不会使用暂时。Temporary is never used in such case.
  • in 当调用站点不使用时,参数无需是直接别名 inin arguments are not required to be direct aliases when call-site does not use in. 如果参数不是左值,则可以使用临时参数。When argument is not an LValue, a temporary may be used.
  • in 参数可以具有默认值。in parameter may have default value. 当在调用站点上省略相应的参数时,默认值将通过一个临时值传递。When corresponding argument is omitted at the call site, the default value are passed via a temporary.
  • in 参数可能具有隐式转换,包括不保留标识的参数。in arguments may have implicit conversions, including those that do not preserve identity. 在这种情况下,将使用一个临时。A temporary is used in those cases.
  • 普通结构调用的接收方可能不是可写的左值 (现有事例!) 。receivers of ordinary struct calls may not be writeable LValues (existing case!). 在这种情况下,将使用一个临时。A temporary is used in those cases.

自变量临时内存的生存时间与调用站点最接近的范围。The life time of the argument temporaries matches the closest encompassing scope of the call-site.

临时变量的正式生存时间对于涉及引用返回的变量的转义分析的方案,在语义上非常重要。The formal life time of temporary variables is semantically significant in scenarios involving escape analysis of variables returned by reference.

参数的元数据表示形式 inMetadata representation of in parameters.

System.Runtime.CompilerServices.IsReadOnlyAttribute 应用于 byref 参数时,它表示参数是一个 in 参数。When System.Runtime.CompilerServices.IsReadOnlyAttribute is applied to a byref parameter, it means that the parameter is an in parameter.

此外,如果该方法为 抽象 方法,则此类参数的签名 (并且只有此类参数) 必须具有 modreq[System.Runtime.InteropServices.InAttribute]In addition, if the method is abstract or virtual, then the signature of such parameters (and only such parameters) must have modreq[System.Runtime.InteropServices.InAttribute].

动机:这样做是为了确保在方法重写/实现参数匹配的情况下执行此操作 inMotivation: this is done to ensure that in a case of method overriding/implementing the in parameters match.

相同要求适用于 Invoke 委托中的方法。Same requirements apply to Invoke methods in delegates.

动机:这是为了确保 readonly 在创建或分配委托时现有编译器不能简单地忽略。Motivation: this is to ensure that existing compilers cannot simply ignore readonly when creating or assigning delegates.

由 readonly 引用返回。Returning by readonly reference.

动机Motivation

此子功能的动机大致对称于参数的原因 in -避免复制,而是在返回方。The motivation for this sub-feature is roughly symmetrical to the reasons for the in parameters - avoiding copying, but on the returning side. 在此功能之前,方法或索引器有两个选项: 1) 按引用返回并公开给可能的突变或 2) 按导致复制的值返回。Prior to this feature, a method or an indexer had two options: 1) return by reference and be exposed to possible mutations or 2) return by value which results in copying.

解决方案 (ref readonly 返回) Solution (ref readonly returns)

此功能允许成员按引用返回变量,而不会将它们公开给突变。The feature allows a member to return variables by reference without exposing them to mutations.

声明 ref readonly 返回成员Declaring ref readonly returning members

返回签名的修饰符的组合 ref readonly 用于指示成员返回 readonly 引用。A combination of modifiers ref readonly on the return signature is used to to indicate that the member returns a readonly reference.

对于所有目的, ref readonly 成员都被视为与 readonly readonly 字段和参数类似的变量 inFor all purposes a ref readonly member is treated as a readonly variable - similar to readonly fields and in parameters.

对于具有结构类型的成员的示例字段,它们都 ref readonly 递归归类为 readonly 变量。For example fields of ref readonly member which has a struct type are all recursively classified as readonly variables. -允许将它们作为 in 参数传递,但不能作为参数 ref 传递 out- It is permitted to pass them as in arguments, but not as ref or out arguments.

ref readonly Guid Method1()
{
}

Method2(in Method1()); // valid. Can pass as `in` argument.

Method3(ref Method1()); // not valid. Cannot pass as `ref` argument
  • ref readonly 允许在同一位置返回允许返回 refref readonly returns are allowed in the same places were ref returns are allowed. 这包括索引器、委托、lambda、本地函数。This includes indexers, delegates, lambdas, local functions.

  • 不允许重载 on ref / ref readonly /时差。It is not permitted to overload on ref/ref readonly / differences.

  • 允许重载普通 byval 并 ref readonly 返回差异。It is permitted to overload on ordinary byval and ref readonly return differences.

  • 出于 OHI 的目的 (重载、隐藏、实现) , ref readonly 与不同 refFor the purpose of OHI (Overloading, Hiding, Implementing), ref readonly is similar but distinct from ref. 例如,重写一个方法的方法 ref readonly 本身必须为 ref readonly ,且具有标识可转换的类型。For example the a method that overrides ref readonly one, must itself be ref readonly and have identity-convertible type.

  • 出于委托/lambda/方法组转换的目的, ref readonly 类似但不同于 refFor the purpose of delegate/lambda/method group conversions, ref readonly is similar but distinct from ref. Lambda 和适用的方法组转换候选项必须匹配 ref readonly 目标委托的返回,使其 ref readonly 返回标识可转换的类型。Lambdas and applicable method group conversion candidates have to match ref readonly return of the target delegate with ref readonly return of the type that is identity-convertible.

  • 出于泛型方差的目的, ref readonly 返回为 nonvariant。For the purpose of generic variance, ref readonly returns are nonvariant.

注意: ref readonly 对于具有引用类型或基元类型的返回,不会出现警告。NOTE: There are no warnings on ref readonly returns that have reference or primitives types. 通常,它可能是毫无意义的,但在某些情况下,用户必须/希望将基元作为传递 inIt may be pointless in general, but in some cases user must/want to pass primitives as in. 示例-重写泛型方法 ref readonly T Method() ,如在 T 将替换为时 intExamples - overriding a generic method like ref readonly T Method() when T was substituted to be int.

可以让分析器在低效使用退货的情况下发出警告 ref readonly ,但这种分析的规则过于模糊,无法成为语言规范的一部分。It is conceivable to have an analyzer that warns in cases of inefficient use of ref readonly returns, but the rules for such analysis would be too fuzzy to be a part of a language specification.

ref readonly 成员返回Returning from ref readonly members

在方法体中,语法与常规引用返回相同。Inside the method body the syntax is the same as with regular ref returns. readonly将从包含方法中推断。The readonly will be inferred from the containing method.

动机是不 return ref readonly <expression> 必要的长时间,只允许部件上的不匹配 readonly 始终会导致错误。The motivation is that return ref readonly <expression> is unnecessary long and only allows for mismatches on the readonly part that would always result in errors. ref但是,与通过严格的别名与按值传递的其他方案的一致性需要。The ref is, however, required for consistency with other scenarios where something is passed via strict aliasing vs. by value.

与带参数的情况不同 inref readonly 返回从不通过本地副本返回的。Unlike the case with in parameters, ref readonly returns never return via a local copy. 考虑到,在返回此类做法后,复制会立即停止存在是毫无意义的。Considering that the copy would cease to exist immediately upon returning such practice would be pointless and dangerous. 因此 ref readonly ,返回始终是直接引用。Therefore ref readonly returns are always direct references.

示例:Example:

struct ImmutableArray<T>
{
    private readonly T[] array;

    public ref readonly T ItemRef(int i)
    {
        // returning a readonly reference to an array element
        return ref this.array[i];
    }
}

  • 的参数 return ref 必须是 现有规则 (LValue) An argument of return ref must be an LValue (existing rule)
  • 的参数 return ref 必须是 "safe to return" (现有规则) An argument of return ref must be "safe to return" (existing rule)
  • ref readonly 成员中,的参数 return ref 不需要是可写 的。In a ref readonly member an argument of return ref is not required to be writeable . 例如,此类成员可以引用只读字段或其 in 参数之一。For example such member can ref-return a readonly field or one of its in parameters.

返回规则的安全。Safe to Return rules.

对于引用的正常返回规则也适用于 readonly 引用。Normal safe to return rules for references will apply to readonly references as well.

请注意, ref readonly 可以从常规的 ref 本地/参数/返回,但不能通过其他方法获取。Note that a ref readonly can be obtained from a regular ref local/parameter/return, but not the other way around. 否则,返回的安全性的 ref readonly 推断方式与常规 ref 返回相同。Otherwise the safety of ref readonly returns is inferred the same way as for regular ref returns.

考虑到右可以作为参数传递 in 并返回,因为 ref readonly 我们需要一个以上的规则- 右不安全地按引用返回Considering that RValues can be passed as in parameter and returned as ref readonly we need one more rule - RValues are not safe-to-return by reference.

当右值通过副本传递给某个 in 参数,然后以形式返回时,请考虑这种情况 ref readonlyConsider the situation when an RValue is passed to an in parameter via a copy and then returned back in a form of a ref readonly. 在调用方的上下文中,此类调用的结果是对本地数据的引用,因此不安全返回。In the context of the caller the result of such invocation is a reference to local data and as such is unsafe to return. 右不安全返回后,现有规则 #6 已经处理了这种情况。Once RValues are not safe to return, the existing rule #6 already handles this case.

示例:Example:

ref readonly Vector3 Test1()
{
    // can pass an RValue as "in" (via a temp copy)
    // but the result is not safe to return
    // because the RValue argument was not safe to return by reference
    return ref Test2(default(Vector3));
}

ref readonly Vector3 Test2(in Vector3 r)
{
    // this is ok, r is returnable
    return ref r;
}

更新的 safe to return 规则:Updated safe to return rules:

  1. 可以安全地返回堆中的变量引用refs to variables on the heap are safe to return
  2. 可以安全地返回 in ref/in 参数参数自然只能以 readonly 形式返回。ref/in parameters are safe to return in parameters naturally can only be returned as readonly.
  3. out 参数可以安全地返回 (但必须明确赋值,因为现在已经是如此) out parameters are safe to return (but must be definitely assigned, as is already the case today)
  4. 只要接收方可以安全返回,就可以安全地返回实例结构字段instance struct fields are safe to return as long as the receiver is safe to return
  5. "this" 不能安全地从结构成员返回'this' is not safe to return from struct members
  6. 如果作为形参传递给该方法的所有 refs/out 都可以安全返回,则从另一种方法返回的 ref 将是安全的。 具体说来,无论接收方是结构、类还是类型为泛型类型参数,它都是不相关的。a ref, returned from another method is safe to return if all refs/outs passed to that method as formal parameters were safe to return. Specifically it is irrelevant if receiver is safe to return, regardless whether receiver is a struct, class or typed as a generic type parameter.
  7. 右无法安全地按引用返回。 具体说来,右可以作为 in 参数进行传递。RValues are not safe to return by reference. Specifically RValues are safe to pass as in parameters.

注意:当涉及引用类型和引用操作时,有关于安全的返回的其他规则。NOTE: There are additional rules regarding safety of returns that come into play when ref-like types and ref-reassignments are involved. 规则同样适用于 refref readonly 成员,因此不在此处提及。The rules equally apply to ref and ref readonly members and therefore are not mentioned here.

别名行为。Aliasing behavior.

ref readonly 除了只读) 之外,成员提供与普通成员相同的别名行为 ref (。ref readonly members provide the same aliasing behavior as ordinary ref members (except for being readonly). 因此,为了捕获 lambda、async、迭代器、stack 溢出等 .。。同样的限制也适用。Therefore for the purpose of capturing in lambdas, async, iterators, stack spilling etc... the same restrictions apply. 亦.- I.E. 由于无法捕获实际引用和成员计算的副作用性质,因此不允许这样做。due to inability to capture the actual references and due to side-effecting nature of member evaluation such scenarios are disallowed.

如果返回的是常规结构方法的接收方,则允许并需要使用该方法进行复制 ref readonly ,此方法采用普通的可 this 写引用。It is permitted and required to make a copy when ref readonly return is a receiver of regular struct methods, which take this as an ordinary writeable reference. 在这种情况下,在所有情况下,都会对只读变量应用本地副本。Historically in all cases where such invocations are applied to readonly variable a local copy is made.

元数据表示形式。Metadata representation.

System.Runtime.CompilerServices.IsReadOnlyAttribute 应用于返回 byref 返回方法的时,这意味着该方法返回 readonly 引用。When System.Runtime.CompilerServices.IsReadOnlyAttribute is applied to the return of a byref returning method, it means that the method returns a readonly reference.

此外,此类方法的结果签名 (并且只有) 的方法必须具有 modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute]In addition, the result signature of such methods (and only those methods) must have modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute].

动机:这是为了确保 readonly 在通过返回调用方法时现有编译器不能直接忽略 ref readonlyMotivation: this is to ensure that existing compilers cannot simply ignore readonly when invoking methods with ref readonly returns

只读结构Readonly structs

简而言之,一项功能,它使 this 结构的所有实例成员(构造函数除外)的参数成为参数 inIn short - a feature that makes this parameter of all instance members of a struct, except for constructors, an in parameter.

动机Motivation

编译器必须假定对结构实例的任何方法调用都可以修改该实例。Compiler must assume that any method call on a struct instance may modify the instance. 确实会将可写引用作为参数传递给方法 this ,并完全启用此行为。Indeed a writeable reference is passed to the method as this parameter and fully enables this behavior. 若要允许对变量进行此类调用 readonly ,请将调用应用到临时副本。To allow such invocations on readonly variables, the invocations are applied to temp copies. 这可能是 unintuitive 的,有时会 readonly 出于性能原因强制用户放弃。That could be unintuitive and sometimes forces people to abandon readonly for performance reasons.
示例:https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-fields/Example: https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-fields/

添加了对参数的支持 inref readonly 返回了防御性复制的问题会变得更糟,因为 readonly 变量将变得更常见。After adding support for in parameters and ref readonly returns the problem of defensive copying will get worse since readonly variables will become more common.

解决方案Solution

允许 readonly 在结构声明上执行修饰符,这会导致 this 被视为 in 除构造函数之外的所有结构实例方法上的参数。Allow readonly modifier on struct declarations which would result in this being treated as in parameter on all struct instance methods except for constructors.

static void Test(in Vector3 v1)
{
    // no need to make a copy of v1 since Vector3 is a readonly struct
    System.Console.WriteLine(v1.ToString());
}

readonly struct Vector3
{
    . . .

    public override string ToString()
    {
        // not OK!!  `this` is an `in` parameter
        foo(ref this.X);

        // OK
        return $"X: {X}, Y: {Y}, Z: {Z}";
    }
}

对 readonly 结构成员的限制Restrictions on members of readonly struct

  • 只读结构的实例字段必须是只读的。Instance fields of a readonly struct must be readonly.
    动机: 只能从外部写入,而不能通过成员写入。Motivation: can only be written to externally, but not through members.
  • 只读结构的实例 autoproperties 必须是只是获取的。Instance autoproperties of a readonly struct must be get-only.
    动机: 对实例字段的限制的影响。Motivation: consequence of restriction on instance fields.
  • Readonly 结构不能声明类似于字段的事件。Readonly struct may not declare field-like events.
    动机: 对实例字段的限制的影响。Motivation: consequence of restriction on instance fields.

元数据表示形式。Metadata representation.

System.Runtime.CompilerServices.IsReadOnlyAttribute 应用于值类型时,它表示该类型为 readonly structWhen System.Runtime.CompilerServices.IsReadOnlyAttribute is applied to a value type, it means that the type is a readonly struct.

具体而言:In particular:

  • IsReadOnlyAttribute类型标识不重要。The identity of the IsReadOnlyAttribute type is unimportant. 事实上,如果需要,它可以在包含程序集中由编译器嵌入。In fact it can be embedded by the compiler in the containing assembly if needed.

ref/in 扩展方法ref/in extension methods

实际上现有的提议 (https://github.com/dotnet/roslyn/issues/165) 和对应的原型 PR (https://github.com/dotnet/roslyn/pull/15650) 。There is actually an existing proposal (https://github.com/dotnet/roslyn/issues/165) and corresponding prototype PR (https://github.com/dotnet/roslyn/pull/15650). 我只想确认这种想法并不完全新。I just want to acknowledge that this idea is not entirely new. 但在这里,这是一项相关性,因为 ref readonly 完美地删除了有关此类方法的最争用资源问题-应如何处理右值接收器。It is, however, relevant here since ref readonly elegantly removes the most contentious issue about such methods - what to do with RValue receivers.

一般思路允许扩展方法 this 按引用获取参数,前提是该类型已知为结构类型。The general idea is allowing extension methods to take the this parameter by reference, as long as the type is known to be a struct type.

public static void Extension(ref this Guid self)
{
    // do something
}

编写这种扩展方法的原因主要是:The reasons for writing such extension methods are primarily:

  1. 接收方是大型结构时避免复制Avoid copying when receiver is a large struct
  2. 允许在结构上改变扩展方法Allow mutating extension methods on structs

我们不想在类上允许此操作的原因The reasons why we do not want to allow this on classes

  1. 它的用途非常有限。It would be of very limited purpose.
  2. 这会中断长时间的固定,即方法调用无法 null null 在调用后变为非接收器。It would break long standing invariant that a method call cannot turn non-null receiver to become null after invocation.

事实上, null null 除非 显式 赋值或由或传递,否则当前无法将非变量 ref 变为 outIn fact, currently a non-null variable cannot become null unless explicitly assigned or passed by ref or out. 这可以极大帮助提高可读性或其他形式的 "这在此处为空"。That greatly aids readability or other forms of "can this be a null here" analysis. 3. 这种情况很难与 "计算一次" 对 null 条件访问语义进行协调。It would be hard to reconcile with "evaluate once" semantics of null-conditional accesses. 示例: obj.stringField?.RefExtension(...) -需要捕获的副本 stringField 以使 null 检查有意义,但随后 this 不会将对 RefExtension 中的赋值反映回字段。Example: obj.stringField?.RefExtension(...) - need to capture a copy of stringField to make the null check meaningful, but then assignments to this inside RefExtension would not be reflected back to the field.

在采用第一个参数的 结构 上声明扩展方法的能力是长时间的请求。An ability to declare extension methods on structs that take the first argument by reference was a long-standing request. 其中一个阻止性注意事项是 "如果接收方不是左值,会发生什么情况?"。One of the blocking consideration was "what happens if receiver is not an LValue?".

  • 有一个引用者,还可以将任何扩展方法作为静态方法调用 (有时,它是解析歧义) 的唯一方法。There is a precedent that any extension method could also be called as a static method (sometimes it is the only way to resolve ambiguity). 它会规定应禁止 RValue 接收方。It would dictate that RValue receivers should be disallowed.
  • 另一方面,在涉及结构实例方法时,可以在类似情况下对副本进行调用。On the other hand there is a practice of making invocation on a copy in similar situations when struct instance methods are involved.

"隐式复制" 之所以存在的原因是因为大多数结构方法不会实际修改结构,而不能指示。The reason why the "implicit copying" exists is because the majority of struct methods do not actually modify the struct while not being able to indicate that. 因此,最可行的解决方案是只对副本进行调用,但这种做法已知会对性能造成损害并引发 bug。Therefore the most practical solution was to just make the invocation on a copy, but this practice is known for harming performance and causing bugs.

现在,有了参数的可用性 in ,扩展也有可能指示意向。Now, with availability of in parameters, it is possible for an extension to signal the intent. 因此,可通过要求在可 ref 写接收方中调用扩展来解析难题,并在必要时 in 允许隐式复制。Therefore the conundrum can be resolved by requiring ref extensions to be called with writeable receivers while in extensions permit implicit copying if necessary.

// this can be called on either RValue or an LValue
public static void Reader(in this Guid self)
{
    // do something nonmutating.
    WriteLine(self == default(Guid));
}

// this can be called only on an LValue
public static void Mutator(ref this Guid self)
{
    // can mutate self
    self = new Guid();
}

in 扩展和泛型。in extensions and generics.

ref扩展方法的用途是直接或通过调用变异成员来改变接收方。The purpose of ref extension methods is to mutate the receiver directly or by invoking mutating members. 因此, ref this T 只要 T 约束为结构,就允许使用扩展。Therefore ref this T extensions are allowed as long as T is constrained to be a struct.

另一种 in 方法是专门用于减少隐式复制。On the other hand in extension methods exist specifically to reduce implicit copying. 但是,任何使用 in T 参数都必须通过接口成员来完成。However any use of an in T parameter will have to be done through an interface member. 由于所有接口成员都被视为改变,因此任何此类使用都需要复制。Since all interface members are considered mutating, any such use would require a copy. -不是减少复制,而是相反。- Instead of reducing copying, the effect would be the opposite. 因此, in this TT 是泛型类型参数,而不考虑约束时,不允许这样做。Therefore in this T is not allowed when T is a generic type parameter regardless of constraints.

(概述) 的扩展方法的有效种类:Valid kinds of extension methods (recap):

this现在允许在扩展方法中使用以下形式的声明:The following forms of this declaration in an extension method are now allowed:

  1. this T arg -常规 byval 扩展。this T arg - regular byval extension. (现有事例) (existing case)
  • T 可以是任何类型,包括引用类型或类型参数。T can be any type, including reference types or type parameters. 实例在调用后将是相同的变量。Instance will be the same variable after the call. 允许对 此参数转换 类型进行隐式转换。Allows implicit conversions of this-argument-conversion kind. 可在右上调用。Can be called on RValues.

  • in this T self - in 拓.in this T self - in extension. T 必须是实际的结构类型。T must be an actual struct type. 实例在调用后将是相同的变量。Instance will be the same variable after the call. 允许对 此参数转换 类型进行隐式转换。Allows implicit conversions of this-argument-conversion kind. 可以在右上调用,如有需要,可以在 temp 上调用 () 。Can be called on RValues (may be invoked on a temp if needed).

  • ref this T self - ref 拓.ref this T self - ref extension. T 必须是结构类型或约束为结构的泛型类型参数。T must be a struct type or a generic type parameter constrained to be a struct. 实例可以通过调用来写入。Instance may be written to by the invocation. 仅允许标识转换。Allows only identity conversions. 必须在可写的左值上调用。Must be called on writeable LValue. (从不通过 temp) 调用。(never invoked via a temp).

Readonly 引用局部变量。Readonly ref locals.

推动.Motivation.

一旦 ref readonly 引入成员后,就清楚了需要将它们与适当类型的局部成对使用。Once ref readonly members were introduced, it was clear from the use that they need to be paired with appropriate kind of local. 成员的计算可能会产生或观察到副作用,因此,如果必须多次使用该结果,则需要对其进行存储。Evaluation of a member may produce or observe side effects, therefore if the result must be used more than once, it needs to be stored. ref由于不能为普通局部变量分配引用,因此不会对此提供帮助 readonlyOrdinary ref locals do not help here since they cannot be assigned a readonly reference.

解决方案.Solution.

允许声明 ref readonly 局部变量。Allow declaring ref readonly locals. 这是一种 ref 不能写入的新局部变量。This is a new kind of ref locals that is not writeable. 因此, ref readonly 局部变量可以接受对只读变量的引用,而无需公开这些变量来写入。As a result ref readonly locals can accept references to readonly variables without exposing these variables to writes.

声明和使用 ref readonly 局部变量。Declaring and using ref readonly locals.

此类局部变量的语法 ref readonly 在特定顺序) 中使用声明站点 (的修饰符。The syntax of such locals uses ref readonly modifiers at declaration site (in that specific order). 与普通 ref 局部变量类似, ref readonly 局部变量必须在声明中进行引用初始化。Similarly to ordinary ref locals, ref readonly locals must be ref-initialized at declaration. 与常规 ref 局部变量不同, ref readonly 局部变量可以引用 readonly 左值,如 in 参数、 readonly 字段和 ref readonly 方法。Unlike regular ref locals, ref readonly locals can refer to readonly LValues like in parameters, readonly fields, ref readonly methods.

对于所有目的, ref readonly 本地都被视为 readonly 变量。For all purposes a ref readonly local is treated as a readonly variable. 对使用的大多数限制都与 readonly 字段或 in 参数相同。Most of the restrictions on the use are the same as with readonly fields or in parameters.

例如, in 具有结构类型的参数的字段以递归方式分类为 readonly 变量。For example fields of an in parameter which has a struct type are all recursively classified as readonly variables .

static readonly ref Vector3 M1() => . . .

static readonly ref Vector3 M1_Trace()
{
    // OK
    ref readonly var r1 = ref M1();

    // Not valid. Need an LValue
    ref readonly Vector3 r2 = ref default(Vector3);

    // Not valid. r1 is readonly.
    Mutate(ref r1);

    // OK.
    Print(in r1);

    // OK.
    return ref r1;
}

局部变量使用限制 ref readonlyRestrictions on use of ref readonly locals

除了其 readonly 性质外, ref readonly 局部变量的行为类似于普通 ref 局部变量,并且受到完全相同的限制。Except for their readonly nature, ref readonly locals behave like ordinary ref locals and are subject to exactly same restrictions.
有关与在闭包中捕获相关的限制, async 则声明为方法或 safe-to-return 分析同样适用于 ref readonly 局部变量。For example restrictions related to capturing in closures, declaring in async methods or the safe-to-return analysis equally applies to ref readonly locals.

三元 ref 表达式。Ternary ref expressions. (称为 "条件左值" ) (aka "Conditional LValues")

动机Motivation

使用 refref readonly 局部变量时,需要根据条件将此类局部变量替换为一个或另一个目标变量。Use of ref and ref readonly locals exposed a need to ref-initialize such locals with one or another target variable based on a condition.

典型的解决方法是引入如下方法:A typical workaround is to introduce a method like:

ref T Choice(bool condition, ref T consequence, ref T alternative)
{
    if (condition)
    {
         return ref consequence;
    }
    else
    {
         return ref alternative;
    }
}

请注意, Choice 不会完全替换三元,因为 所有 参数都必须在调用站点上进行计算,这会导致 unintuitive 的行为和 bug。Note that Choice is not an exact replacement of a ternary since all arguments must be evaluated at the call site, which was leading to unintuitive behavior and bugs.

以下操作不会按预期方式工作:The following will not work as expected:

    // will crash with NRE because 'arr[0]' will be executed unconditionally
    ref var r = ref Choice(arr != null, ref arr[0], ref otherArr[0]);

解决方案Solution

允许特殊类型的条件表达式,该表达式的计算结果为基于条件的左值参数之一的引用。Allow special kind of conditional expression that evaluates to a reference to one of LValue argument based on a condition.

使用 ref 三元表达式。Using ref ternary expression.

ref条件表达式风格的语法是<condition> ? ref <consequence> : ref <alternative>;The syntax for the ref flavor of a conditional expression is <condition> ? ref <consequence> : ref <alternative>;

与仅限普通条件表达式一样, <consequence><alternative> 根据布尔条件表达式的结果计算。Just like with the ordinary conditional expression only <consequence> or <alternative> is evaluated depending on result of the boolean condition expression.

与普通的条件表达式不同, ref 条件表达式:Unlike ordinary conditional expression, ref conditional expression:

  • 要求 <consequence><alternative> 都是左值。requires that <consequence> and <alternative> are LValues.
  • ref 条件表达式本身是左值和ref conditional expression itself is an LValue and
  • ref 如果 <consequence><alternative> 都可写,则条件表达式是可写的左值ref conditional expression is writeable if both <consequence> and <alternative> are writeable LValues

示例:Examples:
ref 三元是左值,因此它可以通过引用传递/赋值/返回;ref ternary is an LValue and as such it can be passed/assigned/returned by reference;

     // pass by reference
     foo(ref (arr != null ? ref arr[0]: ref otherArr[0]));

     // return by reference
     return ref (arr != null ? ref arr[0]: ref otherArr[0]);

作为 LValue,还可以分配给。Being an LValue, it can also be assigned to.

     // assign to
     (arr != null ? ref arr[0]: ref otherArr[0]) = 1;

     // error. readOnlyField is readonly and thus conditional expression is readonly
     (arr != null ? ref arr[0]: ref obj.readOnlyField) = 1;

可用作方法调用的接收方,并在必要时跳过复制。Can be used as a receiver of a method call and skip copying if necessary.

     // no copies
     (arr != null ? ref arr[0]: ref otherArr[0]).StructMethod();

     // invoked on a copy.
     // The receiver is `readonly` because readOnlyField is readonly.
     (arr != null ? ref arr[0]: ref obj.readOnlyField).StructMethod();

     // no copies. `ReadonlyStructMethod` is a method on a `readonly` struct
     // and can be invoked directly on a readonly receiver
     (arr != null ? ref arr[0]: ref obj.readOnlyField).ReadonlyStructMethod();

ref 可以在常规 (中使用三元) 上下文。ref ternary can be used in a regular (not ref) context as well.

     // only an example
     // a regular ternary could work here just the same
     int x = (arr != null ? ref arr[0]: ref otherArr[0]);

缺点Drawbacks

对于引用和只读引用的增强支持,我可以看到两个主要参数:I can see two major arguments against enhanced support for references and readonly references:

  1. 此处解决的问题很旧。The problems that are solved here are very old. 为什么现在突然解决它们,尤其是因为它不会帮助现有代码?Why suddenly solve them now, especially since it would not help existing code?

当我们发现新域中使用了 c # 和 .Net 时,一些问题将变得更加突出。As we find C# and .Net used in new domains, some problems become more prominent.
作为比计算开销平均更重要的环境的示例,我可以列出As examples of environments that are more critical than average about computation overheads, I can list

  • 用于计算费用和响应能力的云/数据中心方案是一种竞争优势。cloud/datacenter scenarios where computation is billed for and responsiveness is a competitive advantage.
  • 游戏/VR/AR,其中包含对延迟的软实时要求Games/VR/AR with soft-realtime requirements on latencies

此功能不会牺牲任何现有的优势(如类型安全),同时允许在某些常见情况下降低开销。This feature does not sacrifice any of the existing strengths such as type-safety, while allowing to lower overheads in some common scenarios.

  1. 我们是否可以合理保证被调用方将在选择协定时由规则运行 readonlyCan we reasonably guarantee that the callee will play by the rules when it opts into readonly contracts?

使用时,我们有类似的信任关系 outWe have similar trust when using out. 的实现不正确 out 可能会导致未指定的行为,但实际上很少发生。Incorrect implementation of out can cause unspecified behavior, but in reality it rarely happens.

使正式验证规则熟悉 ref readonly 会进一步降低信任问题。Making the formal verification rules familiar with ref readonly would further mitigate the trust issue.

备选方法Alternatives

主要的竞争设计实际上是 "无任何操作"。The main competing design is really "do nothing".

未解决的问题Unresolved questions

设计会议Design meetings

https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-02-22.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-03-01.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-08-28.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-25.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-27.mdhttps://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-02-22.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-03-01.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-08-28.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-25.md https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-09-27.md