2

一探即将到来的 C# 10

 3 years ago
source link: https://www.cnblogs.com/hez2010/p/csharp-10-in-advance.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言#

本来因为懒不想写这篇文章,但是不少人表示有兴趣,于是最后决定还是写一下。

.NET 6 最近几个预览版一直都在开发体验(如 hot reload、linker 等)、平台支持(如 Android、iOS 等)、工具链(如 crossgen2、PGO 工具和 wasm 的 AOT 等)、JIT(如 LSRA、Jump threading、PGO 和 guarded devirtualization 以及使 struct 保持在寄存器上等)、GC(如 Regions 等)以及 BCL(如 TimeOnly、DateOnly 以及 Json DOM 等)方面做改进,然而却一直没有公布 C# 10 的任何内容,即使在 Build 2021 大会上也没有提及这方面内容。然而实际上不少特性的实现已经接近尾声了,那么让我们提前来看看 C# 10 可以为我们带来什么东西。

当然,不是所有下面列出的特性都一定会进入 C# 10,也可能会和本文有所出入,我在每一个特性后面加了一个百分比表示最终实装的可能性,仅供参考。

Backing Fields(60%)#

相信不少人在编写属性的时候,因为自动属性不能满足自己的需求于是不得不改回手动实现属性,这个时候总是会想“如果能不用手动写字段的定义就好了”,现在这个梦想成真了:

private int myInt;
public int MyInt { get => myInt; set => myInt = value; }

C# 10 中新增了一个 field,当使用它时会自动为属性创建字段定义,不需要再手动定义字段了,因此也叫做半自动属性。

public int MyInt { get => field; set => field = value; }

Record Structs(100%)#

Records 此前只支持 class,但是现在同样支持 struct 啦,于是你可以定义值类型的 record,避免不必要的堆内存分配:

record struct Point(int X, int Y);

with on Anonymous Objects(80%)#

此前 with 只能配合 records 使用,但是现在它被扩展到了匿名对象上,你可以通过 with 来创建匿名对象的副本并且修改它的值啦:

var foo = new { A = 1, B = "test", C = 4.4 };
var bar = foo with { A = 3 };
Console.WriteLine((bar.A, bar.B, bar.C)); // (3, test, 4.4)

Global Usings(80%)#

此前 using 语句的生效范围是单个文件的,如果你想使用一些 namespace,或者定义一系列的类型别名在整个项目内使用,那么你就需要这样:

using System.Linq;
using static System.Math;
using i32 = System.Int32;
using i64 = System.Int64;

然后在每个文件中重复一遍。但是现在不需要了,你可以定义全局的 using 了:

global using System.Linq;
global using static System.Math;
global using i32 = System.Int32;
global using i64 = System.Int64;

然后在整个项目中就都可以用了。

File Scoped Namespace(90%)#

C# 10 开始你将能够在文件顶部指定该文件的 namespace,而不需要写一个 namespace 然后把其他代码都嵌套在大括号里面,毕竟绝大多数情况下,我们在写代码时一个文件里确实只会写一个 namespace,这样可以减少一层嵌套也是很不错的:

namespace MyProject;

class MyClass
{
    // ...
}

如果采用这样的写法,每一个文件将只能声明一个 namespace。

Constant Interpolated String(100%)#

顾名思义,常量字符串插值:

const string a = "foo";
const string b = $"{a}_bar"; // foo_bar

常量字符串插值将在编译时完成。

Lambda Improvements(100%)#

C# 10 大幅度改进了 lambda,扩展了使用场景,并改进了一系列的推导,提出自然委托类型,还函数上升至 first-class。

支持 Attributes#

f = [Foo] (x) => x; // 给 lambda 设置
f = [return: Foo] (x) => x; // 给 lambda 返回值设置
f = ([Foo] x) => x; // 给 lambda 参数设置

支持显示指定返回值类型#

此前 C# 的 lambda 返回值类型靠推导,C# 10 开始允许在参数列表最前面显示指定 lambda 类型了:

f = int () => 4;

支持 ref 等修饰#

f = ref int (ref int x) => ref x; // 返回一个参数的引用

First-class Functions#

方法可以被隐式转换到 Delegate,使得函数上升至 first-class。

