1-3 - C#语言习惯 - 推荐使用查询语法而不是循环

简介:   C#语言中并不缺少控制程序流程的结构,for、while、do-while和foreach等都可以做到这点。 历史上所有计算机语言设计者都不曾遗漏这些重要的循环控制结构。 不过我们还有一个更好的方式:查询与法(query syntax)。

 

  C#语言中并不缺少控制程序流程的结构,for、while、do-while和foreach等都可以做到这点。

历史上所有计算机语言设计者都不曾遗漏这些重要的循环控制结构。

不过我们还有一个更好的方式:查询与法(query syntax)。

 

  查询语法可以让程逻辑的表达式由“命令式”转为“声明式”

查询语法定义了想要的结果,而把如何得到这些结果的任务交给了其他的专门实现。

本篇提到的所有查询语法都可以通过方法调用语法来实现,并感受其带来的所有好处。

不过最重要的是,查询语法(实现了查询语法表达式模式的方法语法也可以)要比传统的命令式循环结构更加清晰地表达你的意图。

 

  下面这一段代码演示了用命令的方式填充一个数组,然后将其内容输出至控制台:

 1             int[] foo = new int[100];
 2 
 3             for (int i = 0; i < foo.Length; i++)
 4             {
 5                 foo[i] = i * i;
 6             }
 7 
 8             foreach (int item in foo)
 9             {
10                 Console.WriteLine(item.ToString());
11             }

 

  即使是编写这一段简单的代码,你也需要关注于具体的实现细节。

而若是采用查询语法实现同样的功能,那么代码将变得更加易读且易于重用:

 

  第一步,可以将生成数组的工作交给一个查询完成:

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

 

  类似的修改可以应用到第二个循环上,不过这里你要编写一个扩展方法来逐一操作集合中的元素:

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

 

  但是,int类型数组并没有提供ForAll()的查询方法。但是.NET BCL已经为List<T>提供了一个ForAll的实现。

因此,我们只需要为IEnumerable<T>实现同样的方法,即可实现ForAll()查询操作:

 1     public static class Extensions
 2     {
 3         public static void ForAll<T>(this IEnumerable<T> sequence, Action<T> action)
 4         {
 5             foreach (T item in sequence)
 6             {
 7                 action(item);
 8             }
 9         }
10     }

 

  运行结果如下:

 

  这样看上去,似乎并没有什么翻天覆地的改变,不过却带来了更好的重用性。

每次你想在一个序列的元素上执行某个操作时,都可以使用该ForAll方法。

 

  这只是个简单的操作,因此你或许看不到太多的好处。确实如此,不过咱们可以继续来看一些其他的问题。

 


 

 

  很多操作需要处理嵌套的循环。例如,用0~99的整数生成所有的(x,y)二元组,使用嵌套循环也不难:

 1         private static IEnumerable<Tuple<int, int>> ProduceIndices()
 2         {
 3             for (int x = 0; x < 100; x++)
 4             {
 5                 for (int y = 0; y < 100; y++)
 6                 {
 7                     yield return Tuple.Create(x, y);
 8                 }
 9             }
10         }

  当然,你也可以使用查询来生成同样的数据:

1         private static IEnumerable<Tuple<int, int>> QueryIndices()
2         {
3             return 
4                 from x in Enumerable.Range(0, 100) 
5                 from y in Enumerable.Range(0, 100) 
6                 select Tuple.Create(x, y);
7         }

 

  二者看上去有些相似,不过即使问题变得越来越复杂,查询语法一样可以保持简单。

比如说,我们规定要生成的(x,y)二元组中x和y的和要小于100,那么两个方法就要变成:

 1         private static IEnumerable<Tuple<int, int>> ProduceIndices()
 2         {
 3             for (int x = 0; x < 100; x++)
 4             {
 5                 for (int y = 0; y < 100; y++)
 6                 {
 7                     if (x + y < 100)
 8                     {
 9                         yield return Tuple.Create(x, y);
10                     }
11                 }
12             }
13         }
14 
15         private static IEnumerable<Tuple<int, int>> QueryIndices()
16         {
17             return
18                 from x in Enumerable.Range(0, 100)
19                 from y in Enumerable.Range(0, 100)
20                 where (x + y < 100)
21                 select Tuple.Create(x, y);
22         }

 

  看上去仍然差不多,不过命令式的语法已经开始将要表达的语意逐渐隐藏在必要的语法中了。

我们继续更改一下问题,我们需要让返回的二元组按照其远离远点的距离逆序排列。

 

  下面两个不同的方法能生成同样的正确结果:

 1         private static IEnumerable<Tuple<int, int>> ProduceIndices()
 2         {
 3             var storage = new List<Tuple<int, int>>();
 4 
 5             for (int x = 0; x < 100; x++)
 6             {
 7                 for (int y = 0; y < 100; y++)
 8                 {
 9                     if (x + y < 100)
10                     {
11                         storage.Add(Tuple.Create(x, y));
12                     }
13                 }
14             }
15 
16 
17 
18             storage.Sort((point1, point2) => (
19                     point2.Item1 * point2.Item1 +
20                     point2.Item2 * point2.Item2
21                 ).CompareTo(
22                     point1.Item1 * point1.Item1 +
23                     point1.Item2 * point1.Item2
24                 )
25             );
26             return storage;
27         }
28 
29         private static IEnumerable<Tuple<int, int>> QueryIndices()
30         {
31             return
32                 from x in Enumerable.Range(0, 100)
33                 from y in Enumerable.Range(0, 100)
34                 where (x + y < 100)
35                 orderby (x * x + y * y) descending
36                 select Tuple.Create(x, y);
37         }

  运行结果如下:

 

 

  现在,你可以看到明显的不同了吧。相比而言,命令式的方法非常难以理解。

