在使用foreach对异步委托赋值的时候,发现一个问题。代码如下:
成都创新互联专注于镇海企业网站建设,响应式网站设计,电子商务商城网站建设。镇海网站建设公司,为镇海等地区提供建站服务。全流程定制网站建设,专业设计,全程项目跟踪,成都创新互联专业和态度为您提供的服务static void Main(string[] args) { Listlst_tsk = new List (); List lst_item = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; foreach (var item in lst_item) { Task tsk = new Task(() => { Console.WriteLine(item); }); lst_tsk.Add(tsk); tsk.Start(); } Console.ReadLine(); }
往Task中,赋值一个拉姆达表达式,期待运行的结果应该是1,2,3,4,5,6,7,8,9,10乱序输出。但是实际上的结果是10,10,10,10,10,10,10,10,10,10。很多人都认为这是c#编译器的一个bug。Eric做出了解释,根据Eric的文章,在foreach循环语句中的变量只有一个item,该变量在循环过后,被赋值为10了。当异步线程启动的时候,取到的item早就变成10了,因此就得出上面的结果。
根据Eric的文章,foreach只是一个语法糖,它对应的代码如下
IEnumeratore = ((IEnumerable )values).GetEnumerator(); try { int m; // OUTSIDE THE ACTUAL LOOP while(e.MoveNext()) { m = (int)(int)e.Current; funcs.Add(()=>m); } } finally { if (e != null) ((IDisposable)e).Dispose(); }
可以看到m并不包括在while语句中,而且()=>m的意思是返回当前m变量的值,而不是返回委托创建时m变量的值。因此当这个委托真正运行的时候,找到的m可能已经是其它值了。
如果把语法糖改成如下的方式:
try { while(e.MoveNext()) { int m; // INSIDE m = (int)(int)e.Current; funcs.Add(()=>m); } }
那么m在while内部,每一个m都是单独的。根据Eric,不这样改的一个原因就是,它可能会增加了在循环中使用闭包的次数,(因为异步线程在启动时,都会用到循环中的m,这个m的生命周期在while循环中,只能通过闭包机制,使得其值能够继续保留在内存中,能够让异步委托在调用的时候继续访问到该值)。而且,如果这样修改了,用户会觉得foreach每一个循环都使用了一个新的变量,而不是一个存储了新值的旧变量。
因此,一开始的演示代码,只需要如下修改既可以了:
static void Main(string[] args) { Listlst_tsk = new List (); List lst_item = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; foreach (var item in lst_item) { var copy= item;//增加一个临时的拷贝变量 Task tsk = new Task(() => { Console.WriteLine(copy ); }); lst_tsk.Add(tsk); tsk.Start(); } Console.ReadLine(); }
这样的话,每次委托运行的时候,都会去找copy 变量了。
可能是很多人的意见影响了C#编译器团队,在C#5.0中,他们决定修改这个问题,foreach循环中的变量存在于循环中,因此每次循环都使用的是一个新的变量。for循环暂时不做修正。因此,演示代码在VS2012下,使用C#5.0的编译器编译,得到的结果是如预期那样的乱序输出。
创新互联www.cdcxhl.cn,专业提供香港、美国云服务器,动态BGP最优骨干路由自动选择,持续稳定高效的网络助力业务部署。公司持有工信部办法的idc、isp许可证, 机房独有T级流量清洗系统配攻击溯源,准确进行流量调度,确保服务器高可用性。佳节活动现已开启,新人活动云服务器买多久送多久。