C# 6 新特性
微软新一代开发工具Visual Studio 2015
正式版已经于北京时间 2015年7月20日 23:30 发布了,而作为.Net
开发中的主力开发语言C#
,也已经走到了C# 6
。
那么作为C#
开发语言的最新版本,微软又给它添加或改进了哪些特性呢?
C# 6
的新特性是和微软的 .Net 开源编译器Roslyn紧密相连的,在其 GitHub 页面中,也给出了C# 6
新特性:New Language Features in C# 6。下面内容一部分为简单的翻译,以及在查看时的一些理解和疑问,由于目前我还没发下载最新版 Win10 和 VS 2015,因此有些地方暂时还没发验证猜想。等待后续更新。
下面先列出C# 6
中的新特性:
自动属性初始值表达式
public int X { get; set; } = x;
只读自动属性
public int Y { get; } = y;
静态导入
using static
using static System.Console; // ... Write(4);
索引器对象初始化
new JObject { ["x"] = 3 }
在
catch
和finally
块中使用await
try { ... } catch { await ... } finally { await ... }
异常筛选器
catch(Exception e) when (e.Count > 5) { ... }
函数/属性表达式
public double Dist => Sqrt(X * X + Y * Y);
Null条件运算符
customer?.Orders?[5]
字符串插值
$"{p.Name} is {p.Age} years old."
nameof
运算符string s = nameof(Console.Write);
其它改进
- #pragma
- 集合初始化支持扩展方法
Add
- 重载方法解析改进
自动属性增强
自动属性初始值表达式
现在可以为自动属性添加一个初始值表达式了,如下:
public class Customer
{
public string First { get; set; } = "Jane";
public string Last { get; set; } = "Doe";
}
我们知道所谓自动属性,其实是属性的一种快速写法,编译器为我们做了额外的工作,包括为自动属性生成对应的字段以及添加读写方法。这里的自动熟悉初始值,会被直接赋值给属性对应的字段,而不经过自动属性的setter
索引器,就如同直接给字段赋初始值一样。因此,如果我们在应用中使用了某些AOP
框架,并且为属性添加了NotifyPropertyChanged
或类似特性,当属性被初始化的时候,我们并不能得到通知。
和字段的初始化一样,自动属性初始化表达式中不能引用
this
,因为在类被初始化之前它们就已经被执行了。
只读自动属性
现在我们可以声明一个只读自动熟悉了,同时使用上面的自动熟悉初始化表达式设置初始值。
public class Customer
{
public string First { get; } = "Jane";
public string Last { get; } = "Doe";
}
只读自动属性对应的字段被隐式的声明为readonly
。因此,一个只读属性还可以在其声明类型的构造函数中赋值,实质上也还是直接对属性所对应的字段赋值。
public class Customer
{
public string Name { get; };
public Customer(string first, string last)
{
Name = first + " " + last;
}
}
这使得我们可以更简洁的表述类型,但是它同时也消除了语言中可变类型与不可变类型之间的区别:自动属性是一种简写,只有当我们愿意让我们的类可变并且属性适合使用默认值初始化的时候才使用。现在,使用只读属性,可变与不可变之间区别不再。
成员表达式
Lambda表达式可以被定义为一个表达式主体以及传统函数那样由一个块包含的函数体。现在这种特性也可以用来定义类型成员了。
方法表达式
方法、用户自定义运算符以及转换操作现在可以通过使用“Lambda箭头”给定一个表达主体来定义了。
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public static implicit operator string(Person p) => p.First + " " + p.Last;
就如同方法语句块中只有一个返回表达式一样。
对于无返回值的方法、包括返回Task
类型的方法、这种语法仍然适用,但是箭头后面必须是一个声明表达式:
public void Print() => Console.WriteLine(First + " " + Last);
属性表达式
属性和索引器可以有读方法(get
)以及写方法(set
)。上述的表达式写法可以被用到只读属性和索引器中:
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
注意这里并没有
get
关键字,使用这种语法时它是隐式声明的。
Using static
这个特性使我们可以声明静态方法所在命名空间,然后再代码中可以直接使用静态方法而不用添加其所在命名空间前缀。
using static System.Console;
using static System.Math;
using static System.DayOfWeek;
class Program
{
static void Main()
{
WriteLine(Sqrt(3*3 + 4*4));
WriteLine(Friday - Monday);
}
}
当我们需要使用特定域中的一组函数时很有用,System.Math
只是一个常见的例子。同时,它还允许直接指定枚举类型个人命名,比如System.DayOfWeek
。
扩展方法
扩展方法是静态方法,但是被当作实例方法来使用。通过使用using static
而不是引入命名空间,使得一个类型的扩展方法可被真正做为“扩展”方法。
using static System.Linq.Enumerable; // The type, not the namespace
class Program
{
static void Main()
{
var range = Range(5, 17); // Ok: not extension
var odd = Where(range, i => i % 2 == 1); // Error, not in scope
var even = range.Where(i => i % 2 == 0); // Ok
}
}
Null条件运算符
在我们的代码中通常要进行一堆的 null 检查,现在C# 6
引入了新的运算符,只有当运算符作用者不为null
时才能访问其成员和元素,否则表达式返回null
。
int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null
Null条件运算符可以和空合并运算符 ??
联合使用:
int length = customers?.Length ?? 0; // 0 if customers is null
Null条件运算符只有在检查到成员非空时才会执行后面紧跟的成员访问、元素访问和方法调用。
int? first = customers?[0].Orders.Count();
上面这个例子本质上等价于:
int? first = (customers != null) ? customers[0].Orders.Count() : null;
除了customers
被计算一次之外,除非customers
非空,否则跟在 ?
后面的成员访问和方法调用将不会被执行。
当然,Null条件运算符可以被连续调用,每次调用都会进行null
检查。
int? first = customers?[0].Orders?.Count();
注意一个方法调用不可以直接跟在 ?
运算符之后,因为这会导致太多的语法歧义。
等待测试。这里在
?
运算符后面直接跟的方法是指当前类型中的方法还是?
前面的变量所拥有的实例方法?看前面的示例,是可以直接调用变量的实力方法的(...Orders?.Count();
),如果是说当前类型的方法,那么歧义是因为与三目运算符?:
冲突吗?
然而,我们可以调用委托中的Invoke
方法:
if (predicate?.Invoke(e) ?? false) { … }
这种使用方法应该在事件触发的时候很常用:
PropertyChanged?.Invoke(this, args);
这是一种简单并且线程安全
的方式去在触发事件之前做 null 检查。说它是线程安全
的,是因为这个方式只执行 ?
左边表达式一次,并且将其保存在一个临时变量中。
说明:这里保存在临时变量中的是那个
PropertyChanged
实例。也就是说,上面的事件触发代码等价于:
var handler = PropertyChanged; if (handler != null) { handler.Invoke(this, args); }
而这里,需要说明在
.Net
中使用C#
进行事件的订阅和取消时,其实返回的Handler
是一个新的对象,所以对于上面的代码来说,当我们通过临时变量handler
引用PropertyChanged
实例之后,即使又有线程对这个事件进行了取消订阅操作导致事件变为null
,也不会影响这个临时变量的值,因此不会导致接下来的调用出现异常。因此,通过
?
方式触发事件是线程安全
的(这个也应该是编译器特别优化的吧)。而在使用C# 6
之前的语言编程时,如果要触发一个事件,我们也应该像上面那样通过一个临时变量引用事件实例,然后再判断触发,这样才能保证事件的线程安全。
字符串插值
string.Format
和它的同类被我们经常使用,但是它们的用法有一点笨拙且容易出错。特别是占位符{0}
等,它们必须与参数分别提供:
var s = String.Format("{0} is {1} year old", p.Name, p.Age);
字符串插值允许我们通过“洞”来直接把表达式插入到正确的位置:
var s = $"{p.Name} is {p.Age} year old";
和string.Format
一样,我们可以定义一些可选的对齐及格式方式:
var s = $"{p.Name,20} is {p.Age:D3} year old";
“洞”的内容可以是任意表达式,甚至是其它字符串:
var s = $"{p.Name} is {p.Age} year{(p.Age == 1 ? "" : "s")} old";
注意条件表达式被括号括起来了,所以
: "s"
并不会和格式说明符混淆。
nameof表达式
有时候我们会需要提供一些这样的字符串,字符串内容是某些程序元素的名称。比如当我们抛出一个ArgumentNullException
异常的时候,我们希望可以给出出错的参数名称;而当我们激活一个PropertyChanged
事件的时候,我们需要给出变化的属性的名称;等等。
使用一个硬编码的字符串很简单,但是很容易出错。我们可能一不小心出现拼写错误,又或者当进行代码重构时这个字符串就会过期。nameof
表达式本质上是一个字符串,由编译器检查你所给的参数并赋值,Visual Studio 知道它所引用的内容,所以导航和重构都能正确工作。
(if x == null) throw new ArgumentNullException(nameof(x));
你可以在nameof
表达式传递更复杂的由点分割的参数,但这只是告诉编译器该看哪里:只有最后的标识符将被使用:
WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"
索引器对象初始化
对象和集合初始化可用于声明性的初始化对象字段和属性、或者给一组集合元素初始化,但是字典对象或者带索引器的对象初始化时却不甚优雅。现在我们可以通过新的对象初始化语法来给索引器指定的键设置初值,如下所示:
var numbers = new Dictionary<int, string>
{
[7] = "seven",
[9] = "nine",
[13] = "thirteen"
};
异常过滤器
VB
中存在,F#
中也存在,现在C#
中也有了。它长的大概像这样:
try { … }
catch (MyException e) when (myfilter(e))
{
…
}
如果括号表达式中的结果为true
,那么将运行catch
块,否则异常将被继续向上抛出。
异常过滤器比捕捉然后重新抛出异常的方式更好,因为异常过滤器不会破坏堆栈内容。如果一个异常最后会导致堆栈被清空,使用过滤器方式你可以看到异常的原始来源,而不是它最后被重新抛出的地方。
异常过滤器有一些被普遍认可的“滥用”,比如日志记录,而这会导致一些副作用。它们可以检查而不拦截异常,这样,这些过滤器通常会给出一个false
返回值,这会产生一些副作用,比如异常块总是不被执行:
private static bool Log(Exception e) { /* log it */ ; return false; }
…
try { … } catch (Exception e) when (Log(e)) {}
不知道异常过滤器函数签名是不是被固定了,如果仅仅要求函数有一个bool返回值(以及一个
Exception
参数和其它可选参数),那么对于异常过滤器函数,我们可以通过传入更多的参数来决定返回值是true
或者false
,以此来控制异常处理块是否执行。
在catch和finally块中使用await
下面是微软原文的幽默:
C# 5
中我们不允许在catch块和finally块中出现await
关键字,因为我们设法让自己相信那是不可能实现的。但是现在我们弄懂了,显然这并不是不可能的。
这实际上是一个很值得注意的限制,使得人们不得不用其它“丑陋的”解决方式。现在这个限制不在了:
Resource res = null;
try
{
res = await Resource.OpenAsync(…); // You could do this.
…
}
catch(ResourceException e)
{
await Resource.LogAsync(res, e); // Now you can do this …
}
finally
{
if (res != null) await res.CloseAsync(); // … and this.
}
这个实现是相当复杂的,但是你不用去担心。这是这个语言中异步的重点。
集合初始化器扩展方法
当我们在
C#
中首次实现集合初始化器的时候,Add
方法不能作为扩展方法调用。VB
从一开始就有了,但是看起来我们似乎忘了在C#
中加入。现在这个问题解决了:集合初始化代码中可以很愉快的使用一个叫做Add
的扩展方法。这不是一个大的特性,但是它偶尔很有用,而结果证明在新的编译器中实现它相当于删除一个阻止它的检查。
改进的重载解析
在重载解析上有很多小的改进,这可能会使得很多事情会按照你所期望的方式工作。所有的改进都和“betterness”有关,所谓“betterness”,是编译器决定对于指定参数两个重载方法哪一个更合适的方法。
当在可空值类型重载方法之间选择时,你可能会注意到这点;另一点是在函数组(相对于Lambdas)中重载期望的委托。细节不值得在这里多说,只是想让你知道!