你能指出这个 ForEach 扩展方法中的错误吗?
2011-10-07 16:06 鹤冲天 阅读(4958) 评论(32) 编辑 收藏 举报带返回值的 ForEach 扩展
Linq 中没有原生的 ForEach 扩展方法,我们可以很轻松的扩展一个:
1 2 3 |
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) { foreach (var element in source) action(element); } |
上面这个 ForEach 是没有返回值的,写完 ForEach 本句代码也就结束了,这与 Linq 链式编程风格是不符的。
细心查看的话,你会发现 Enumerble 和 Queryable 中的每个扩展方法都是有返回值的,这样才能保证代码链链不断。
改进下,让 ForEach 返回 IEnumerable<T>,相信不少朋友会错误地写出如下的代码:
1 2 3 4 |
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action) { foreach (var element in source) action(element); return source; } |
园子里中就有这么一篇文章是这样实现的,文章还被选入了博客文库。你能胜过小编,指出其中的错误吗。
通过单元测试查找错误
我们来测试下这个扩展方法,先写第一个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Employee { public string Name { get; set; } public decimal Bonus { get; set; } } [TestMethod()] public void ForEachTest() { var employees = new Employee[] { new Employee{ Name = "张三", Bonus = 500 }, new Employee { Name = "李四", Bonus = 800} }; var actualBonus = employees .ForEach(e => e.Bonus += 200) .First(e => e.Name == "李四") .Bonus; Assert.AreEqual(1000, actualBonus); } |
说明:在 .NET Framework 2.0 版中,Array 类实现 System.Collections.Generic.IList<T>、System.Collections.Generic.ICollection<T> 和 System.Collections.Generic.IEnumerable<T> 泛型接口。 由于实现是在运行时提供给数组的,因而对于文档生成工具不可见。 因此,泛型接口不会出现在 Array 类的声明语法中,也不会有关于只能通过将数组强制转换为泛型接口类型(显式接口实现)才可访问的接口成员的参考主题。 将某一数组强制转换为这三种接口之一时需要注意的关键一点是,添加、插入或移除元素的成员会引发 NotSupportedException。
摘自:http://msdn.microsoft.com/zh-cn/library/system.array.aspx
这个测试是可以顺利通过的,但不能说明代码是正确的,我们再来写一个测试。
Enumerable.Range 方法 可以生成整数序列,我们就调用它来作为 source 参数:
1 2 3 4 5 6 |
[TestMethod()] public void ForEachTest2() { var nums = Enumerable.Range(1, 10); var actual = nums.ForEach(i => i *= 10).First(); Assert.AreEqual(10, actual); } |
第 4 行仅为了测试,其它情况下使用 Select(i => i*10) 更为恰当。
这次测试通不过了:
应该思考下了,自已动脑解决 胜过 他人直接告知 。
再给出最后一个测试,看了基本就能找出错误所在了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public IEnumerable<Employee> GetEmployees() { yield return new Employee {Name = "张三", Bonus = 500}; yield return new Employee {Name = "李四", Bonus = 800}; } [TestMethod()] public void ForEachTest3() { var employees = GetEmployees(); var actualBonus = employees .ForEach(e => e.Bonus += 200) .First(e => e.Name == "李四") .Bonus; Assert.AreEqual(1000, actualBonus); } |
问题所在
通过上面几个测试,应该发现 yield 的特性会让上面的 ForEach 出问题,请参看 Artech 的文章 《 从yield关键字看IEnumerable和Collection的区别 》进行更深入的了解。
如果你使用的是 IQueryable<T> 问题可能出现,要考虑一些 ORM 框架的缓存情况。
正确实现
那么怎么正确实现 ForEach 扩展方法呢,我想还是留给大家来思考完成吧,一味接受可不是好的学习方式。
另外,还可以实现如下签名的扩展,以方便使用:
1
|
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T, int> action) {/*...*/ } |
有关 Linq 中为什么没有原生的的 ForEach 扩展的讨论,请参见文章:
-------------------
思想火花,照亮世界