【问题标题】:C# Struct instance behavior changes when captured in lambda在 lambda 中捕获时 C# Struct 实例行为发生变化
【发布时间】:2012-11-28 17:06:43
【问题描述】:

我已经解决了这个问题,但我正在尝试找出它为什么有效。基本上,我正在使用 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();
        }
    }
}

【问题讨论】:

  • 尚不清楚您的示例试图显示什么。我强烈怀疑这只是为什么可变结构是一个非常糟糕的主意的另一个例子。
  • 我想我开始明白了。我正在尝试以更简单的形式重现这一点。
  • 请注意,您的代码中根本没有使用 LINQ。您正在使用 .NET 2.0 中存在的 List.FindAll 和一个 lambda 表达式 - 通常与 with LINQ 一起使用,但实际上并不是 LINQ 的一部分。这是让我开始的部分原因 - 我试图找到任何与 LINQ 相关的东西......

标签: c# struct


【解决方案1】:

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

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# 编译器的行为是否符合此处的规范。我怀疑是这样。即使不是,我也不想建议团队应该付出任何努力来修复它。像这样的代码只是乞求以微妙的方式被破坏。

【讨论】:

  • 如果我们在混合中添加接口,鼻恶魔!
  • @leppie:问题是,我的一部分立即开始想知道如何才能发挥最佳效果......
  • 谢谢。这就说得通了。我花了一段时间才意识到这样做的程序员正在修改一个可变结构。当我在 LINQ 中添加它时,它就停止了工作。真的很混乱。
  • @user1097991:根据我对这个问题的评论,这里不涉及 LINQ。有一个 lambda 表达式,但那不是 LINQ。
  • @luiscubal:这至少是一个设计缺陷。事实上它一个编译器错误(已确认)。
【解决方案2】:

好的。我们在这里肯定有问题,但我怀疑这个问题不是闭包本身,而是 foreach 实现。

C# 4.0 规范声明(8.8.4 foreach 语句)“迭代变量对应于一个只读局部变量,其范围延伸到嵌入的语句”。这就是为什么我们不能改变循环变量或增加它的属性(正如 Jon 已经说过的):

struct Mutable
{
    public int X {get; set;}
    public void ChangeX(int x) { X = x; }
}

var mutables = new List<Mutable>{new Mutable{ X = 1 }};
foreach(var item in mutables)
{
  // Illegal!
  item = new Mutable(); 

  // Illegal as well!
  item.X++;
}

在这方面,只读循环变量的行为几乎与任何只读字段完全相同(就在构造函数之外访问此变量而言):

  • 我们无法在构造函数之外更改只读字段
  • 我们无法更改值类型的只读字段的属性
  • 我们将只读字段视为值,这会导致每次访问值类型的只读字段时都使用临时副本。

.

class MutableReadonly
{
  public readonly Mutable M = new Mutable {X = 1};
}

// Somewhere in the code
var mr = new MutableReadonly();

// Illegal!
mr.M = new Mutable();

// Illegal as well!
mr.M.X++;

// Legal but lead to undesired behavior
// becaues mr.M.X remains unchanged!
mr.M.ChangeX(10);

有很多与可变值类型相关的问题,其中一个与最后一种行为有关:通过 mutator 方法(如ChangeX)更改只读结构会导致行为模糊,因为我们将修改副本 但不是只读对象本身:

mr.M.ChangeX(10);

相当于:

var tmp = mr.M;
tmp.ChangeX(10);

如果循环变量被 C# 编译器视为只读局部变量,那么期望它们的行为与只读字段相同似乎是合理的。

现在简单循环中的循环变量(没有任何闭包)的行为几乎与只读字段相同,只是每次访问时都复制它。但是,如果代码发生变化并开始使用闭包,循环变量就会开始表现得像纯只读变量:

var mutables = new List<Mutable> { new Mutable { X = 1 } };

foreach (var m in mutables)
{
    Console.WriteLine("Before change: {0}", m.X); // X = 1

    // We'll change loop variable directly without temporary variable
    m.ChangeX(10);

    Console.WriteLine("After change: {0}", m.X); // X = 10
}

foreach (var m in mutables)
{
    // We start treating m as a pure read-only variable!
    Action a = () => Console.WriteLine(m.X));

    Console.WriteLine("Before change: {0}", m.X); // X = 1

    // We'll change a COPY instead of a m variable!
    m.ChangeX(10);

    Console.WriteLine("After change: {0}", m.X); // X = 1
}

不幸的是,我找不到严格的规则,只读局部变量的行为方式,但很明显,这种行为因循环体而异:我们不会为简单循环中的每次访问都复制到本地变量,但我们会这样做如果循环体关闭循环变量。

我们都知道Closing over loop variable considered harmful 并且循环实现在 C# 5.0 中已更改。解决 C# 5.0 之前的旧问题的简单方法是引入局部变量,但有趣的是,在这种情况下引入局部变量也会改变行为:

