【问题标题】:self mutating readonly struct field自变异只读结构字段
【发布时间】:2011-04-09 09:33:42
【问题描述】:

我知道Eric Lippert's blog post about situation 但我认为这是一种不同的情况,因为该领域会改变自身而不是其领域。如果 Enumerator 是只读的并且输出始终为 0,您如何解释调用 MoveNext()?

 class SomeClass
    {
        private List<int> list;
        private [readonly] List<int>.Enumerator enumerator;

        public SomeClass()
        {
            list = new List<int>() { 1, 2, 3 };
            enumerator = list.GetEnumerator();
        }

        public int ReadValue()
        {
            if (enumerator.MoveNext())
                return enumerator.Current;
            return -1;

        }
    }
static void Main()
    {
        SomeClass c = new SomeClass();
        int value;
        while ((value = c.ReadValue()) > -1)
            MessageBox.Show(value.ToString());
    }

【问题讨论】:

    标签: c# struct readonly


    【解决方案1】:

    我认为这是一种不同的情况

    你错了。这正是我在您引用的博客文章中描述的情况。

    在这里重复我的分析:结构上的每个非静态方法调用都带有一个名为“this”的“ref”参数。我们没有在参数列表中显示“ref this”参数,但它在那里,由编译器为您生成。 ref 传递的任何内容都必须是变量。由于 readonly 变量可能(在这种情况下,将会)被调用改变,我们必须确保 readonly 变量永远不会被 ref 传递。当您在只读结构上调用方法时,我们创建一个临时变量,将结构复制到临时变量,在方法调用上将 ref 作为“this”传递给临时变量,然后丢弃临时变量。这解释了您所看到的行为;由 MoveNext 引起的每个突变都发生在一个副本上,然后被丢弃。

    您能否解释一下为什么这种情况(与我在博客中描述的情况完全相同)有何不同?您认为枚举器有什么不同之处使它们与众不同?

    【讨论】:

    • 从“mutate”我理解改变变量引用,而不是内容。在 readonly object obj 这样的情况下,readonly 关键字保护引用变量的值,而不是对象本身。但现在我看到对于值类型,它保护了值。而且我不明白为什么改变结构需要制作新副本。为什么突变只读变量不会引发异常????非常感谢您的解释
    • @TakeMeAsAGuest:回复:动机:动机是让只读值类型实际上是只读的;你会怎么做呢?回复:我们是否复制 ref 类型变量的内容?不,正如我曾经说过的,改变只读的引用类型变量是不可能。编译器检测到这样做的尝试并在编译时禁止它,因此询问我们在运行时做什么是没有意义的。这种情况永远不会出现,因为这是不可能的。
    • 请原谅我,但这总结了值类型和您的决定有什么奇怪的地方。我永远不会让只读值发生变异,“至少会产生一些警告”当前的行为对我来说很奇怪。
    • @TakeMeAsAGuest:你还没有说你希望我们做什么,只是说你不喜欢我们实际做的事情。我明白你不喜欢它。我也不是特别喜欢。您希望看到的替代行为是什么?
    • @TakeMeAsAGuest:好的,例如,在只读结构变量上调用 ToString() 应该是警告、错误还是什么?这将在副本上调用 ToString() ;如果 ToString() 改变变量,它将改变副​​本。我们应该在警告中告诉用户,还是在只读结构变量上调用 ToString() 是非法的,还是什么?
    【解决方案2】:

    如果我理解您发布的 Eric Lippert 的博客文章,声明

    if(enumerator.MoveNext())
    

    首先制作枚举器的副本,在该副本上执行 MoveNext()。那个副本死了,下一行:

    return enumerator.Current;
    

    返回原始枚举器的 Current,而不是副本,这就是为什么你总是得到 0

    【讨论】:

    • 我仍然不明白为什么 NoveNext 创建一个副本,我没有将结构视为对象
    • 仍然无法理解为什么删除 readonly 会使其按预期执行。顺便说一句,如果将 List.Enumerator 的引用保存为 Enumerator (接口引用类型),它也会正常执行。
    • 它似乎为任何类型的引用制作了这个副本,无论是结构还是类。 :|
    • @TakeMeAsAGuest:假设存在一个方法static void DoMoveNext&lt;T&gt;(ref T it) where T: IEnumerator {T.MoveNext();},如果enumerator 不是readonly,那么调用DoMoveNext(ref enumerator); 将是合法的,并且可以按预期工作。如果它是只读的,编译器将标记一个错误。调用 enumerator.MoveNext() 等效于调用 DoMoveNext(ref enumerator);,除了一个区别:编译器不会标记错误,而是假装您编写了可以编译 var temp=enumerator; DoMoveNext(ref temp); 的内容,并希望这就是您的意思。
    【解决方案3】:

    readonly 关键字给 C# 编译器带来了困难。它无法知道 MoveNext() 和 Current 是否具有违反只读合约的副作用。 MoveNext() 当然可以。因此,要生成有效代码,它必须创建迭代器值的副本。它发生了两次,一次是为 MoveNext() 方法调用,一次是在读取 Current 属性时。您可以通过在程序上运行 ildasm.exe 轻松看到这一点,该副本在 Debug 版本中命名为 CS$0$0001。

    如果编译器至少为此代码生成警告,那就太好了。但是很难准确地做到,它确实需要知道该成员是否有副作用。它不知道。 方式有太多的结构类型具有没有副作用的属性获取器,所以总是生成警告是不可行的。

    需要让它知道的特性是 const 关键字,因为它在 C++ 中使用。可以将方法声明为 const 以表明它不会改变对象的状态。我严重怀疑该功能是否会进入 C# 语言,但编写 const 正确的代码并不是那么容易,坦率地说,有点像皮塔饼。

    【讨论】:

    • 感谢您的解释,很少有人能回答这个问题。我想要 c# 语言中的 const 关键字,编译器可以检查方法是否在琐碎的情况下改变状态,如果它不能,它只会给出错误。这将为编译器和 jit 优化器提供巨大的机会来更好地优化代码。如果您对我的其他问题提出您的想法,我会很高兴:stackoverflow.com/questions/5600623/… 您可以在重复的线程上回答。我不能放弃你,因为我的声誉低于 15
    • 我们不能给你那个。向 connect.microsoft.com 发布功能请求
    • 我仍然认为 MoveNext 不会改变结构,只是改变它的内部状态,而不是地址。
    • 它是一个结构体,它没有地址。这就是值类型的行为方式。您的推理仅对引用类型有效。
    • 您提出了关于值类型、cpu 寄存器公开的有趣想法。再见。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    相关资源
    最近更新 更多