C#与C++的发展历程第四 - C#6的新时代
C#6.0随着.NET Framework 4.6而来,.NET Framework 4.6相较于.NET Framework 4.5(包括4.5.1、4.5.2等)变化不是太大,C#6.0也不像之前版本升级时总有几个吸引眼球的变化,而只是一些语法糖般的变化,不过这个版本在编译器方面做了许多工作。每次升级总会有一些侧重点吧,而且C#发展这么多年,这么多特性已经使其位于顶级编程语言行列(从编码舒适度来看甩Java好几条街),实在是也很难再有什么突破性变化了吧。
本文整理下C# 6的新变化,希望对看到的园友有一定帮助。
C#6开始,C#和C++有了很大的不同,本文也不再继续介绍C#6新特性对应的C++特性。
自动属性改进
1.只读自动属性 可以声明真正的只读(不可变)属性(自动属性),在之前版本中如果要让自动属性不可写,唯一的方法就是将set
设置为private
。下面的示例提供了新旧两种代码的对比。
// 以前
public string Name {get; private set; }
// 当前
public string Name {get; }
对于这种只有get的只读自动属性,可以在构造函数中进行赋值,或者使用下面这种新的初始化语法进行赋值。
2.自动属性初始化
可以使用如下的语法对自动属性或自动只读属性进行初始化。
public string Name {get; set;} = "World";
public string Name {get; } = "World";
注意赋值语句最后有个分号,当然少了这个分号,VS立马就给错误提示了。
表达式体作为函数实现
对于很多只有一行代码的函数,使用这个新特性可以减少一对括号,使代码看起来更简洁。如下面两种方法是等价的。
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
后面语句(即异常过滤器)执行结果为true
则catch
段中的代码会正常执行,而如果异常过滤器执行结果为false
则catch
段会被跳过。
在异常过滤器出现之前,类似功能的代码要实现为:
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起出现的集合初始化器可以使我们用如下这样的方式去初始化List
及Dictionary
。代码例子之前的博文。
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);
}
这样集合初始化器就又可以用了。
其它
- struct中可以声明无参构造函数。在之前版本的C#中,struct只能包含有参构造函数。
- 可以在
catch
/finally
使用await
语句了,一个典型的作用就是需要在catch
中使用异步的logger方法这样的情况。 - 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共勉。