C#与C++的发展历程第四 - C#6的新时代

C#6.0随着.NET Framework 4.6而来,.NET Framework 4.6相较于.NET Framework 4.5(包括4.5.一、4.5.2等)变化不是太大,C#6.0也不像以前版本升级时总有几个吸引眼球的变化,而只是一些语法糖般的变化,不过这个版本在编译器方面作了许多工做。每次升级总会有一些侧重点吧,并且C#发展这么多年,这么多特性已经使其位于顶级编程语言行列(从编码温馨度来看甩Java好几条街),实在是也很难再有什么突破性变化了吧。java

本文整理下C# 6的新变化,但愿对看到的园友有必定帮助。mysql

C#6开始,C#和C++有了很大的不一样,本文也再也不继续介绍C#6新特性对应的C++特性。nginx

自动属性改进

1.只读自动属性 能够声明真正的只读(不可变)属性(自动属性),在以前版本中若是要让自动属性不可写,惟一的方法就是将set设置为private。下面的示例提供了新旧两种代码的对比。sql

// 之前 public string Name {get; private set; } // 当前 public string Name {get; } 

对于这种只有get的只读自动属性,能够在构造函数中进行赋值,或者使用下面这种新的初始化语法进行赋值。编程

2.自动属性初始化
可使用以下的语法对自动属性或自动只读属性进行初始化。app

public string Name {get; set;} = "World"; public string Name {get; } = "World"; 

注意赋值语句最后有个分号,固然少了这个分号,VS立马就给错误提示了。less

表达式体做为函数实现

对于不少只有一行代码的函数,使用这个新特性能够减小一对括号,使代码看起来更简洁。以下面两种方法是等价的。异步

public string Hello(string name) { return $"Hello, {name}"; } public string Hello(string name) => $"Hello, {name}"; 

对于只读属性也可使用这个特性,如:

// .NET Core由Configuration中读取配置 public string DefaultFileName => this._configuration["DefaultFileName"]; 

null条件运算符

这个运算符有两种形式,分别为?.?[]。在C#支持这个运算符以前,咱们访问引用类型对象的属性或索引器都须要首先判断该对象是否为空以避免发生“空引用”异常。常见写法如:

if (section != null) { var path = section[name]; } 

而使用null条件运算符能够将上面的语句简化为:

var path = section?[name]; 

虽然看似只是消灭了两行括号,但看过周爱民老师的《JavaScript语言精粹》后了解到这是由过程式语言到函数式语言一种转变,即由命令语句转变为表达式。
若是访问的属性为引用类型,经过null条件运算符获得的结果的类型不变,而若是访问的属性为值类型,则经过null条件运算符获得的结果的类型为该值类型对于的可空类型的包装。
即若是name为string类型,则path依然为string类型。而若是name为int类型,则path会变成int?类型。能够经过??使path的类型和name的类型一致:

var path = section?[name]??0; 

虽然null条件运算符能够大大减小运行时错误(忘了检查引用是否为空)的发生。但不能忽视因为引用类型对象为空而致使属性取默认值所带来的结果错误。

null可空运算符不仅对访问属性、索引器等,也能够用来调用方法或触发事件。以下面两种写法:

if (_mysqlConn != null) { MysqlConn.Close(); } _mysqlConn?.Close(); 

调用事件也是同理:

PropertyChanged?.Invoke(e); 

using导入静态类型

在这个特性出现以前,咱们使用using指令只能引入命名空间。这个特性出现后,也能够将类型导入,从而能够直接调用类型中的静态方法。下面的例子能够很好的展现这个语法的使用:

using static System.String; return !IsNullOrEmpty(path); 

至今位置在实际编码过程当中没发现这个新功能有啥太明显的做用。可能惟一能少码一些代码的地方就像控制台应用程序中能够经过导入Console类,来减小调用频繁调用Console.WriteLine()方法时的输入量。

字符串插值

在这个特性出现以前,咱们用的最多的的字符串插值方法就是string.Format(),如:

var str = string.Format("{0}-{1}",No,Name); 

string.Format()的主要缺点就是很容易弄乱参数与占位符的位置,致使拼出错误的字符串。
如今有了这个特性,string.Format()方法基本能够退役了。以前的代码能够直接改写为下面的样子:

var str = $"{No}-{Name}"; 

之前用于string.Format()占位符的格式化字符串对于字符串插值语法也有效:

var str = string.Format("{0:00}",No); var str = $"{No:00}"; 

对于时间格式化也能够按以下简化,而且这种写法能够更容易的把时间“融入”到字符串中:

var dateStr = DateTime.Now.ToString("yyyy-MM-dd"); var dataStr = $"{DateTime.Now:yyyy-MM-dd}"; 