foreach (var mLoop in mutables)
{
    // Introducing local variable!
    var m = mLoop;

    // We're capturing local variable instead of loop variable
    Action a = () => Console.WriteLine(m.X));

    Console.WriteLine("Before change: {0}", m.X); // X = 1

    // We'll roll back this behavior and will change
    // value type directly in the closure without making a copy!
    m.ChangeX(10); // X = 10 !!

    Console.WriteLine("After change: {0}", m.X); // X = 1
}

实际上这意味着 C# 5.0 有非常微妙的重大变化,因为没有人会再引入局部变量(甚至像 ReSharper 这样的工具在 VS2012 中也停止警告它,因为它不是问题)。

我对这两种行为都很好,但不一致似乎很奇怪。

【讨论】:

  • 我想知道微软添加一个结构体方法和属性可以用来指示它们是否修改底层结构的属性是否会有任何困难,以便编译器可以禁止调用this-mutating只读结构上的方法和属性,但允许在不修改 this 的属性(包括设置器)上使用它[后者的一个例子是 ArraySegment 的一个版本,它允许对底层数组进行读写索引访问] .
  • C# 5.0 中的更改是否会更改原始问题中描述的行为?
  • 不,不是。但它可以隐藏得更深,因为在 c# 5 中没有人会引入局部变量来捕获循环变量。
【解决方案3】:

我怀疑这与 lambda 表达式的计算方式有关。有关详细信息,请参阅this question 及其答案。

问题:

在 C# 中使用 lambda 表达式或匿名方法时,我们必须警惕访问修改后的闭包陷阱。例如:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure

由于修改了闭包,上述代码将导致查询中的所有 Where 子句都基于 s 的最终值。

答案:

这是 C# 中最糟糕的“陷阱”之一,我们将采取重大更改来修复它。在 C# 5 中,foreach 循环变量将在逻辑上位于循环,因此闭包每次都会得到一个新的副本。

【讨论】:

  • 我认为它 is 与 lambda 表达式有关,但与您期望的不太一样。它在 C# 5 中 not 已修复 - 我现在正在使用 C# 5 编译器,但仍然看到相同的行为。
  • @JonSkeet - 这不会让我感到惊讶。我之前在闭包中看到的任何内容都没有专门针对结构,所以我做了一个有根据的猜测。我实际上根本没有测试它。
【解决方案4】:

为了完成 Sergey 的帖子,我想添加以下带有手动关闭的示例,以演示编译器的行为。当然,编译器可能有任何其他实现满足 在 foreach 中捕获 语句变量的 readonly 要求。

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

    foreach (MutableStruct item in list)
    {
       var c = new Closure(item);

       Console.WriteLine(c.Item.Value);
       Console.WriteLine("Before: {0}", c.Item.Value);  // 10
       c.Item.AssignValue(30);
       Console.WriteLine("After: {0}", c.Item.Value);   // Still 10!
    }
}

class Closure
{
    public Closure(MutableStruct item){
    Item = item;
}
    //readonly modifier is mandatory
    public readonly MutableStruct Item;
    public void Foo()
    {
        Console.WriteLine(Item.Value);
    }
}  

【讨论】:

    【解决方案5】:

    这可能会解决您的问题。它将foreach 替换为for 并使struct 不可变。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace WeirdnessExample
    {
        public struct RawData
        {
            private readonly int id;
    
            public int ID
            {
                get{ return id;}
            }
    
            public RawData(int newID)
            {
                id = newID;
            }
        }
    
        public class ProcessedData
        {
            private readonly int id;
    
            public int ID
            {
                get{ return id;}
            }
    
            public ProcessedData(int newID)
            {
                id = newID;
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                List<ProcessedData> processedRecords = new List<ProcessedData>();
                processedRecords.Add(new ProcessedData(1));
    
    
                List<RawData> rawRecords = new List<RawData>();
                rawRecords.Add(new RawData(2));
    
    
                for (int i = 0; i < rawRecords.Count; i++)
                {
                    RawData rawRec = rawRecords[i];
                    int id = rawRec.ID;
                    if (i < 0 || i > 20)
                    {
                        RawData rawRec2 = rawRec;
                        List<ProcessedData> matchingRecs = processedRecords.FindAll(mr => mr.ID == rawRec2.ID);
                    }
    
                    Console.Write(String.Format("With LINQ: ID Before Assignment = {0}, ", rawRec.ID)); //2
                    rawRec = new RawData(rawRec.ID + 8);
                    Console.WriteLine(String.Format("ID After Assignment = {0}", rawRec.ID)); //2
                    i++;
                }
    
                rawRecords = new List<RawData>();
                rawRecords.Add(new RawData(2));
    
                for (int i = 0; i < rawRecords.Count; i++)
                {
                    RawData rawRec = rawRecords[i];
                    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 = new RawData(rawRec.ID + 8);
                    Console.WriteLine(String.Format("ID After Assignment = {0}", rawRec.ID)); //10
                    i++;
                }
    
                Console.ReadLine();
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-30
      • 1970-01-01
      • 1970-01-01
      • 2020-12-04
      • 2019-03-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多