当拉姆达捕获C#结构实例行为更改(C# Struct instance behavior chang

2019-08-03 07:36发布

我有针对此问题的解决办法,但我想弄清楚,为什么它的工作原理。 基本上,我通过使用的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();
        }
    }
}

Answer 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#编译器是否表现为每这里的规范。 我怀疑它是。 即使不是,我不想暗示球队应该把任何努力来修复它。 像这样的代码只是乞求以微妙的方式被打破。



Answer 2:

好。 我们一定有一个问题在这里,但我怀疑,这个问题不是本身,而是用foreach执行,而不是关闭。

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

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);

有一个大量的相关可变的值类型的问题和相关的最后行为其中之一:通过突变方法改变只读结构(如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
}

不幸的是我无法找到严格的规则如何只读局部变量应该表现,但其明确表示,基于循环体这种行为是不同的:我们不会复制到当地人在简单的循环每次访问,但我们这样做,如果循环体关闭在循环变量。

我们都知道, 关闭了循环变量认为是有害的 ,并且环实现是在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,因为它不是一个问题,停止警告它)。

我既行为确定的,但矛盾似乎很奇怪。



Answer 3:

我怀疑这是与如何lambda表达式求值做。 见这个问题和它的更多细节的答案。

题:

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

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

由于修改后的关闭,上面的代码将导致所有的查询Where子句中要依据的终值s

回答:

这是在C#中最糟糕的“陷阱”之一, 我们将采取的重大更改,以解决它。 在C#5 foreach循环变量是逻辑上的循环体内部,因此关闭每次都会得到一个新的副本。



Answer 4:

只是为了完成谢尔​​盖的帖子,我想补充以下示例具有手动关闭,演示编译器的行为。 当然,编译器可能具有满足foreach语句变量中捕获 只读要求任何其他实现。

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);
    }
}  


Answer 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();
        }
    }
}


文章来源: C# Struct instance behavior changes when captured in lambda
标签: c# struct