更强大的是$能够和@结合使用,这样遇到多行字符串,使用@表示的字符串字面量能够直接写成多行,同时可使用$来实现的字符串插值。对于在代码中嵌入SQL来讲这是一个很是方便的特性。

var sql = $@"insert into {table} (fromid,toid,strength) values ({fromid},{toid},{strength})"; 

如上面这个字符串,咱们既不须要用+作多行链接,又不用写string.Format(),整个代码看上去干净、整洁。
只要是C#表达式,即便包含很是复杂的计算也均可以用于字符串插值。

nameof关键字

nameof的功能很单一,就是获取一个符号的名称,这个“符号”能够是参数,成员,属性等。nameof的一个用途经过下面的例子来展现:

以下方法是一个常见的检查参数是否为空的方法(这段代码本身的项目用了好久,但忘记最初是从哪“借鉴”的了):

public static void CheckNotNull<T>(this T value, string paramName) where T : class { Require<ArgumentNullException>(value != null, string.Format(Resources.ParameterCheck_NotNull, paramName)); } // Require方法的实现省略,其功能是检查参数值是否为空,若是为空记录一条含有参数名称的日志 

调用这个方法也很简单:

public void Process(int no, string name) { no.CheckNotNull(nameof(no)); name.CheckNotNull(nameof(name)); // ...省略 } 

在nameof关键字出现以前,咱们只能写常量字符串。

no.CheckNotNull("no"); 

若是参数名一直不变,这样的常量字符串写法就不会有问题。但现实状况是项目重构常常会发生,一但参数名改变,咱们可能会忘记修改常量字符串。而若是咱们使用nameof关键字,咱们在更改参数名的同时VS这样的IDE都是自动帮咱们进行重构,把全部用到此参数的地方都进行重命名操做。

另一个nameof经常使用的场景是如WPF这种的XAML应用中,当属性须要触发PropertyChanged时便利性会有很大提高。在nameof关键出现以前,MVVMLight库的作法是要求传入一个lambda表达式,经过解析Lambda表达式体来使调用强类型话,并保证传给PropertyChanged的参数名是正确的。代码以下:

public string Name { get { return _name; } // Set方法会最终调用下面的RaisePropertyChanging方法 set { Set(() => Name, ref _name, value); } } // MvvmLight源代码(部分,来自ObservableObject.cs文见) protected virtual void RaisePropertyChanging<T>(Expression<Func<T>> propertyExpression) { var handler = PropertyChanging; if (handler != null) { var propertyName = GetPropertyName(propertyExpression); handler(this, new PropertyChangingEventArgs(propertyName)); } } 

如今有了nameof关键字,上面的Name属性能够实现为:

public string Name { get { return _name; } set { if (value != _name) { _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UXComponents.ViewModel.Name))); } } } 

节省的代码和运算复杂度都是不少的。

异常过滤器

异常过滤去用于在catch捕获异常前进行一次过滤。博主尚未在项目中用到过这个特性,这里用MSDN上的一段代码来讲明。

