在 lambda 中捕获时 C# 结构实例行为发生变化

标签 c# struct

我已经解决了这个问题,但我想弄清楚为什么它会起作用。基本上,我正在使用 foreach 遍历结构列表。如果我在调用结构的方法之前包括引用当前结构的 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();
        }
    }
}

最佳答案

好的,我已经成功地用一个相当简单的测试程序重现了这一点,如下所示,我现在明白了。不可否认,理解它并没有让我感到恶心,但是嘿......代码后的解释。

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!
        }
    }
}

这两个循环的不同之处在于,在第二个循环中,循环变量由 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);
}

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

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

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

教训:

  • 可变结构是邪恶的
  • 方法改变的结构是双重邪恶的
  • 通过对已捕获的迭代变量的方法调用来改变结构在破坏的程度上是三重邪恶的

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

关于在 lambda 中捕获时 C# 结构实例行为发生变化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13610559/

相关文章:

c# - 接口(interface)实现中成员的返回类型必须与接口(interface)定义完全匹配?

c# - 将 javascript gettime() 转换为 c# 日期时间

c++ - 为什么不能在不创建节点作为指针的情况下创建链表?

matlab - 在 Matlab 中将结构转换为 double 类型

go - 将嵌套配置 Yaml 映射到结构

c# - 处理匿名类型的 MVC Helper 方法

c# - 为未暴露给 UI 的方法编写测试 - 需要一般帮助

json - Golang - 使用更改键值解码 JSON

c# - C# MongoDB 2.0 中多个字段的项目

c++ - 在 C++ 头文件中声明重载运算符时遇到问题