原则8:优先考虑查询语法(query syntax)而不是循环结构

By D.S.Qiu

尊重他人的劳动,支持原创,转载请注明出处http://dsqiu.iteye.com

C# 语言对不同控制结构都不乏支持: for , while , do/while 和 foreach ,所有都是语言的一部分。从过去的计算机语言设计来看,很难不让人怀疑语言的设计者不是错过某些惊奇的循环结构。实际上,总是存在一个更好的方法:查询语法(query syntax)。

查询语法可以让你的程序逻辑从必要模式(imperative model)变为声明模式(declarative model)。查询语法定义了答案是什么而且决定了怎样得到这个答案的特殊实现。上面说到的整个点,这里指的是查询语法,方法调用语法带来的好处查询语句一样有。这点对于查询语句很重要,而且进一步扩展,方法语法实现查询表达模式,比 imperative 循环结构更加清晰表达你的意图。

这段代码用必要的方法填充数值然后打印内容到控制台:

int[] foo = new int[100];
for (int num = 0; num < foo.Length; num++) 
    foo[num] = num * num;
foreach (int i in foo) 
    Console.WriteLine(i.ToString());

即使上面的例子很小,但过于重视操作如何执行而不是什么操作执行。使用查询语法重新创建更可读的代码可以重复利用不同的编译块。

首先第一步,你使用查询结果改变数组的产生:

int[] foo = (from n in Enumerable.Range(0, 100) 
select n * n).ToArray();

第二个循环你只要做类似的改动,尽管你需要写一个扩展方法对每个元素执行操作:

foo.ForAll((n) => Console.WriteLine(n.ToString()));

.NET BCL 使用 List<T> 实现 ForAll 。这只是简单地创建 IEnumerable<T> :

public static class Extensions 
{
    public static void ForAll<T>( 
    this IEnumerable<T> sequence, 
    Action<T> action)
    {
        foreach (T item in sequence) 
            action(item);
    } 
}

这可能看起来没有意义,但是它更加能重复利用。任何你对数组元素的操作, ForAll 都能做到。

这个例子很简单,所以你不会看到很多好处。实际,你也许是对的。我们接着看其他不同问题。

很多问题要求你通过嵌套循环来完成。假设你需要从0到99产生所有(x,y)对。很明显你会使用嵌套循环:

private static IEnumerable<Tuple<int, int>> ProduceIndices() 
{
    for (int x = 0; x < 100; x++) 
        for (int y = 0; y < 100; y++)
            yield return Tuple.Create(x, y);
}

当然,使用查询你可以产生相同的对象:

private static IEnumerable<Tuple<int, int>> QueryIndices() 
{
    return from x in Enumerable.Range(0, 100) 
        from y in Enumerable.Range(0, 100) 
        select Tuple.Create(x, y);
}

看起来很相似,但是查询更加简单即使这个问题的描述变得更困难起来。改变问题为产生的(x,y)对要求x和y的和小于100。比较这两个方法:

private static IEnumerable<Tuple<int, int>> ProduceIndices2() 
{
    for (int x = 0; x < 100; x++) 
        for (int y = 0; y < 100; y++)
            if (x + y < 100) 
                yield return Tuple.Create(x, y);
}
private static IEnumerable<Tuple<int, int>> QueryIndices2() 
{
    return from x in Enumerable.Range(0, 100) 
           from y in Enumerable.Range(0, 100)
           where x + y < 100 
            select Tuple.Create(x, y);
}

仍然很相近,但是 imperative 语法开始使用必须的语法产生结果而隐藏它的意义。所以再次改变问题。现在,条件一个条件:返回的数组必须按照它们到原点的距离排列。