public static async Task<string> MakeRequest() { var client = new HttpClient(); var streamTask = client.GetStringAsync("https://localHost:10000"); try { var responseText = await streamTask; return responseText; } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301")) { return "Site Moved"; } } 

catch语句中when开始那部分就是新增的异常过滤器。
若是when后面语句(即异常过滤器)执行结果为truecatch段中的代码会正常执行,而若是异常过滤器执行结果为falsecatch段会被跳过。
在异常过滤器出现以前,相似功能的代码要实现为:

try { var responseText = await streamTask; return responseText; } catch (System.Net.Http.HttpRequestException e) { if (e.Message.Contains("301")) return "Site Moved"; else throw; } 

可是以前这种实现方式中经过throw来从新抛出异常会致使一些异常信息丢失。而使用异常过滤器返回false跳过的异常会保留全部原始的异常信息。
异常过滤器也是叠加使用,如:

try { var responseText = await streamTask; return responseText; } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301")) { return "Site Moved"; } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("304")) { return "Use the Cache"; } 

第二个推荐的异常过滤器的使用模式是须要将一个更泛化的异常catch放在具体的catch以前的。
好比,记录日志这种需求,咱们须要在一个泛化的异常catch中记录日志,但不处理异常,异常能够继续向下传递,并被更具体的catch进行处理。

能够实现一个这样的记录异常的扩展方法。

public static bool LogException(this Exception e) { Console.Error.WriteLine(@"Exceptions happen: {e}"); return false; } 

而后能够像以下这样进行使用:

try { PerformFailingOperation(); } catch (Exception e) when (e.LogException()) { // This is never reached! } catch (RecoverableException ex) { Console.WriteLine(ex.ToString()); } 

因为上面的LogException方法返回false,因此第一个catch不会处理异常,异常会向下传播并被第二个catch所捕获。

第三个异常过滤器的使用场景是用于区分在调试模式下和生产模式下的异常的处理。

try { PerformFailingOperation(); } catch (RecoverableException ex) when (!System.Diagnostics.Debugger.IsAttached) { Console.WriteLine(ex.ToString()); } 

如上代码,在附加调试器的状况下catch将不被执行,异常向下抛出并被调试器捕获从而进入调试状态。而在生产模式,异常会被捕获并处理。
在这个特性出现以前,若是咱们想方便的调试出现的异常最多见的方法就是在catch段的第一行打上断点。而如今有了异常过滤器,只须要添加这样一个when子句就能够了。

C#异常过滤器的语法有点支持模式匹配的语言的影子,听说C#7会全面支持模式匹配。期待一下。

索引初始化器

在C#3起出现的集合初始化器可使咱们用以下这样的方式去初始化ListDictionary。代码例子以前的博文

List<Plant> plants = new List<Plant> { new Plant { Name = "牡丹", Category = "芍药科", ImageId =6}, new Plant { Name = "莲", Category = "莲科", ImageId =10 }, new Plant { Name = "柳", Category = "杨柳科", ImageId = 12 } }; Dictionary<int, Plant> plantsDic = new Dictionary<int, Plant> { { 11, new Plant { Name = "牡丹", Category = "芍药科", ImageId =6}}, { 12, new Plant { Name = "莲", Category = "莲科", ImageId =10 }}, { 13, new Plant { Name = "柳", Category = "杨柳科", ImageId = 12 }} }; 

C#6中新增了一种索引初始化器,可使Dictionary的初始化更直观:

Dictionary<int, Plant> plantsDic = new Dictionary<int, Plant> { [11] = new Plant { Name = "牡丹", Category = "芍药科", ImageId =6}, [12] = new Plant { Name = "莲", Category = "莲科", ImageId =10 }, [13] = new Plant { Name = "柳", Category = "杨柳科", ImageId = 12 } }; 

添加Add扩展方法使类支持集合初始化去

咱们按以下方式实现一个集合类:

public class Enrollment : IEnumerable<Plant> { private List<Plant> allPlants = new List<Plant>(); public void Add(Plant s) { allPlants.Add(s); } public IEnumerator<Plant> GetEnumerator() { return ((IEnumerable<Plant>)allPlants).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<Plant>)allPlants).GetEnumerator(); } } 

因为这个类的实现有符合要求的Add方法,咱们可使用集合初始化器来给类的成员变量添加对象。

Plantation plantation = new Plantation() { new Plant {Name = "牡丹", Category = "芍药科", ImageId = 6}, new Plant {Name = "莲", Category = "莲科", ImageId = 10}, new Plant {Name = "柳", Category = "杨柳科", ImageId = 12} }; 

但若是因为各类缘由,咱们的Add方法被命名为其它名称,如:

public void Plant(Plant s) { allPlants.Add(s); } 

则集合初始化器方式再也不可用。为了让集合初始化其继续可用,能够添加下面这样的扩展方法:

public static class PlantExtensions { public static void Add(this Plantation e, Plant s) => e.Plant(s); } 

这样集合初始化器就又能够用了。

其它

  1. struct中能够声明无参构造函数。在以前版本的C#中,struct只能包含有参构造函数。
  2. 能够在catch/finally使用await语句了,一个典型的做用就是须要在catch中使用异步的logger方法这样的状况。
  3. C#6新的编译器会更智能的区分Task.Run(Action)Task.Run(Func<Task>())这种的重载,再遇到Task DoThings(){ }这种签名的重载时会智能的选择后者。

提示:
C#语言的编译与项目所依赖的.Net Framework版本无关。虽然VS在2015版本才内置支持C#6.0的编译器,但咱们仍然可使用VS2015编写基于.Net Framework 3.5甚至更早版本Framework的项目并享受C#6带来的如字符串插值等便利特性。
若是想脱离VS编译C#6的项目,须要使用随VS2015安装的Microsoft Build Tools 2015(也能够单独下载安装,安装位置在%SystemDrive%\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe),而不能使用位于%SystemDrive%\Windows\Microsoft.NET\Framework64\v4.0.30319中的Build Tool。

展望

C#7应该年末就会到来,对C#7比较期待的几点包括“外观”很简单的值类型元组,对象展开功能。有了这些C#7就能达到比Python还要流畅的代码编写感觉了。与各位C#er共勉。

相关文章
相关标签/搜索