我已经解决了这个问题,但我想弄清楚为什么它会起作用。基本上,我正在使用 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/