private static IEnumerable<Tuple<int, int>> ProduceIndices3() 
{
    var storage = new List<Tuple<int, int>>();
    for (int x = 0; x < 100; x++) 
        for (int y = 0; y < 100; y++)
            if (x + y < 100) 
                storage.Add(Tuple.Create(x, y));
    storage.Sort((point1, point2) => 
        (point2.Item1*point2.Item1 + 
            point2.Item2 * point2.Item2).CompareTo( point1.Item1 * point1.Item1 + point1.Item2 * point1.Item2));
    return storage; 
} 
private static IEnumerable<Tuple<int, int>> QueryIndices3() 
{
    return from x in Enumerable.Range(0, 100) 
        from y in Enumerable.Range(0, 100) 
        where x + y < 100 
        orderby (x*x + y*y) descending 
        select Tuple.Create(x, y);
}

有些细节很明显改变了。 imperative 版本更难去理解。如果你看得快,你几乎不会注意到比较函数的参数被对换了。那样保证排序的结果是降序的。没有注释或其他支持文档, imperative 代码很是很难读懂。

即使你发现 where 的参数被对换了,你会觉得这是错误么? imperative 模式过于强调操作怎么样执行以至于很容易在这些操作中迷失什么操作正在完成的原意。

还有一个理由使用查询语法而不是循环结构:查询可以比循环结构创建更多组合的 API 。查询运费很自然构造算法块执行序列上的操作。查询的模式可以让开发者枚举序列中组合单一的操作为的组合操作。循环结构不能有类似的组合。你必须临时存储每步的结果,或创建对序列上组合操作的方法。

上个例子就体现了这点。操作组合自 一个过滤操作(where块),一个排序操作(sort块)和一个选择操作select块)。这些操作全部在一个枚举操作完成。 imperative 版本创建历史存储模型而且排序操作被分离出来。

我几经讨论过查询语法,但是你应该记住每个查询操作都有相应的方法调用语法。有时候查询语法更自然,有时候方法调用语法语法更自然。在上面例子,查询语法更加可读。下面是对应的方法调用语法:

private static IEnumerable<Tuple<int, int>> MethodIndices3() 
{
    return Enumerable.Range(0, 100). 
        SelectMany(x => Enumerable.Range(0,100), 
        (x,y) => Tuple.Create(x,y)).
        Where(pt => pt.Item1 + pt.Item2 < 100). 
        OrderByDescending(pt => 
            pt.Item1* pt.Item1 + pt.Item2 * pt.Item2);
}

查询语法或方法调用语法哪个更加可读是一个风格问题。在这个例子,我相信查询语法是更清晰的。然而,其他例子可能会不同。此外,一些方法是没有对应的查询语法的。向 Take , TakeWile , Skip ,SkipWhile , Min ,和 Max 在一定程度上需要你使用方法语法。其他语言,特别是 VB.NET 定义了更多查询语法的关键字。

有的人老是会认为我们讨论的这部分即查询的性能会比循环更慢。虽然你可以用一个简单例子说明循环能跑赢查询,但这不是一般地规则。如果你有一个特别的例子即查询结果表现的不够好,你才需要决定测量性能。然而,在你完成重写一个算法之前,考虑使用 LINQ 的并行扩展。使用查询语法的另一个好处是使用 .AsParallel() 方法可以并行执行查询。(查看原则35)。

C#开始是一个 imperative 语言。它几下保留了属于这个遗产的所有特性。自然你可随意使用最熟悉的工具(循环结构)。然而,这些工具可能不是最好的工具。当你发现你自己写着循环结构的形式,问下自己是否使用查询来写。如果查询语法不行,考虑使用函数调用语法。在大多数情况,你会发现创建比使用 imperative 循环结构更清晰的代码。

小结:

这篇文章讲的点还是比较好的,至少是一种新的编程模式(对于像D.S.Qiu这样的人来说),可以尝试多使用些,会带来一些便利。本来一直很纠结 imperative model 和 declararive model 的中文翻译,由于对计算机语言的历史都不了解,对一些专业名称很没有太多认识,甚至都没有怎么接触,只有困惑来了就去看下 wikipedia 。每天都是到这个时候才写完,脖子今天一直很不舒服,不要有颈椎病呀,完了。

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!

有关本书的其他章节翻译请点击查看,转载请注明出处,尊重原创!

如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件([email protected])交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

转载请在文首注明出处:http://dsqiu.iteye.com/blog/1982137

更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)