C# 8.0

Posted on | 3426 words | ~7 mins
C#

九月份,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%。

tiobe_2019_9

然而对一个人、一件事、一个物的真爱又怎会随潮流和时间而变?平时依旧会关注.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