只读实例成员

注意

本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

支持者问题:<https://github.com/dotnet/csharplang/issues/1710>

总结

提供一种方法来指定结构体上的单个实例成员不修改状态,就像 readonly struct 指定实例成员不修改状态一样。

值得注意的是,只读实例成员 != 纯实例成员实例成员保证不会修改任何状态。 readonly 实例成员只能保证不修改实例状态。

readonly struct 上的所有实例成员都可以视为隐式 只读实例成员 在非只读结构体上声明的 显式只读实例成员的行为方式相同。 例如,如果调用实例成员(在当前实例或实例字段上)且该实例本身不是只读的,它们仍会创建隐藏副本。

动机

在 C# 8.0 之前,用户能够创建 readonly struct 类型,编译器强制其所有字段为只读(按扩展名,没有实例成员修改状态)。 但是,在某些情况下,你有一个现有 API,它公开可访问的字段,或者混合了可变和非可变成员。 在这种情况下,不能将类型标记为 readonly(这将是一个重大更改)。

这通常没有太大的影响,除非是在 in 参数的情况下。 利用非只读结构 in 参数,编译器将为每个实例成员调用创建参数的副本,因为它无法保证调用不会修改内部状态。 这可能会导致大量副本,且整体性能会比直接按值传递结构要更差。 有关示例,请参见 sharplab 上的这个代码

其他一些可能发生隐藏副本的情况包括static readonly字段文本。 如果将来得到支持,可直接复制的常量最终会处于相同的境地;也就是说,如果结构未标记为 readonly,那么它们当前在进行实例成员调用时都需要完整复制。

设计

允许用户指定实例成员本身为 readonly,并且不会修改实例的状态(当然,所有适当的验证将由编译器完成)。 例如:

public struct Vector2
{
    public float x;
    public float y;

    public readonly float GetLengthReadonly()
    {
        return MathF.Sqrt(LengthSquared);
    }

    public float GetLength()
    {
        return MathF.Sqrt(LengthSquared);
    }

    public readonly float GetLengthIllegal()
    {
        var tmp = MathF.Sqrt(LengthSquared);

        x = tmp;    // Compiler error, cannot write x
        y = tmp;    // Compiler error, cannot write y

        return tmp;
    }

    public readonly float LengthSquared
    {
        get
        {
            return (x * x) +
                   (y * y);
        }
    }
}

public static class MyClass
{
    public static float ExistingBehavior(in Vector2 vector)
    {
        // This code causes a hidden copy, the compiler effectively emits:
        //    var tmpVector = vector;
        //    return tmpVector.GetLength();
        //
        // This is done because the compiler doesn't know that `GetLength()`
        // won't mutate `vector`.

        return vector.GetLength();
    }

    public static float ReadonlyBehavior(in Vector2 vector)
    {
        // This code is emitted exactly as listed. There are no hidden
        // copies as the `readonly` modifier indicates that the method
        // won't mutate `vector`.

        return vector.GetLengthReadonly();
    }
}

可属性访问器应用“只读”,以指示访问器中的 this 不会被修改。 以下示例具有只读设置器,因为这些访问器修改了成员字段的状态,但不改变该成员字段的值。

public readonly int Prop1
{
    get
    {
        return this._store["Prop1"];
    }
    set
    {
        this._store["Prop1"] = value;
    }
}

readonly 应用于属性语法时,这意味着所有访问器都是 readonly

public readonly int Prop2
{
    get
    {
        return this._store["Prop2"];
    }
    set
    {
        this._store["Prop2"] = value;
    }
}

“只读”只能应用于不修改包含类型的访问器。

public int Prop3
{
    readonly get
    {
        return this._prop3;
    }
    set
    {
        this._prop3 = value;
    }
}

Readonly 可以应用于某些自动实现的属性,但不会产生有意义的影响。 无论是否存在 readonly 关键字,编译器都会将所有自动实现的 getter 视为只读属性。

// Allowed
public readonly int Prop4 { get; }
public int Prop5 { readonly get; set; }

// Not allowed
public int Prop6 { readonly get; }
public readonly int Prop7 { get; set; }
public int Prop8 { get; readonly set; }

“只读”可以应用于手动实现的事件,但不能应用于类似字段的事件。 “只读”不能应用于单个事件访问器(添加/删除)。

// Allowed
public readonly event Action<EventArgs> Event1
{
    add { }
    remove { }
}

// Not allowed
public readonly event Action<EventArgs> Event2;
public event Action<EventArgs> Event3
{
    readonly add { }
    readonly remove { }
}
public static readonly event Event4
{
    add { }
    remove { }
}

其他语法示例:

  • 表达式主体定义的成员:public readonly float ExpressionBodiedMember => (x * x) + (y * y);
  • 泛型约束:public readonly void GenericMethod<T>(T value) where T : struct { }

编译器会像往常一样发射实例成员,并额外发出一个编译器识别的属性,表明实例成员不修改状态。 这实际上会使隐藏的 this 参数变为 in T,而不是 ref T

这样,用户就可以安全地调用上述实例方法,而无需编译器制作副本。

这些限制包括:

  • readonly 修饰符不能应用于静态方法、构造函数或析构函数。
  • readonly 修饰符不能应用于委托。
  • readonly 修饰符不能应用于类或接口的成员。

缺点

与现在的 readonly struct 方法存在同样的缺点。 某些代码仍可能导致隐藏副本。

备注

也可以使用属性或其他关键字。

这一提案与functional purity和/或constant expressions有些关联,但更像是它们的子集,这两者都有一些现有的建议。