【问题标题】:Changing a struct inside another struct in a foreach loop在 foreach 循环中更改另一个结构内的结构
【发布时间】:2009-11-06 15:10:16
【问题描述】:

以下代码打印(调用 MyMethod 时):

0
0
0
1

我希望它打印出来:

0
0
1
1

这是为什么?

代码:

private struct MyStruct
{
    public MyInnerStruct innerStruct;
}

private struct MyInnerStruct
{
    public int counter;

    public void AddOne()
    {
        ++counter;
    }
}

public static void MyMethod()
{
    MyStruct[] myStructs = new MyStruct[] { new MyStruct() };

    foreach (var myStruct in myStructs)
    {
        MyStruct myStructCopy = myStruct;

        Console.WriteLine(myStruct.innerStruct.counter);
        Console.WriteLine(myStructCopy.innerStruct.counter);

        myStruct.innerStruct.AddOne();
        myStructCopy.innerStruct.AddOne();

        Console.WriteLine(myStruct.innerStruct.counter);
        Console.WriteLine(myStructCopy.innerStruct.counter);
    }
}

【问题讨论】:

    标签: c# .net struct foreach


    【解决方案1】:

    您看到这种行为的原因是由于使用了迭代变量。迭代变量是只读的,因为在 C# 中您无法修改它们(C# 语言规范第 8.8.4 节对此进行了详细说明

    迭代变量对应一个只读局部变量,其作用域延伸到嵌入语句上

    使用只读可变结构是导致意外行为的途径。您实际上是在使用变量的副本,而不是直接使用变量。因此,在myStruct 的情况下增加的是副本,而不是实际值。这就是为什么原始值保持不变的原因。

    Eric 就该主题撰写了一篇相当深入的文章,您可以在此处访问

    您应该始终拥有不可变结构的另一个原因。

    【讨论】:

    • 很好,这就是我要找的博客 :)
    • 这个解释是不是又引出了另一个问题?如果 myStruct 是只读的(我希望它是),那么 myStruct.InnerCopy.AddOne() 肯定会通过抛出异常而失败。如果您可以在 const / 只读对象上调用非 const 方法(正如我们在 C++ 世界中所称的那样)并让它静默不做任何事情而不是抱怨,那么肯定有一些非常错误的事情......
    • AAT,这里发生的是在可变 copy 上调用了变异方法。值类型是按值复制的,这就是为什么它们被称为“值”类型。如果您不希望按值复制内容,请不要使用值类型。
    • @AAT C# 的只读和 C++ 的 const 概念是非常不同的结构。在 C++ 中,它试图确保单个值不会发生意外突变。在 C# 中,当应用于值类型时,只读会导致在使用时(或至少在访问成员时)创建值的副本,从而防止原始值的突变。我不愿多说,因为我并不深入了解 C# 确保返回结构值的所有情况。希望 Eric 稍后会给出更彻底的答案。
    • 确实,Jared,您的解释是正确的。不幸的是,我们并没有严格确保像“foreach”和“using”变量这样的所谓“只读变量”的处理方式与我们处理只读的方式相同完全字段; 可能滥用某些编译器行为(涉及闭包重写)来诱导这些可变值类型的变量发生突变。我认为这些行为是编译器错误,但场景模糊且规范不精确。希望我们会在假设的未来版本中解决这些问题。
    【解决方案2】:

    迭代变量是只读的,通常禁止直接修改只读结构,或通过ref 将此类结构或其任何部分传递给任何会修改它的代码。不幸的是,有一个问题:尽管许多结构方法和属性不会改变底层结构,但调用结构上的方法或属性需要通过ref 传递,无论方法或属性是否会改变结构实例.这给 C# 的设计者留下了五个选择:

    1. 在未将结构显式复制到可变存储位置(很可能是局部变量)的情况下,不允许在只读上下文中对结构使用结构方法或属性。
    2. 允许在只读上下文中的结构上使用结构方法或属性,并希望它们不会修改它们不应该修改的任何内容。
    3. 在只读结构上调用方法或属性时,静默复制该结构并将该副本提供给方法或属性。
    4. 提供一种方式,通过该方式结构方法或属性可以以声明方式指示它们是否会改变底层结构;只允许结构方法和属性在声明时修改它们自己的字段,并且只允许将只读结构传递给未声明为修改它们的方法。
    5. 提供一种方式,通过该方式结构方法或属性可以以声明方式指示它们是否会改变底层结构;禁止在只读上下文中的结构上使用此类方法和属性,但在调用没有此类声明的方法时制作只读结构的防御性副本,以防万一方法应该具有它们但没有。

    选择#1 会有点烦人,尤其是对于通过属性公开其状态的结构。从性能的角度来看,选择 #2 或 #4 会很好,但意外写入 this 的结构例程可能会导致意外行为。 Microsoft 选择了#3(除了添加了可选属性外,选项#5 相同)。在许多方面,这是最糟糕的选择(不写 this 的方法将比没有复制操作时运行得慢,写 this 的方法无论有没有它都无法正常工作,唯一的事情是当结构写入只读变量时损坏将是其代码已损坏的结构的实例)。尽管如此,这个选择现在已经很成熟了,而且就是这样。

    顺便说一句,即使是嵌套在只读结构或结构类型属性中的读取操作结构也可能比使用非只读字段昂贵得多,但只是避免写入它们。调用myStruct.innerStruct.AddOne()时,编译器先复制MyStruct,再复制myStruct.InnerStruct,然后调用AddOne()方法。相比之下,在调用myStructCopy.innerStruct.AddOne()时,它可以通过引用AddOne()方法传递innerStruct,而无需复制任何内容。

    【讨论】:

      【解决方案3】:

      我想您会发现 MyStruct.innerStruct 实际上返回的是结构的副本,而不是结构本身。因此,您正在增加副本的价值...

      当然,我最近在某个地方读到了一篇关于此的博客。

      如果您更改以下内容

      myStruct.innerStruct.AddOne();
      Console.WriteLine(myStruct.innerStruct.counter);
      

      MyInnerStruct inner = myStruct.innerStruct;
      inner.AddOne();
      Console.WriteLine(inner.counter);
      

      然后你应该会看到它开始工作了。

      【讨论】:

      • 但是为什么 myStructCopy 的计数器会增加呢?我最好的猜测是这与 myStruct 只读有关。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-07-30
      • 2013-06-23
      • 2018-10-21
      • 2021-05-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多