By Bill Wagner
January 2012
自从c#3.0引入LINQ后,它已经改变了不少C#开发者的编码习惯。然而,似乎仍有数量不少的c#开发者社区仍未完全采用它。我和那些还没有把LINQ作为他们日常开发工具的程序员聊过,原因有二,其一是他们没有充足的时间学习LINQ,其二是一部分人的老板还没有把LINQ作为它们工作中可应用的技术之一。他们应该感到羞愧,因为LINQ的函数库与语言增强功能可以让你写出更易阅读、更易维护、更易扩展的算法代码。投入时间学习这样一种大量提高生产率的工具集,对于你及其组中其它将会阅读或是扩展的人来说是有充足理由的。
为了表达清楚,我写了两个版本的示例程序。用来帮助你学习那些多数开发人员在提到LINQ时就想到的方法。示例使用反射来输出System.Linq.Enumerable类的所有公共方法,这些方法代表了LINQ中的不少API。示例中加载了system.core.dll程序集,之后利用反射找出Enumerable类的所有公共方法及其常规参数、范型参数、返回值及应用的特性(Attribute),并将它们以与API签名相似的格式输出到控制台。我们来检查与比较一下两个版本在可读性,表现性方面的差异。
LINQ出世前
下列两个方法是未利用LINQ API的查找所有LINQ方法信息的算法实现。
Figure 1: Find all the LINQ apis without using LINQ:
1. private static void NoLinqAnalysis(Assembly assembly)
2. {
3. Type[] types = assembly.GetExportedTypes();
4. Type enumerable = null;
5. foreach (Type t in types)
6. if (t.Name == "Enumerable")
7. enumerable = t;
8. if (enumerable == null)
9. {
10. Console.WriteLine("No data found.");
11. return;
12. }
13. MethodInfo[] methods = enumerable.GetMethods(BindingFlags.Public |
14. BindingFlags.Static);
15. foreach (var m in methods)
16. {
17. string generics = "";
18. if (m.IsGenericMethodDefinition)
19. {
20. Type[] typeParms = m.GetGenericArguments();
21. StringBuilder parmString = new StringBuilder();
22. parmString.Append("<");
23. for (int i = 0; i < typeParms.Length; i++)
24. {
25. parmString.Append(typeParms[i].Name);
26. if (i < typeParms.Length - 1)
27. parmString.Append(", ");
28. else
29. parmString.Append(">");
30. }
31. generics = parmString.ToString();
32. }
33. ParameterInfo returnParm = m.ReturnParameter;
34. string returnTypeName = processGenericTypeInformation(
35. returnParm.ParameterType);
36. Console.Write(returnTypeName);
37. Console.Write(" ");
38. Console.Write(m.Name);
39. Console.Write(generics);
40. Console.Write("(");
41. if (m.GetCustomAttributes(typeof(ExtensionAttribute), false)
42. .Length == 1)
43. Console.Write("this ");
44. int count = 1;
45. ParameterInfo[] parms = m.GetParameters();
46. foreach (var p in parms)
47. {
48. var typeName = processGenericTypeInformation(p.ParameterType);
49. Console.Write("{0} {1}", typeName, p.Name);
50. if (count++ < m.GetParameters().Count())
51. Console.Write(", ");
52. }
53. Console.WriteLine(")");
54. }
55. }
56. private static string processGenericTypeInformation(Type genericType)
57. {
58. if (!genericType.IsGenericType)
59. return genericType.Name;
60. string parmTypeName = genericType.Name.Remove(
61. genericType.Name.IndexOf('`'));
62. Type typeInfo = genericType.GetGenericArguments()[0];
63. string typeName = processGenericTypeInformation(typeInfo);
64. parmTypeName = string.Format("{0}<{1}", parmTypeName, typeName);
65. int i = 1;
66. while (i < genericType.GetGenericArguments().Length)
67. {
68. typeInfo = genericType.GetGenericArguments()[i];
69. typeName = processGenericTypeInformation(typeInfo);
70. parmTypeName += ", " + typeName;
71. i++;
72. }
73. parmTypeName += ">";
74. return parmTypeName;
75. }
下面是方法1中的一部分。该部分实现了类型Enumerable的查找。如果没找到,直接返回,结束方法。
1. Type[] types = assembly.GetExportedTypes();
2. Type enumerable = null;
3. foreach (Type t in types)
4. if (t.Name == "Enumerable")
5. enumerable = t;
6. if (enumerable == null)
7. {
8. Console.WriteLine("No data found.");
9. return;
10. }
一旦找到了,下一步就是找出其所有公共静态方法。正是这些方法构成了LINQ的API:
1. MethodInfo[] methods = enumerable.GetMethods(BindingFlags.Public |
2. BindingFlags.Static);
3. foreach (var m in methods)
4. {
遍历每一个方法,查找构成方法签名的每个元素。包括:返回值类型、方法名、类型参数(泛型方法)及所有参数。而每一个参数也需要查找它的类型信息,包括泛型类型参数以及参数名。
下面代码段从有泛型参数的方法中找出泛型参数列表:
1. string generics = "";
2. if (m.IsGenericMethodDefinition)
3. {
4. Type[] typeParms = m.GetGenericArguments();
5. StringBuilder parmString = new StringBuilder();
6. parmString.Append("<");
7. for (int i = 0; i < typeParms.Length; i++)
8. {
9. parmString.Append(typeParms[i].Name);
10. if (i < typeParms.Length - 1)
11. parmString.Append(", ");
12. else
13. parmString.Append(">");
14. }
15. generics = parmString.ToString();
16. }
接下来的几行代码是接收方法的返回值类型。因为返回值类型可能是个泛型类型,所以此处我使用了一个从返回值类型中接收泛型类型信息的帮助方法,该方法稍后我会介绍。
一旦输出返回值类型,代码也会在方法名的输出中包含前面找到的泛型类型参数信息。
最后,代码确定并输出参数信息。同样,这些参数可能是泛型的,所以同样的工具方法(获取泛型参数信息)还会使用。
1. Console.Write("(");
2. if (m.GetCustomAttributes(typeof(ExtensionAttribute), false)
3. .Length == 1)
4. Console.Write("this ");
5. int count = 1;
6. ParameterInfo[] parms = m.GetParameters();
7. foreach (var p in parms)
8. {
9. var typeName = processGenericTypeInformation(p.ParameterType);
10. Console.Write("{0} {1}", typeName, p.Name);
11. if (count++ < m.GetParameters().Count())
12. Console.Write(", ");
13. }
14. Console.WriteLine(")");
也有帮助检查那些可能表示泛型类型的类型对象,以泛型参数的格式输出它们的名字。该方法是递归的,因为泛型类型可能还包含着其它泛型参数(像这样的表达式Expression<Func<IEnumerable<T>>>)。递归主要处理这种情况。
我没有逐行详细介绍,因为这些方法使用了多数C#开发人员熟悉的C#语法,对于他们来说应该是显而易见的。而当把算法迁移到LINQ上时,我着重强调的那些代码段中的多数都将修改。太多循环了。随意的if语句遍布于代码中。不少循环中又嵌套了if语句和break语句。这样的代码太复杂了,让人很难理解核心算法是什么。
新工具,新表达
下列的代码段表达的还是同样的算法,但这次使用了如今C#中新的API与语法。这版代码使用了不少LINQ的API和不少的LINQ的查询语法。
用LINQ找出全部LINQ的API
1. private static void LinqAnalysis(Assembly assembly)
2. {
3. var enumerableType = assembly.GetExportedTypes()
4. .Single(t => t.Name == "Enumerable");
5. var methods = from m in enumerableType.GetMethods(
6. BindingFlags.Public | BindingFlags.Static)
7. let isExtension = m.GetCustomAttributes(
8. typeof(ExtensionAttribute), false)
9. .Any()
10. select new
11. {
12. returnParm = m.ReturnType.FormatTypeName(),
13. name = m.Name +
14. (m.GetGenericArguments().Any() ?
15. "<" + m.GetGenericArguments()
16. .Select(t => t.Name)
17. .CommaSeparated() + ">"
18. : ""),
19. parameters = "(" +
20. (isExtension ? "this " : "") +
21. m.GetParameters()
22. .Select(p => p.ParameterType.FormatTypeName()
23. + " " + p.Name)
24. .CommaSeparated()
25. + ")"
26. };
27. foreach (var m in methods)
28. Console.WriteLine("{0} {1}{2}", m.returnParm, m.name, m.parameters);
29. }
30. public static class LINQHelpers
31. {
32. public static string FormatTypeName(this Type genericType)
33. {
34. if (!genericType.IsGenericType)
35. return genericType.Name;
36. var parmTypeName = genericType.Name.Remove(genericType.Name
37. .IndexOf('`'));
38. var genericTypes = (from g in genericType.GetGenericArguments()
39. select g.FormatTypeName())
40. .CommaSeparated();
41. return string.Format("{0}<{1}>", parmTypeName, genericTypes);
42. }
43. public static string CommaSeparated(this IEnumerable<string> items)
44. {
45. return items.Any()
46. ? items.Aggregate((partialResult, item) =>
47. string.Format("{0}, {1}", partialResult, item))
48. : "";
49. }
50. }
看这段代码的同时,我将介绍一些新的概念。最终,你会对LINQ有一个较大的了解,同时知道如何在你的日常开发中应用它。
首行代码查找名字为Enumerable的类型对象:
1. var enumerableType = assembly.GetExportedTypes()
2. .Single(t => t.Name == "Enumerable");
Single方法会返回一个满足条件的对象,上例中,条件就是Name属性值为“Enumerable”。如果满足条件的对象不足或不止一个,该方法会抛出一个异常。
紧接的一行语句构建了一条查询,该查询会返回一个对象序列,包含每个API格式化后的信息。这是一个相当长的查询,然而,如果把它分解成几部分,并不难理解。首先,为查询定义了来源,查询公共静态方法的序列:
1. var methods = from m in enumerableType.GetMethods(
2. BindingFlags.Public | BindingFlags.Static)
接下来是一个LET子句,它帮助存储初步结果。此处,保存着是否为扩展方法的信息。如果序列不为空,Any方法就会返回true,否则为false.
1. let isExtension = m.GetCustomAttributes(
2. typeof(ExtensionAttribute), false)
3. .Any()
语句的最后一部分是一个select子句。它创建了一个包含三个字符串的匿名类型实例,三个字符串分别保存返回值类型、方法名和参数列表。此处使用了初始化器语法。返回值类型通过一个扩展方法进行查找,该扩展方法稍后我会介绍。方法名信息的查找使用另一个LINQ查询表达式,使用了方法调用语法:
1. vname = m.Name +
2. (m.GetGenericArguments().Any() ?
3. "<" + m.GetGenericArguments()
4. .Select(t => t.Name)
5. .CommaSeparated() + ">"
6. : ""),
方法名的获取很清楚。GetGenericArguments返回代表泛型参数类型信息的序列。调用Select为每一个泛型参数返回名称。CommaSeparated就是那个稍后会介绍的扩展方法。它的名称就暗示了其功能:将一个字符串列表转换为一个由逗号分隔的字符串。在泛型参数字符串两端加上尖括号。
初始化参数的代码利用另一个LINQ查询,该查询使用方法调用语法。
1. parameters = "(" + (isExtension ? "this " : "") +
2. m.GetParameters()
3. .Select(p => p.ParameterType.FormatTypeName()
4. + " " + p.Name)
5. .CommaSeparated()
6. + ")"
调用Select返回一个包含参数类型及参数名的字符串序列。CommaSeparated格式化序列为一个逗号分隔的字符串。如果该方法是扩展方法,余下的代码会添加一个this关键字,并且在整个字符串两端添加小括号。
最后,我们来看看两个扩展方法,分别是格式化泛型参数为字符串的FormatTypeName和把序列变成逗号分隔字符串的CommaSeparated。
CommaSeparated更简单些:它使用了Aggregate方法:
1. public static string CommaSeparated(this IEnumerable<string> items)
2. {
3. return items.Any()
4. ? items.Aggregate((partialResult, item) =>
5. string.Format("{0}, {1}", partialResult, item))
6. : "";
7. }
Aggregate是Enumerable类的一个成员方法。它将输入序列聚合为一个单个结果。此处,每个输入字符串都被追加到当前结果中,同时附加一个逗号作为分隔符。当你需要将一个输入序列变成单个结果时,你或许会考虑选择Aggregate作为工具使用。
FormatTypeName检查传入类型并返回该类型名,返回的类型名字符串与你在代码中看到一样。如果传入类型不是泛型,类型名称会被直接返回。否则,添加到元数据中的额外字符将被移除,并且增加尖括号包围的泛型参数。产品代码中,我几乎肯定会使用方法调用语法。为了举例,我把查询语法与方法调用语法混合使用,只是为了向你展示它们是等效的。再说一次,CommaSeparated负责连接字符串.
为了表达的更清楚,本例中我直接使用了字符串连接。多数情况下,我还是建议使用StringBuilder或是String.Format。这是少数情况下可以直接使用字符串连接,因为调用的次数相当少,所以产生的额外垃圾回收对程序运行不会有过多影响。
1. public static string FormatTypeName(this Type genericType)
2. {
3. if (!genericType.IsGenericType)
4. return genericType.Name;
5. var parmTypeName = genericType.Name.Remove(genericType.Name
6. .IndexOf('`'));
7. var genericTypes = (from g in genericType.GetGenericArguments()
8. select g.FormatTypeName())
9. .CommaSeparated();
10. return string.Format("{0}<{1}>", parmTypeName, genericTypes);
11. }
结论
对比LINQ使用前、后两套代码的可读性与易理解两方面。如果你没有养成使用LINQ的习惯,后面那个用了LINQ的版本可能会有些陌生。然而,如果你经常使用或阅读LINQ代码,那理解起来会更轻松。安德丝(Anders)经常说C#中添加LINQ及其它功能性的概念,有助于将编程过程中的关注点从“怎么做”转移到“做什么”上来。当你读别人写的代码时也是一样。读到LINQ版本的代码时,注意到对“做什么”显然要比“怎么做”关注的多。LINQ代码用查询表达式与方法调用代码了循环与条件判断。阅读这样的代码更多展现的是“正在做什么”而不是“如何来完成它”。对于包含多个列表及列表项的算法尤甚,或是泛型参数或是参数列表。LINQ版本快速揭示了这段代码正在将一系列字符串连接成一个由逗号分隔的独立字符串。之前的LINQ版本则通过执行连接的列表迅速展示了所有循环的边界情况:最后一项后面不用添加逗号,在字符串开始或结尾处添加括号等。
我编码时发现的另一重点是,我完成LINQ版本的代码所用时间只是完成非LINQ代码所用时间的一半。多半这些额外时间都来处理好些边界情况了。单个参数都有多条独立的路径,而且还不只一个参数。多类型参数和单类型参数也都有独立的路径。非LINQ版本的代码需要更多返回值是空列表和空引用的检查。LINQ的查询和方法对空输入序列则有合理的反映。
作为对基于LINQ实现代码的可维护性的说明,我们来添加一个需求。对于输出先按照方法名再按照参数数量进行分类。只需要在LINQ查询中增加一行代码(下面高亮的一行):
12. var methods = from m in enumerableType.GetMethods(
13. BindingFlags.Public | BindingFlags.Static)
14. let isExtension = m.GetCustomAttributes(
15. typeof(ExtensionAttribute), false)
16. .Any()
17. orderby m.Name, m.GetParameters().Count()
18. select new
19. {
20. returnParm = m.ReturnType.FormatTypeName(),
21. name = m.Name +
22. (m.GetGenericArguments().Any() ?
23. "<" + m.GetGenericArguments()
24. .Select(t => t.Name)
25. .CommaSeparated() + ">"
26. : ""),
27. parameters = "(" +
28. (isExtension ? "this " : "") +
29. m.GetParameters()
30. .Select(p => p.ParameterType.FormatTypeName()
31. + " " + p.Name)
32. .CommaSeparated()
33. + ")"
34. };
我不打算在非LINQ版本代码中实现这一需求了,因为没有太多必要这么做:我还需要分配临时存储空间。还要编写自定义比较函数。这大概还要在实现代码1中再增加一页了。通过应用LINQ,我可以在项目范围内适应更多新的需求。
从正式版发布至今,我一直在向开发人员解释LINQ。当开发人员了解并适应了这些新功能后,他们就会发现LINQ给他们代码在生产率与可维护性方面带来的益处。如果你是那少数还未采用LINQ中的一员,希望本文给了你另一个用它的理由。