代码改变世界

C# 5.0中新增特性

2016-02-02 15:19  Sam tsai  阅读(2828)  评论(0编辑  收藏  举报

C# 5.0随着VisualStudio 2012一起正式发布了,让我们来看看C#5.0中增加了哪些功能。

1. 异步编程

在.Net 4.5中,通过async和await两个关键字,引入了一种新的基于任务的异步编程模型(TAP)。在这种方式下,可以通过类似同步方式编写异步代码,极大简化了异步编程模型。如下式一个简单的实例:

    static async void DownloadStringAsync2(Uri uri)
    {
        var webClient = new WebClient();
        var result = await webClient.DownloadStringTaskAsync(uri);
        Console.WriteLine(result);
    }

 

而之前的方式是这样的:

    static void DownloadStringAsync(Uri uri)
    {
        var webClient = new WebClient();
        webClient.DownloadStringCompleted += (s, e) =>
            {
                Console.WriteLine(e.Result);
            };
        webClient.DownloadStringAsync(uri);
    }

 

也许前面这个例子不足以体现async和await带来的优越性,下面这个例子就明显多了:

   

 1  public void CopyToAsyncTheHardWay(Stream source, Stream destination)
 2     {
 3         byte[] buffer = new byte[0x1000];
 4         Action<IAsyncResult> readWriteLoop = null;
 5         readWriteLoop = iar =>
 6         {
 7             for (bool isRead = (iar == null); ; isRead = !isRead)
 8             {
 9                 switch (isRead)
10                 {
11                     case true:
12                         iar = source.BeginRead(buffer, 0, buffer.Length,
13                             readResult =>
14                             {
15                                 if (readResult.CompletedSynchronously) return;
16                                 readWriteLoop(readResult);
17                             }, null);
18                         if (!iar.CompletedSynchronously) return;
19                         break;
20                     case false:
21                         int numRead = source.EndRead(iar);
22                         if (numRead == 0)
23                         {
24                             return;
25                         }
26                         iar = destination.BeginWrite(buffer, 0, numRead,
27                             writeResult =>
28                             {
29                                 if (writeResult.CompletedSynchronously) return;
30                                 destination.EndWrite(writeResult);
31                                 readWriteLoop(null);
32                             }, null);
33                         if (!iar.CompletedSynchronously) return;
34                         destination.EndWrite(iar);
35                         break;
36                 }
37             }
38         };
39         readWriteLoop(null);
40     }
41 
42     public async Task CopyToAsync(Stream source, Stream destination)
43     {
44         byte[] buffer = new byte[0x1000];
45         int numRead;
46         while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
47         {
48             await destination.WriteAsync(buffer, 0, numRead);
49         }
50     }

 

关于基于任务的异步编程模型需要介绍的地方还比较多,不是一两句能说完的,有空的话后面再专门写篇文章来详细介绍下。另外也可参看微软的官方网站:Visual Studio Asynchronous Programming,其官方文档Task-Based Asynchronous Pattern Overview介绍的非常详细, VisualStudio中自带的CSharp Language Specification中也有一些说明。

2. 调用方信息

很多时候,我们需要在运行过程中记录一些调测的日志信息,如下所示:

    public void DoProcessing()
    {
        TraceMessage("Something happened.");
    }

 

为了调测方便,除了事件信息外,我们往往还需要知道发生该事件的代码位置以及调用栈信息。在C++中,我们可以通过定义一个宏,然后再宏中通过__FILE__和__LINE__来获取当前代码的位置,但C#并不支持宏,往往只能通过StackTrace来实现这一功能,但StackTrace却有不是很靠谱,常常获取不了我们所要的结果。

针对这个问题,在.Net 4.5中引入了三个Attribute:CallerMemberName、CallerFilePath和CallerLineNumber。在编译器的配合下,分别可以获取到调用函数(准确讲应该是成员)名称,调用文件及调用行号。上面的TraceMessage函数可以实现如下:

  

  public void TraceMessage(string message,
            [CallerMemberName] string memberName = "",
            [CallerFilePath] string sourceFilePath = "",
            [CallerLineNumber] int sourceLineNumber = 0)
    {
        Trace.WriteLine("message: " + message);
        Trace.WriteLine("member name: " + memberName);
        Trace.WriteLine("source file path: " + sourceFilePath);
        Trace.WriteLine("source line number: " + sourceLineNumber);
    }

 

另外,在构造函数,析构函数、属性等特殊的地方调用CallerMemberName属性所标记的函数时,获取的值有所不同,其取值如下表所示:

调用的地方

CallerMemberName获取的结果

方法、属性或事件

方法,属性或事件的名称

构造函数

字符串 ".ctor"

静态构造函数

字符串 ".cctor"

析构函数

该字符串 "Finalize"

用户定义的运算符或转换

生成的名称成员,例如, "op_Addition"。

特性构造函数

特性所应用的成员的名称

例如,对于在属性中调用CallerMemberName所标记的函数即可获取属性名称,通过这种方式可以简化 INotifyPropertyChanged 接口的实现。关于调用方信息更详细的资料,请参看MSDN:http://msdn.microsoft.com/zh-cn/library/hh534540.aspx

方法调用信息

这是一个被写在Writting Enterprisey Code上的完整风格指南,但是其中我最喜欢的是迷人对你调用过的所有函数的日志记录:

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推荐使用可选的参数,这意味着方法调用者可以不用考虑参数,编译器将会用默认值填充。

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);
}

现在,如果调用LogTrace("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,不需要逐字的匹配字符串,表达式:

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]得到调用代码的行号,这个也许对诊断方法有用,但是如果你真的需要它,这也许是这段调用代码太过“企业化”的迹象。

在lambdas中使用循环变量

技术上,这个对长期存在的困扰和煎熬的修正,但是使得C#增加了可用性,所以我将会提及它。

自从C# 3以来,编写匿名函数比命名的更见快捷和容易了,匿名函数被广泛地使用鱼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中使用循环变量,但是它将给你一个错误的结果。它会使用循环退出时的玄幻变量值,不是呗捕获时的值。

例如,下面是一个返回“加法器”集合的函数:

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));
}

很明显这大错特错!在返回的集合中的每个函数都在捕获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中,这些修复以及你可以覆盖的循环变量能使你获得你期望的结果。

via:mindscapehq.com , OSChina原创编译