在lambda中捕获时,C#Struct实例行为会发生变化

use*_*991 20 c# struct

我已经解决了这个问题,但我正在试图找出它的工作原理.基本上,我使用foreach循环遍历结构列表.如果我在调用struct的方法之前包含引用当前结构的LINQ语句,则该方法无法修改结构的成员.无论是否甚至调用LINQ语句,都会发生这种情况.我能够通过将我正在寻找的值分配给变量并在LINQ中使用它来解决这个问题,但我想知道是什么导致了这一点.这是我创建的一个例子.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WeirdnessExample
{
    public struct RawData
    {
        private int id;

        public int ID
        {
            get{ return id;}
            set { id = value; }
        }

        public void AssignID(int newID)
        {
            id = newID;
        }
    }

    public class ProcessedData
    {
        public int ID { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<ProcessedData> processedRecords = new List<ProcessedData>();
            processedRecords.Add(new ProcessedData()
            {
                ID = 1
            });


            List<RawData> rawRecords = new List<RawData>();
            rawRecords.Add(new RawData()
            {
                ID = 2
            });


            int i = 0;
            foreach (RawData rawRec in rawRecords)
            {
                int id = rawRec.ID;
                if (i < 0 || i > 20)
                {
                    List<ProcessedData> matchingRecs = processedRecords.FindAll(mr => mr.ID == rawRec.ID);
                }

                Console.Write(String.Format("With LINQ: ID Before Assignment = {0}, ", rawRec.ID)); //2
                rawRec.AssignID(id + 8);
                Console.WriteLine(String.Format("ID After Assignment = {0}", rawRec.ID)); //2
                i++;
            }

            rawRecords = new List<RawData>();
            rawRecords.Add(new RawData()
            {
                ID = 2
            });

            i = 0;
            foreach (RawData rawRec in rawRecords)
            {
                int id = rawRec.ID;
                if (i < 0)
                {
                    List<ProcessedData> matchingRecs = processedRecords.FindAll(mr => mr.ID == id);
                }
                Console.Write(String.Format("With LINQ: ID Before Assignment = {0}, ", rawRec.ID)); //2
                rawRec.AssignID(id + 8);
                Console.WriteLine(String.Format("ID After Assignment = {0}", rawRec.ID)); //10
                i++;
            }

            Console.ReadLine();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 38

好吧,我已经设法用一个相当简单的测试程序重现这个,如下所示,我现在明白了.不可否认地理解它不会让我觉得任何不那么恶心,但嘿......代码后的解释.

using System;
using System.Collections.Generic;

struct MutableStruct
{
    public int Value { get; set; }

    public void AssignValue(int newValue)
    {
        Value = newValue;
    }
}

class Test
{
    static void Main()
    {
        var list = new List<MutableStruct>()
        {
            new MutableStruct { Value = 10 }
        };

        Console.WriteLine("Without loop variable capture");
        foreach (MutableStruct item in list)
        {
            Console.WriteLine("Before: {0}", item.Value); // 10
            item.AssignValue(30);
            Console.WriteLine("After: {0}", item.Value);  // 30
        }
        // Reset...
        list[0] = new MutableStruct { Value = 10 };

        Console.WriteLine("With loop variable capture");
        foreach (MutableStruct item in list)
        {
            Action capture = () => Console.WriteLine(item.Value);
            Console.WriteLine("Before: {0}", item.Value);  // 10
            item.AssignValue(30);
            Console.WriteLine("After: {0}", item.Value);   // Still 10!
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

两个循环之间的区别在于,在第二个循环中,循环变量由lambda表达式捕获.第二个循环实际上变成了这样的东西:

// Nested class, would actually have an unspeakable name
class CaptureHelper
{
    public MutableStruct item;

    public void Execute()
    {
        Console.WriteLine(item.Value);
    }
}

...
// Second loop in main method
foreach (MutableStruct item in list)
{
    CaptureHelper helper = new CaptureHelper();
    helper.item = item;
    Action capture = helper.Execute;

    MutableStruct tmp = helper.item;
    Console.WriteLine("Before: {0}", tmp.Value);

    tmp = helper.item;
    tmp.AssignValue(30);

    tmp = helper.item;
    Console.WriteLine("After: {0}", tmp.Value);
}
Run Code Online (Sandbox Code Playgroud)

当然,每当我们复制变量时,helper我们都会获得结构的新副本.这应该是正常的 - 迭代变量是只读的,所以我们期望它不会改变.但是,您有一个方法可以更改结构的内容,从而导致意外行为.

请注意,如果您尝试更改属性,则会出现编译时错误:

Test.cs(37,13): error CS1654: Cannot modify members of 'item' because it is a
    'foreach iteration variable'
Run Code Online (Sandbox Code Playgroud)

教训:

  • 可变结构是邪恶的
  • 通过方法变异的结构是双重邪恶的
  • 通过在其上已捕获的迭代变量方法调用突变一个结构是三重恶破损的程度

对于我来说,C#编译器是否按照此处的规范运行并不是100%清楚.我怀疑是的.即使不是,我也不想暗示团队应该付出任何努力来修复它.像这样的代码只是乞求以微妙的方式被打破.

  • @leppie:麻烦的是,我的一部分立即开始想知道如何滥用这个以达到最佳效果...... (4认同)
  • @luiscubal:这至少是一个设计缺陷.实际上它*是*编译器错误(已确认). (4认同)
  • 如果我们添加接口混合,鼻子恶魔! (3认同)