在 lambda 中捕获时 C# Struct 实例行为会发生变化 [英] C# Struct instance behavior changes when captured in lambda
问题描述
我已经解决了这个问题,但我正在努力弄清楚它为什么会起作用.基本上,我正在使用 foreach 遍历结构列表.如果在调用结构体的方法之前包含引用当前结构体的 LINQ 语句,则该方法无法修改结构体的成员.无论 LINQ 语句是否被调用,都会发生这种情况.我能够通过将我正在寻找的值分配给一个变量并在 LINQ 中使用它来解决这个问题,但我想知道是什么导致了这种情况.这是我创建的示例.
I've got a work around for this issue, but I'm trying to figure out why it works . Basically, I'm looping through a list of structs using foreach. If I include a LINQ statement that references the current struct before I call a method of the struct, the method is unable to modify the members of the struct. This happens regardless of whether the LINQ statement is even called. I was able to work around this by assigning the value I was looking for to a variable and using that in the LINQ, but I would like to know what is causing this. Here's an example I created.
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();
}
}
}
推荐答案
好的,我已经设法用一个相当简单的测试程序重现了这一点,如下所示,现在我明白了.诚然理解它并没有让我觉得不那么恶心,但是嘿...代码后的解释.
Okay, I've managed to reproduce this with a rather simpler test program, as shown below, and I now understand it. Admittedly understanding it doesn't make me feel any less nauseous, but hey... Explanation after code.
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 表达式捕获.第二个循环实际上变成了这样:
The difference between the two loops is that in the second one, the loop variable is captured by a lambda expression. The second loop is effectively turned into something like this:
// 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
复制变量时,我们都会得到一个新的结构副本.这通常应该没问题 - 迭代变量是只读的,所以我们期望它不会改变.但是,您有一个 方法 会更改结构体的内容,从而导致意外行为.
Now of course each time we copy the variable out of helper
we get a fresh copy of the struct. This should normally be fine - the iteration variable is read-only, so we'd expect it not to change. However, you have a method which changes the contents of the struct, causing the unexpected behaviour.
请注意,如果您尝试更改属性,则会出现编译时错误:
Note that if you tried to change the property, you'd get a compile-time error:
Test.cs(37,13): error CS1654: Cannot modify members of 'item' because it is a
'foreach iteration variable'
课程:
- 可变结构是邪恶的
- 被方法改变的结构是双重邪恶的
- 通过对已捕获的迭代变量的方法调用来改变结构在破坏的程度上是三重的
- Mutable structs are evil
- Structs which are mutated by methods are doubly evil
- Mutating a struct via a method call on an iteration variable which has been captured is triply evil to the extent of breakage
我不是 100% 清楚 C# 编译器的行为是否符合此处的规范.我怀疑是.即使不是,我也不想建议团队应该努力修复它.像这样的代码只是乞求以微妙的方式被破坏.
It's not 100% clear to me whether the C# compiler is behaving as per the spec here. I suspect it is. Even if it's not, I wouldn't want to suggest the team should put any effort into fixing it. Code like this is just begging to be broken in subtle ways.
这篇关于在 lambda 中捕获时 C# Struct 实例行为会发生变化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!