如果你不仔细看的话,甚至都不会发现比较函数中参数被颠倒了(实际上这是个错误),而这只是为了能够降序排列自己。

要是没有任何注释或文档,命令式的代码将会更加难以阅读并理解。

 

  即使你足够细心地发现了比较函数中参数的颠倒,你是不是会觉得这是个错误呢?

这一段命令式代码太过于强调实现目标所需要的详细步骤,以至于让人很容易陷入此类细节,甚至忘记了最初将要达到的目的是什么。

 

  还有一个让你更加倾向于使用查询语法的理由:

查询语法比循环结构能提供更具有组合性的API。查询语法将很自然地把算法分解成小块的代码,每一块仅仅对序列中的元素进行单一的操作。

查询语法的延迟执行模型也让开发者能将这些单一的操作组合成多步的操作,且在一次遍历序列的时候就可以完整执行,而循环结构则无法以类似的方式组合起来。

你必须为每一步操作创建临时的存储,或者为序列将要执行的每一批操作都创建专用的方法。

 

 

  最后一个例子说明了上述工作原理,其操作实际上是将一次过滤(where)子句、一次排序(orderby)子句和一个查询(select)子句组合了起来。

所有的这些步骤都仅在一次遍历中实现。命令式的代码则创建了一个中间的存储,将排序操作分离开来。

 

 

  虽然叫做“查询语法”,不过每一个查询都有一个与之对应的方法调用语法。

有些时候使用查询比较自然,而有些时候方法调用却更加直观一些。

对于上面的例子,查询语法的可读性更强。这个查询对应的方法调用语法如下:

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

 

  到底查询语法还是方法调用语法更可读一些,这是个仁者见仁的问题。

不过对于这个例子,我相信查询语法更清晰一些。但其它的一些例子,情况也许有些不同。

此外,有些方法没有与之对应的查询语法,例如Take、TakeWhile、Skip、SkipWhile、Min、Max等。

如果需要使用这些方法,那么就必须要以方法调用语法完成。 

  不过其它的一些语言,例如VB.NET,却为其中的很多方法提供了对应的查询语法关键词。

 

  但有些人会经常提起,查询语法执行速度要逊于普通循环。

  然而,虽然你可以容易地设计出一个手工编写的、比查询语法高性能的循环,但这并不是问题的关键。

你首先需要判断的是,是不是某个特别的情况下某个查询的效率有问题。

在你最终决定放弃查询语法之前,还可以试一下LINQ的并行计算扩展。只需要简单地在查询后添加.AsParallel()方法即可。

 

  C#起初是以命令式语言的方式设计的,随后越走越远,组件提供了命令式语言的大多数功能。

  人们会很自然地选择最熟悉的工具,不过这并不带包这些工具就是最高效的。

当你需要编写循环时,首先看看能否用查询语法实现。若是无法使用查询语法,那么再看看是否可以使用方法调用语法替代。

  你会发现,这样写出的代码总会比命令式循环结构要简洁一些。

 

【来自:张董'Blogs:http://www.cnblogs.com/LonelyShadow,转载请注明出处。】

亲们。码字不容易,觉得不错的话记得点赞哦。。

目录
相关文章
|
1月前
|
安全 编译器 程序员
C# 中 foreach 循环和 for 循环深度比较
为什么建议你多数情况下使用 foreach 进行遍历循环?看完你就明白了
|
28天前
|
开发框架 .NET API
以C#一分钟浅谈:GraphQL 数据类型与查询
本文从C#开发者的角度介绍了GraphQL的基本概念、核心组件及其实现方法。GraphQL由Facebook开发,允许客户端精确请求所需数据,提高应用性能。文章详细讲解了如何在C#中使用`GraphQL.NET`库创建Schema、配置ASP.NET Core,并讨论了GraphQL的数据类型及常见问题与解决方案。通过本文,C#开发者可以更好地理解并应用GraphQL,构建高效、灵活的API。
98 64
|
25天前
|
开发框架 .NET 测试技术
C# 一分钟浅谈:GraphQL 数据类型与查询
本文介绍了GraphQL的基本概念、数据类型及查询方法,重点从C#角度探讨了GraphQL的应用。通过Hot Chocolate库的实例,展示了如何在ASP.NET Core中实现GraphQL API,包括安装、定义Schema、配置及运行项目。文中还讨论了常见问题与解决方案,旨在帮助开发者更好地理解和使用GraphQL。
26 2
|
2月前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
44 1
|
2月前
|
Java C#
如何避免在C#循环中使用await
如何避免在C#循环中使用await
131 9
|
2月前
|
存储 开发框架 .NET
C#语言如何搭建分布式文件存储系统
C#语言如何搭建分布式文件存储系统
87 2
|
2月前
|
SQL 缓存 分布式计算
C#如何处理上亿级数据的查询效率
C#如何处理上亿级数据的查询效率
47 1
|
3月前
|
前端开发 Java C#
C#语言的优缺点?
C#语言的优缺点?
152 3
|
3月前
|
安全 IDE Java
C#语言的
C#语言是一种面向对象的编程语言
37 1
|
1月前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
36 3