当前位置: 首页 > 编程语言 > C#

在lambda中捕获时,C#Struct实例行为会发生更改分享

时间:2023-04-11 11:57:06 C#

C#学习教程:C#Structinstancebehaviorchangeswhencapturedinalambda基本上,我使用foreach循环遍历结构列表。如果我在调用结构的方法之前包含引用当前结构的LINQ语句,则该方法无法修改结构的成员。无论LINQ语句是否被调用,都会发生这种情况。我能够通过将我正在寻找的值分配给一个变量并在LINQ中使用它来解决这个问题,但我想知道是什么原因造成的。这是我创建的示例。使用系统;使用System.Collections.Generic;使用System.Linq;使用系统文本;namespaceWeirdnessExample{publicstructRawData{privateintid;publicintID{get{returnid;}set{id=value;}}publicvoidAssignID(intnewID){id=newID;}}publicclassProcessedData{publicintID{get;放;}}classProgram{staticvoidMain(string[]args){ListprocessedRecords=newList();processedRecords.Add(newProcessedData(){ID=1});列出rawRecords=newList();rawRecords.Add(newRawData(){ID=2});诠释我=0;foreach(rawRecords中的RawDatarawRec){intid=rawRec.ID;if(i20){列表matchingRecs=processedRecords.FindAll(mr=>mr.ID==rawRec.ID);}Console.Write(String.Format("WithLINQ:IDBeforeAssignment={0},",rawRec.ID));//2rawRec.AssignID(id+8);Console.WriteLine(String.Format("分配后的ID={0}",rawRec.ID));//2i++;}rawRecords=newList();rawRecords.Add(newRawData(){ID=2});我=0;foreach(rawRecords中的RawDatarawRec){intid=rawRec.ID;if(i<0){列表matchingRecs=processedRecords.FindAll(mr=>mr.ID==id);}Console.Write(String.Format("WithLINQ:IDBeforeAssignment={0},",rawRec.ID));//2rawRec.AssignID(id+8);Console.WriteLine(String.Format("分配后的ID={0}",rawRec.ID));//10i++;}控制台.ReadLine();好吧,我已经成功地用一个相当简单的测试程序重现了这一点,如下所示,我现在明白了Understandingitindenial并没有让我觉得不那么恶心,但嘿...解释后的代码。使用系统;使用System.Collections.Generic;structMutableStruct{publicintValue{get;放;}publicvoidAssignValue(intnewValue){Value=newValue;}}classTest{staticvoidMain(){varlist=newList(){newMutableStruct{Value=10}};Console.WriteLine("没有循环变量捕获");foreach(列表中的MutableStruct项目){Console.WriteLine("Before:{0}",item.Value);//10item.AssignValue(30);Console.WriteLine("之后:{0}",item.Value);//30}//重置...list[0]=newMutableStruct{Value=10};Console.WriteLine("带循环变量捕获");foreach(列表中的MutableStruct项目){动作捕获=()=>Console.WriteLine(item.Value);Console.WriteLine("前:{0}",item.Value);//10item.AssignValue(30);Console.WriteLine("之后:{0}",item.Value);//还是10!}}}两个循环的区别在于,在第二个循环中,循环变量是通过lambda表达式捕获的。第二个循环实际上变成了这样://Nestedclass,wouldactuallyhaveunspeakablenameclassCaptureHelper{publicMutableStructitem;publicvoidExecute(){Console.WriteLine(item.Value);}}...//main方法中的第二个循环foreach(MutableStructiteminlist){CaptureHelperhelper=newCaptureHelper();helper.item=项目;动作捕捉=helper.Execute;MutableStructtmp=helper.item;Console.WriteLine("之前:{0}",tmp.Value);tmp=helper.item;tmp.AssignValue(30);tmp=helper.item;Console.WriteLine("之后:{0}",tmp.Value);现在,当然,每次我们从辅助函数复制变量时,我们都会得到一个新的结构副本。这应该可以正常工作——迭代变量是只读的,所以我们希望它不会改变。但是,您有一个方法可以更改结构的内容,从而导致意外行为。请注意,如果您尝试更改属性,您将收到编译时错误:Test.cs(37,13):errorCS1654:Cannotmodifymembersof'itembecauseitisa'foreachiterationvariable'Lessonlearned:I不是100%很明显,C#编译器的行为符合此处的规范。我怀疑是的。即使不是,我也不想暗示团队应该付出任何努力来修复它。像这样的代码只是乞求以微妙的方式被破坏。好的。我们肯定在这里遇到了一些问题,但我怀疑问题不是闭包本身,而是与foreach实现相反的问题。C#4.0规范指出(8.8.4foreach语句)“迭代变量对应于范围扩展到嵌入式语句的只读局部变量”。这就是我们不能更改循环变量或增加其属性的原因(如Jon所说):structMutable{publicintX{get;设置;}publicvoidChangeX(intx){X=x;}}varmutables=newList{newMutable{X=1}};foreach(variteminmutables){//非法!项目=新可变();//也是非法的!项目.X++;行为几乎与任何只读字段完全相同(在构造函数外部访问此变量):.类MutableReadonly{publicreadonlyMutableM=newMutable{X=1};}//代码中的某处varmr=newMutableReadonly();//非法的!先生。M=新的可变();//也是非法的!M.X++先生;//合法但会导致不良行为//因为mr.MX保持不变!Mr.M.ChangeX(10);有许多与可变值类型相关的问题,其中一个与最后一个行为有关:通过增变器方法(如ChangeX)更改只读结构将导致不明确的行为,因为我们将修改副本而不是只读对象本身:Mr.M.ChangeX(10);等同于:vartmp=mr.M;tmp.ChangeX(10);如果C#编译器将循环变量视为只读局部变量,那么它们期望与只读字段具有相同的行为似乎是合理的。现在,简单循环(没有任何闭包)中的循环变量的行为与只读字段几乎相同,只是为每次访问复制。但是如果代码更改和闭包起作用,循环变量开始表现得像纯只读变量:varmutables=newList{newMutable{X=1}};foreach(varminmutables){Console.WriteLine("Beforechange:{0}",mX);//X=1//我们将直接更改循环变量而不使用临时变量m.ChangeX(10);Console.WriteLine("更改后:{0}",mX);//X=10}foreach(varminmutables){//我们开始将m视为纯只读变量!动作a=()=>Console.WriteLine(mX));Console.WriteLine("更改前:{0}",mX);//X=1//我们将更改COPY而不是am变量!m.ChangeX(10);Console.WriteLine("更改后:{0}",mX);//X=1}不幸的是,我找不到只读局部变量应该如何表现的严格规则,但很明显这种行为对于循环体是不同的:而不是复制到本地,但是如果循环我们做body来关闭循环变量。我们都知道关闭循环变量被认为是有害的,并且在C#5.0中更改了循环实现。在C#5.0时代之前,解决这个老问题的简单方法是引入局部变量,但有趣的是,引入局部变量也改变了我们案例中的行为:foreach(varmLoopinmutables){//Introducinglocalvariable!变量m=mLoop;//我们正在捕获局部变量而不是循环变量Actiona=()=>Console.WriteLine(mX));Console.WriteLine("变更前:{0}",mX);//X=1//我们将回滚此行为并直接在闭包中更改//值类型而不进行复制!m.ChangeX(10);//X=10!!Console.WriteLine("修改后:{0}",mX);//X=1}这实际上意味着C#5.0有一个非常微妙的破坏性变化,因为没有人再引入局部变量(即使像ReSharper这样的工具也停止支持这是一个警告,因为这不是问题)。我对这两种行为都很好,但这种不一致似乎很奇怪。我怀疑这与lambda表达式的计算方式有关。有关详细信息,请参阅此问题及其答案。问题:在C#中使用lambda表达式或匿名方法时,一定要警惕访问修改后的闭包陷阱。例子:foreach(varsinstrings){query=query.Where(i=>i.Prop==s);//访问修饰闭包由于修饰闭包,上面的代码会导致查询子句中的所有Where都是基于s的最终值。答:这是C#中最严重的“问题”之一,我们将通过重大更改来修复它。在C#5中,foreach循环变量在逻辑上将位于循环体内,因此闭包每次都会获得一个新副本。为了完成Sergey的帖子,我想添加以下带有手动闭包的示例,它演示了编译器的行为。当然,编译器可能有任何其他实现来满足在foreach语句变量中捕获的只读要求。staticvoidMain(){varlist=newList(){newMutableStruct{Value=10}};foreach(列表中的MutableStruct项目){varc=newClosure(item);Console.WriteLine(c.Item.Value);Console.WriteLine("前:{0}",c.Item.Value);//10c.Item.AssignValue(30);Console.WriteLine("After:{0}",c.Item.Value);//还是10!}}classClosure{publicClosure(MutableStructitem){Item=item;}//readonly修饰符是必须的publicreadonlyMutableStructItem;publicvoidFoo(){Console.WriteLine(Item.Value);这可能会解决您的问题。它将forachforeachfor,并使结构不可变。以上是C#学习教程:在lambda中捕获时,C#Struct实例的行为会发生变化。所有分享的内容,如果对大家有用,需要进一步了解C#学习教程,希望大家多多关注——usingSystem;使用System.Linq;使用系统文本;namespaceWeirdnessExample{publicstructRawData{privatereadonlyintid;publicintID{get{returnid;}}publicRawData(intnewID){id=newID;}}publicclassProcessedData{privatereadonlyintid;publicintID{get{returnid;}}publicProcessedData(intnewID){id=newID;}}classProgram{staticvoidMain(string[]args){ListprocessedRecords=newList();processedRecords.Add(新处理数据(1));列出rawRecords=newList();rawRecords.Add(新原始数据(2));for(inti=0;i20){RawDatarawRec2=rawRec;列表matchingRecs=processedRecords.FindAll(mr=>mr.ID==rawRec2.ID);}Console.Write(String.Format("WithLINQ:IDBeforeAssignment={0},",rawRec.ID));//2rawRec=newRawData(rawRec.ID+8);Console.WriteLine(String.Format("IDAftersignment={0}",rawRec.ID));//2i++;}rawRecords=newList();rawRecords.Add(newRawData(2));for(inti=0;imr.ID==id);}Console.Write(String.Format("WithLINQ:IDBeforeAssignment={0},",rawRec.ID));//2rawRec=newRawData(rawRec.ID+8);Console.WriteLine(String.Format("IDAfterAssignment={0}",rawRec.ID));//10i++;}Console.ReadLine();}}}本文收集自网络,不代表立场。如涉及侵权,请点击右侧联系管理员删除,如需转载请注明出处: