C#5新特性详解之二——方法调用和Lambda表达式
C# 5中对于异步编程的支持毫无疑问是第一大新特性,这个在我上周的文章http://www.mindscapehq.com/blog/index.php/2012/03/13/asynchronous-programming-in-c-5/ 里已经写过了。不过还有一些其他C#使用者在意的新特性我觉得也有必要提一下。
方法调用信息
有一个非常全面的企业程序设计指南说道:如果你使用VB编程,你会习惯于使用日志记录每一个被调用的方法:
Function AddTwoNumbers(a As Integer, b As Integer) As Integer
Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Entering AddTwoNumbers")
Dim result = OracleHelpers.ExecInteger("SELECT " & a & " + " & b)
Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Calling PrintPurchaseOrders")
PrintPurchaseOrders() ' IFT 12.11.96: don't know why this is needed but shipping module crashes if it is removed
Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Returned from PrintPurchaseOrders")
Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Exiting AddTwoNumbers")
Return result
End Function
虽然上面符合企业软件标准的代码已经足够高效、简介,但使用C# 5的话它会更加出色。C# 4中引入了“可选参数”(optional parameters)这一概念,也就是说,方法即使不使用参数,编译器也会给它赋上默认值。
public void WonderMethod(int a = 123, string b = "hello") { ... }
WonderMethod(456);
// compiles to WonderMethod(456, "hello")
WonderMethod();
// compiles to WonderMethod(123, "hello")
使用C# 5,在可选参数上定义特殊属性,编译器就可以将调用方法的信息给它赋值。这意味着能够在使用Logger.Trace()时将会自动日志记录调用信息。
public static void Trace(string message, [CallerFilePath] string sourceFile = "", [CallerMemberName] string memberName = "") {
string msg = String.Format("{0}: {1}.{2}: {3}",
DateTime.Now.ToString("yyyy-mm-dd HH:MM:ss.fff"), // Lurking 'minutes'/'months' bug introduced during .NET port in 2003 and has not been noticed because nobody ever looks at the log files because they contain too much useless detail
Path.GetFileNameWithoutExtension(sourceFile),
memberName,
message);
LoggingInfrastructure.Log(msg);
}
也就是说,如果调用Log.Trace("some message"),编译器会将文件和成员被调用的信息而不是空字符串给可选变量赋值。
// In file Validation.cs
public void ValidateDatabase() {
Log.Trace("Entering method");
// compiles to Log.Trace("Entering method", "Validation.cs", "ValidateDatabase")
Log.Trace("Exiting method");
}
注意:你应用的变量必须是可选的。如果它们不是可选的,C#编译器会要求调用函数来提供初始值,并且用来覆盖默认值。
另一个例子就是在实现INotifyPropertyChanged接口时,并不需要使用字符串、正则表达式匹配或者奇怪的结构(mystic weavers)。
public class ViewModelBase : INotifyPropertyChanged {
protected void Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "") {
if (!Object.Equals(field, value)) {
field = value;
OnPropertyChanged(propertyName);
}
}
// usual INPC boilerplate
}
public class Widget : ViewModelBase {
private int _sprocketSize;
public int SprocketSize {
get { return _sprocketSize; }
set { Set(ref _sprocketSize, value); }
// Compiler fills in "SprocketSize" as propertyName
}
}
你甚至还可以通过[CallerLineNumber]获取调用函数的行号,这对于诊断方法非常有用。不过如果你真的需要它,那也许会是太过“企业化”的现象。
在Lambda表达式中使用循环变量
技术上来说,这是对一个长期存在的困惑和痛苦的问题修复。它使得C#更具可用性,所以无论如何我都需要提到它。
从C# 3开始,得利于Lambda语法的使用,匿名函数变得比命名函数更加快速和简单。匿名函数在LINQ中广泛使用,当你不希望在参数化各层的类、接口以及虚拟函数时,也是个不错的选择。匿名函数的一个重要特色就是它们能从本地环境中捕获变量。下面是一个例子:
public static IEnumerable<int> GetGreaterThan(IEnumerable<int> source, int n) {
return source.Where(i => i > n);
}
这儿, i => i > n 是一个抓取n的值的匿名函数,例如,n=17,函数就是i => i > 17。
在早期的C#版本中,如果你在循环语句中无法在Lambda语法中使用循环变量。实际上,比想象的更糟,当你在Lambda语法里使用循环变量时它会给你错误的结果——它是在循环的初始值而不是终值。
例如:下面是一个返回一系列'adder'值的方法,每一个'adder'对应每一个输入的加数。
public static List<Func<int, int>> GetAdders(params int[] addends) {
var funcs = new List<Func<int, int>>();
foreach (int addend in addends) {
funcs.Add(i => i + addend);
}
return funcs;
}
我们输出一下看看:
var adders = GetAdders(1, 2, 3, 4, 5);
foreach (var adder in adders) {
Console.WriteLine(adder(10));
}
// Printout: 15 15 15 15 15
很明显,这是一个严重错误!每个返回的函数都是加到5的结果。这是因为循环变量被覆盖了,而循环变量的最终值为5。
为了让它在C# 3和4里都能运行,你不得不记得先复制循环范围内的循环变量到本地变量中,再用Lambda覆盖本地变量。
foreach (var addend_ in addends) {
var addend = addend_;
// DON'T GO NEAR THE LOOP VARIABLE
funcs.Add(i => i + addend)
}
因为函数是覆盖本地变量而不是循环变量了,因此能将这个值保存好,也就会得到正确的结果。
顺便说一下,这并不是一个模糊不清的例子——我在项目中就多次碰到过。实际上,我就遇到过在一个项目中实现一个过滤器功能,这个函数由用户专用的约束对象集合组成。该代码循环处理约束对象,并构建代表子句的函数列表(如 Name Equals "BOB" 变成 r =>r["Name"]=="BOB"),然后将这些函数组合成最终的过滤器,并运行这所有的子句检查他们是否为真。我第一次就没有运行成功,因为所有子句都覆盖了同一个约束对象——集合中的最后一个。
C# 5修复了这一问题,你将可以获得预期的结果。如果你想利用C#的混合面向对象函数特性,它为你避免了一个多年来一直在制造问题的大陷阱。
从语言的角度来看,C# 5并没有很多需要学习的新功能,虽然异步编程和等待关键字确实是两个非常大的突破。编程快乐!
作者 王然
补充:软件开发 , C# ,