九月份,C# 8.0作为.Net Core 3.0的一部分正式发布了。本文会基于Ubuntu 18.04上的.Net Core 3.0介绍C# 8.0的各种新功能。
刚毕业时用了差不多5年C/C++,之后一口气折腾了七八年C#。论语言优雅和开发效率,C#比Java先进了不少。那时候每次看到TIOBE,VB.Net加C#的rating稳步提升,Java逐步降低,就会开心得不得了(为站对了队而开心?)。可惜由于.Net出身问题,一直在很封闭的圈子里发展,甚是缓慢。.Net份额始终没过10%。有一阵子在上海一家用.Net做开发的互联网公司工作。放眼望去,除了东家,上海只有携程还在成规模的用.Net,人才甚是难招。没多久,携程也投奔了Java,上海.Net环境更加恶劣了。等我离开前东家没一年,就听说前东家也全面转Java。
虽然这几年微软拥抱Linux,拥抱开源,.Net Core越来越给力。但无奈Python/Javascript/Go异军突起,抢了.Net和PHP的大量份额(貌似Java反而受影响很小),.Net份额已经跌到了7%。
然而对一个人、一件事、一个物的真爱又怎会随潮流和时间而变?平时依旧会关注.Net的发展。九月份,C# 8.0作为.Net Core 3.0的一部分正式发布了。本文会基于Ubuntu 18.04上的.Net Core 3.0介绍`C# 8.0](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8)各种新功能。
安装.Net Core 3.0
在Ubuntu 18.04上安装.Net Core Runtime和.Net SDK
1$ wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
2$ sudo dpkg -i packages-microsoft-prod.deb
3(Reading database ... 269274 files and directories currently installed.)
4Preparing to unpack packages-microsoft-prod.deb ...
5Unpacking packages-microsoft-prod (1.0-ubuntu18.04.2) over (1.0-ubuntu18.04.2) ...
6Setting up packages-microsoft-prod (1.0-ubuntu18.04.2) ...
7------------------------------------------------------------
8
9$ sudo add-apt-repository universe
10$ sudo apt-get update
11$ sudo apt-get install apt-transport-https
12$ sudo apt-get update
13$ sudo apt-get install aspnetcore-runtime-3.0
14$ sudo apt-get install dotnet-sdk-3.0
新特性 C# 8.0
本文例子均是基于C# 8.0,增加一些个人的理解。
1. readonly member
如果struct结构中的成员方法(属性)只用于 读,则增加readonly
关键字,以帮助编译器理解设计意图,利于优化。
1public struct Point
2{
3 public double X { get; set; }
4 public double Y { get; set; }
5 public readonly double Distance => Math.Sqrt(X * X + Y * Y);
6 public readonly override string ToString() =>
7 $"({X}, {Y}) is {Distance} from the origin";
8}
:csharp:ToString
方法不会改变Point结构中的任何状态,是一个只读方法,故可以用readonly
进行修饰。但是 :csharp:ToString
方法中引用了 :csharp:Dispose
方法。如果 :csharp:Dispose
不用readonly
修饰,编译器会给出警告warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'
。因此要在Distance方法前也加上readonly
关键词。
1public readonly void Translate(int xOffset, int yOffset)
2{
3 X += xOffset;
4 Y += yOffset;
5}
如果在Point结构中增加上述方法,编译时会报错error CS1604: Cannot assign to 'X' because it is read-only
。由于方法用readonly
关键字进行了修饰,故编译器会检查方法内是否改变了结构状态。
2. Default interface methods
如果我们基于一个已经存在的interface实现了若干类。当我们给interface增加一个新方法时,就不得不修改每一个已经存在的类,实现这个新接口方法。有了default interface methods之后,给interface添加新接口方法的同时,可以提供一个默认实现。所有已经存在的类对应的接口方法都使用该默认实现,而无需做代码调整。举个例子:
1public interface IOld
2{
3 decimal GetZero();
4}
5
6public class OldImpl : IOld
7{
8 public decimal GetZero()
9 {
10 return 0;
11 }
12}
13
14var old = new OldImpl();
15Console.WriteLine($"Result: {old.GetZero()}");
现在需要给IOld接口增加一个GetOne接口方法,但是并不想修改OldImpl类。这时候就需要用到default interface methods这个功能。
1public interface IOld
2{
3 decimal GetZero();
4 public decimal GetOne() => DefaultGetOne(this);
5 protected static decimal DefaultGetOne(IOld old)
6 {
7 return 1;
8 }
9}
10
11var old = new OldImpl() as IOld;
12Console.WriteLine($"Result 0: {old.GetZero()}, Result 1: {old.GetOne()}");
需要注意几点:
- IOld新增接口 :csharp:
GetOne
方法,而OldImpl类无需修改 - 在IOld实现了一个静态函数 :csharp:
DefaultGetOne
是为了方便实现类调用默认的实现。接口中的default interface method也是调用该静态方法 - 因为OldImpl里并没有 :csharp:
GetOne
方法,所以需要把 :csharp:new OldImpl()
强制转换为接口IOld使用 :csharp:GetOne
方法
新的类OldImpl2可以实现自己的 :csharp:GetOne
逻辑
1public class OldImpl2 : IOld
2{
3 public decimal GetZero()
4 {
5 return 0;
6 }
7
8 public decimal GetOne()
9 {
10 return this.GetZero() == 0 ? IOld.DefaultGetOne(this) : -1;
11 }
12}
3. More patterns in more places
简单说,这是更牛逼的语法糖,帮我们把代码写得更简洁更直观。 这个新功能主要包含两部分: 1. 更丰富的Pattern matching;2. 和recursive patterns,pattern包含pattern。
3.1 switch expressions
官网上有一个很精彩的例子讲解switch expressions。 这里给一个简单的例子:
1switch (a)
2{
3 case double:
4 return "double";
5 case 1:
6 return "one";
7 case 2:
8 return "two";
9 default:
10 throw new ArgumentException("bad value");
11}
用switch expression调整一下。注意这个表达式最终作为一个方法的返回值存在(方法就是那个 :csharp:ConvertDecimalToLiteral
)
1public static string ConvertDecimalToLiteral(decimal a)
2 => a switch
3 {
4 double d => "double",
5 1 => "one",
6 2 => "two",
7 _ => throw new ArgumentException("bad value")
8 }
3.2 Property patterns
在switch expressions中使用类实例的属性值。很简单:
1class Foo
2{
3 public int A;
4}
5
6public static string ConvertObjectPropertyToLiteral(Foo foo)
7 => foo switch
8 {
9 {A: 1} => "one",
10 {A: 2} => "two",
11 _ => "other"
12 }
3.3 Tuple patterns
也很简单,给个例子一目了然:
1public static string ConvertIntPairToLiteral(int i, int j)
2 => (i, j) switch
3 {
4 (1, 1) => "dual one",
5 (2, 2) => "dual two",
6 (3, _) => "triple something",
7 (_, _) => "the other"
8 };
3.4 Positional patterns
Deconstruct是C# 7.0引入的新功能
假设类定义中包含 :csharp:Deconstruct
方法,
1public class Foo
2{
3 public void Deconstruct(out int x, out int y) =>
4 (x, y) = (1, 2);
5}
则可以在switch expressions中这样使用positional pattern:
1public static string ConvertDeconstructToLiteral(Foo foo)
2 => foo switch
3 {
4 (0, 0) => "zero",
5 var (x, y) when x > 1 && y > 1 => "best",
6 var (_, _) => "the other",
7 _ => throw new ArgumentException("bad value")
8 };
4. using declarations
过去使用 :csharp:using
要用花括号
1static void Foo()
2{
3 using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
4 {
5 file.WriteLine("Hello World");
6 }
7}
8.0之后,无需再用花括号
1static void Foo()
2{
3 using var file = new System.IO.StreamWriter("WriteLines2.txt");
4 file.WriteLine("Hello World");
5 // file is disposed here
6}
5. Static local functions
C# 7.0时,引入了local function用来更清晰的表达开发者意图,即这个函数仅供定义该函数的方法内使用,生人勿用!C# 8.0进一步升级,如果local function没用使用定义范围内的任何局部变量,则可以声明为static。
1static int Foo()
2{
3 int y;
4 LocalFunction();
5 int z = Add(0, 1);
6 return y;
7
8 void LocalFunction() => y = 0; // local functions
9 static int Add(int left, int right) => left + right; // static local functions
10}
6. Indices and ranges
在Python中有negative indexing和slicing的概念,如下:
1a = [1, 2, 3]
2b = a[-1]
3c = a[1:2]
现在C# 8.0也支持了
1var a = new decimal[] {1M, 2M, 3M};
2var b = a[^1];
3var c = a[1..2];
7. Asynchronous streams
假设有一个数据表提供了很多有规律的数字,且有一个支持分页的API可以遍历这个数据表中的数字。如何将表中的数字全部遍历出来?
1public async Task<IEnumerable<int>> GetNumbers(string apiUrl)
2{
3 var allResults = new List<int>();
4 var nextUrl = apiUrl;
5 while (nextUrl != null)
6 {
7 var page = await _client.GetAsync(nextUrl).Content.ReadAsAsync<int>();
8 allResults.AddRange(page.Numbers);
9 if (page.NextPage < page.TotalPages)
10 {
11 nextUrl = apiUrl + "?page=" + page.NextPage;
12 }
13 else
14 {
15 nextUrl = null;
16 }
17 }
18 return allResults;
19}
这段代码可以工作。如果把这个方法看做一个producer的话,那在所有页获取完毕之前,consumer端是无法开始消耗的。也就是说,尽管我们用了async/await试图并发处理,但实质上producer和consumer顺序完成,并没有任何速度提升。而有了async streams新特征之后,我们可以这样做:
1public async System.Collections.Generic.IAsyncEnumerable<int> GetNumbers(string apiUrl)
2{
3 var nextUrl = apiUrl;
4 while (true)
5 {
6 var page = await _client.GetAsync(nextUrl)
7 .Content.ReadAsAsync<int>();
8 foreach (var number in page.Numbers)
9 {
10 yield return number;
11 }
12
13 if (page.NextPage < page.TotalPages)
14 {
15 nextUrl = apiUrl + "?page=" + page.NextPage;
16 }
17 else
18 {
19 yield break;
20 }
21 }
22}
23
24await foreach (var number in GetNumbers(apiUrl))
25{
26 ProcessAync(number);
27}
每一次API的等待过程,都可以同时异步处理上一次API的结果(ProcessAsync)。
8. 其他
除了以上几种新特性外,C# 8.0还支持:
- Disposable ref structs:ref struct因为不能boxing,故也不能实现接口,也就不能实现IDisposable接口,也就不能用using语法。然而有了Disposable ref structs特性,可以在ref struct中直接声明Dispose方法,从而和using配合。
- Nullable reference types:通过使用
?
给编译器传达更明确的意图,某个引用变量是否可以为null。 - Null-coalescing assignment:
??=
等价于a == null ? 1 : a
。 - Unmanaged constructed types:如果结构体中所有成员都是unmanaged type,则这个结构体也为unmanaged type。
- stackalloc in nested expressions:可以在表达式中使用关键字 :csharp:
stackalloc
。