Delegate f = 1.GetHashCode; // Func<int>
object g = 2.ToString; // object(Func<string>)
var s = (int x) => x; // Func<int, int>

将函数作为变量,然后传给另一个函数的参数:

void Foo(Func<int> f)
{
    Console.WriteLine(f());
}

int Bar()
{
    return 5;
}

var baz = Bar;
Foo(baz);

Natural Delegate Types#

lambda 现在会自动创建自然委托类型。

可以用 var 来创建委托了:

var f = () => 1; // Func<int>
var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string>
var g = "test".GetHashCode; // Func<int>

调用 lambdas#

得益于上述改进,创建的类型明确的 lambda 可以直接调用了。

var zero = ((int x) => x)(0); // 0

Caller Expression Attribute(80%)#

现在,CallerArgumentExpression 这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:

void Foo(int value, [CallerArgumentExpression("value")] string? expression = null)
{
    Console.WriteLine(expression + " = " + value);
}

当你这样调用时:

Foo(4 + 5);

会输出 4 + 5 = 9。这对测试极其有用,因为你可以输出 assert 的原表达式了:

static void Assert(bool value, [CallerArgumentExpression("value")] string? expr = null)
{
    if (!value) throw new AssertFailureException(expr);
}

default 支持解构(100%)#

default 现在支持解构了,因此可以给 tuples 直接赋值。

(int a, int b, int c) = default; // (0, 0, 0)

List Patterns(100%)#

Pattern Matching 的最后一块版图:list patterns,终于补齐了。

void Foo(List<int> list)
{
    switch (list)
    {
        case [4]:
            Console.WriteLine("长度为 4");
            break;
        case { 1, 2, 3 }:
            Console.WriteLine("元素是 1, 2, 3");
            break;
        case { 1, 2, ..var x, 5 }:
            Console.WriteLine($"前两个元素是 1, 2,最后一个元素是 5,倒数第二个元素是 {x}");
            break;
        default:
            Console.WriteLine("其他");
    }
}

同样的,该 pattern 也是 recursive 的,因此你可以嵌套其他 patterns。

除了上述 switch statements 的用法,在 if 以及 switch expressions 等地方也同样可用,例如:

void Foo(List<int> list)
{
    var result = list switch
    {
        [4] => ...,
        { 1, 2, 3 } => ...,
        { 1, 2, ..var x, 5 } => ...,
        _ => ...
    };
}

Abstract Static Member in Interfaces(100%)#

C# 10 中,接口可以声明抽象静态成员了,.NET 的类型系统正式具备 virtual static dispatch 能力。

例如,你想定义一个可加而且有零的接口 IMonoid

interface IMonoid<T> where T : IMonoid<T>
{
    abstract static T Zero { get; }
    abstract static T operator+(T l, T r);
}

然后可以对其进行实现,例如这里的 MyInt

public class MyInt : IMonoid<MyInt>
{
    public MyInt(int val) { Value = val; }
    
    public static MyInt Zero { get; } = new MyInt(0);
    public static MyInt operator+(MyInt l, MyInt r) => new MyInt(l.Value + r.Value);
    
    public int Value { get; }
}

然后就能写出一个方法对 IMoniod<T> 进行求和了,这里为了方便写成扩展方法:

public static class IMonoidExtensions
{
    public static T Sum<T>(this IEnumerable<T> t) where T : IMonoid<T>
    {
        var result = T.Zero;
        foreach (var i in t) result += i;
        return result;
    }
}

最后调用:

List<MyInt> list = new() { new(1), new(2), new(3) };
Console.WriteLine(list.Sum().Value); // 6

这个特性同样也会对 .NET BCL 做出改进,会新增诸如 IAddable<T>INumeric<T> 的接口,并为适用的已有类型实现。

总结#

以上就是在 C# 10 的大部分新特性介绍了,虽然不保证最终效果和本文效果一致,但是也能看到一个大概的方向。

从 interface 的改进上我们可以看到一个好的预兆:.NET 终于开始动类型系统了。2008 年至今几乎没有变过的 CTS 显然逐渐不能适应语言发展的需要,而 .NET 团队也明确给出了信息表明要在 C# 11 前后对类型系统集中进行改进,现在只是一个开始,相信不久之后也将能看到 traits、union types、bottom types 和 HKT 等的实装。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK