代码改变世界

你能指出这个 ForEach 扩展方法中的错误吗?

2011-10-07 16:06 by 鹤冲天, ... 阅读, ... 评论, 收藏, 编辑

带返回值的 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) 更为恰当。

这次测试通不过了:

image

应该思考下了,自已动脑解决 胜过 他人直接告知 。

image

再给出最后一个测试,看了基本就能找出错误所在了:

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 扩展的讨论,请参见文章: