ref readonly 的 C# 行为

Posted

技术标签:

【中文标题】ref readonly 的 C# 行为【英文标题】:C# Behavior of ref readonly 【发布时间】:2019-03-29 06:27:10 【问题描述】:

我正在阅读有关 C# 7.2 here 的文档,我在 ref readonly 方面遇到了这个问题:

编译器强制调用者不能修改引用。尝试直接分配值会产生编译时错误。但是,编译器无法知道是否有任何成员方法修改了结构的状态。为确保对象不被修改,编译器创建一个副本并使用该副本调用成员引用。任何修改都是针对该防御性副本。

这给我(可能还有其他一些人)带来了一些困惑,所以我现在想澄清一下这种行为。假设我有一个这样定义的结构:

public struct Point3D

    private static Point3D origin = new Point3D(0,0,0);

    public static ref readonly Point3D Origin => ref origin;

    public int X  get; set; 
    public int Y  get; set; 
    public int Z  get; set; 

    public static void ChangeOrigin(int x = 0, int y = 0, int z = 0)
    
        origin = new Point3D(x, y, z);
    

现在,假设我使用ref readonly 得到Point3D.Origin 并对其进行了修改:

ref readonly var origin = ref Point3D.Origin;
var originValue = Point3D.Origin;
Point3D.ChangeOrigin(1, 1, 1);

Console.WriteLine("Origin is: (0, 1, 2)", origin.X, origin.Y, origin.Z);
Console.WriteLine("Origin is: (0, 1, 2)", originValue.X, originValue.Y, originValue.Z);

运行这段代码的结果是:

Origin is: (1, 1, 1)
Origin is: (0, 0, 0)

这是意料之中的。 origin 中的值在我调用 ChangeOrigin 时更新,而 originValue 中的值被复制,因此不会更改。我的问题是关于上面提到的“防御性副本”。为什么这是必要的? origin 中的值不能在不调用编译器错误的情况下更改,并且引用会在Point3D.Origin 更新时正确更新,所以有什么理由需要对象的额外副本,这是我通过阅读文档收集的内容,没更新?

【问题讨论】:

您要求我们告诉您这段代码的结果是什么?你不能通过运行代码找到它吗?我们是人,不是计算机。 @mason 我之所以问这个问题,部分原因是我对语言功能感到好奇,还没有升级到 7.2,部分原因是我认为有人会比我更了解它。另外,因为我认为这对将来对此有疑问的人会有所帮助 @mason 是的,我确实可以运行代码。但这并不能告诉我为什么,代码以某种方式运行,现在可以了。这就是提出这个问题的真正动力。而且,如果你知道为什么代码会这样运行,那么你就不需要运行它了。 @mason 简而言之,我对代码的作用不太好奇,而对代码的行为方式为何如此好奇 与 SpinLock 相关的 github 问题。 github.com/dotnet/roslyn/issues/17310 【参考方案1】:

您只能在以下情况下为 readonly 字段赋值:

在声明中初始化变量时。

C# 示例:

 public readonly int y = 5;

在包含实例字段声明的类的实例构造函数中。

在包含静态字段声明的类的静态构造函数中。

这些构造函数上下文也是唯一可以将readonly 字段作为outref 参数传递的上下文。

如果您使用类似以下示例的语句:

p2.y = 66; // Error

你会得到编译器错误信息:

只读字段不能分配给(除了在构造函数或 变量初始化器)

ref 返回上的 readonly 修饰符表示无法修改返回的引用。以下示例返回对原点的引用。它使用readonly 修饰符来指示调用者不能修改来源:

private static readonly Point origin = new Point(0, 0);
public static ref readonly Point Origin => ref origin;

返回的类型不必是readonly structref可以返回的任何类型都可以ref readonly返回。

这一切都直接来自文档,我希望它为您澄清!编码愉快!

所以如果你在构造函数中设置(1, 1, 1),就会是(1, 1, 1)用不同的方法调用改变,如上,你得到(0, 0, 0)

【讨论】:

在这种情况下,他们所说的所有这些“防御性复制”业务是什么意思?为什么在请求不可变引用时创建对象的副本? 把副本想象成一个盾牌,如果你试图修改基础,它将修改defensive copy,基本上使你的修改无效,因为当你使用引用时它会抓住@987654338 @ 那是不变的。您可以继续更改防御副本,但您永远无法通过调用获得它,并且您无法更改 base 顺便说一句,很好地抓住了编译错误。我直接从 Microsoft 文档中提取了我的示例,该文档无法编译 >_ 他们可能是在编写 Windows 10 更新的同时编写的。 ;)

以上是关于ref readonly 的 C# 行为的主要内容,如果未能解决你的问题,请参考以下文章

C# const和readonly修饰符的区别

C# ref 奇怪的行为[重复]

C# - 通过 REF 从异步方法回调更新变量 - WebClient 类

C# readonly vs Java final

C#中关键字ref和out的区别

C#中的const形参