乐趣区

关于c#:C-11-的新特性和改进前瞻

.NET 7 的开发还剩下一个多月就要进入 RC,C# 11 的新个性和改良也行将敲定。在这个工夫点上,不少新个性都曾经实现结束并合并入主分支。C# 11 蕴含的新个性和改良十分多,类型零碎相比之前也有了很大的加强,在确保动态类型平安的同时大幅晋升了语言表达力。那么本文就依照方向从 5 个大类来进行介绍,一起来提前看看 C# 11 的新个性和改良都有什么。

类型零碎的改良

▌形象和虚静态方法

C# 11 开始将 abstract 和 virtual 引入到静态方法中,容许开发者在接口中编写形象和虚静态方法。
接口与抽象类不同,接口用来形象行为,通过不同类型实现接口来实现多态;而抽象类则领有本人的状态,通过各子类型继承父类型来实现多态。这是两种不同的范式。
在 C# 11 中,虚静态方法的概念被引入,在接口中能够编写形象和虚静态方法了。

interface IFoo{    // 形象静态方法
    abstract static int Foo1();    // 虚静态方法
    virtual static int Foo2()
    {return 42;}
}struct Bar : IFoo
{    // 隐式实现接口办法
    public static int Foo1()
    {return 7;}
}

Bar.Foo1(); // ok

因为运算符也属于静态方法,因而从 C# 11 开始,也能够用接口来对运算符进行形象了。

interface ICanAdd<T> where T : ICanAdd<T>
{abstract static T operator +(T left, T right);
}

这样咱们就能够给本人的类型实现该接口了,例如实现一个二维的点 Point:

record struct Point(int X, int Y) : ICanAdd<Point>
{    // 隐式实现接口办法
    public static Point operator +(Point left, Point right)
    {return new Point(left.X + right.X, left.Y + right.Y);
    }
}

而后咱们就能够对两个 Point 进行相加了:

var p1 = new Point(1, 2);var p2 = new Point(2, 3);
Console.WriteLine(p1 + p2); // Point {X = 3, Y = 5}

除了隐式实现接口之外,咱们也能够显式实现接口:

record struct Point(int X, int Y) : ICanAdd<Point>
{    // 显式实现接口办法
    static Point ICanAdd<Point>.operator +(Point left, Point right)
    {return new Point(left.X + right.X, left.Y + right.Y);
    }
}

不过用显示实现接口的形式的话,+ 运算符没有通过 public 公开裸露到类型 Point 上,因而咱们须要通过接口来调用 + 运算符,这能够利用泛型束缚来做到:

var p1 = new Point(1, 2);var p2 = new Point(2, 3);
Console.WriteLine(Add(p1, p2)); // Point {X = 3, Y = 5}T Add<T>(T left, T right) where T : ICanAdd<T>{return left + right;}

对于不是运算符的状况,则能够利用泛型参数来调用接口上的形象和静态方法:

void CallFoo1<T>() where T : IFoo{T.Foo1();
}

Bar.Foo1(); // errorCallFoo<Bar>(); // okstruct Bar : IFoo
{    // 显式实现接口办法
    static void IFoo.Foo1()
    {return 7;}
}

此外,接口能够基于另一个接口扩大,因而对于形象和虚静态方法而言,咱们能够利用这个个性在接口上实现多态。

CallFoo<Bar1>(); // 5 5CallFoo<Bar2>(); // 6 4CallFoo<Bar3>(); // 3 7CallFooFromIA<Bar4>(); // 1CallFooFromIB<Bar4>(); // 2void CallFoo<T>() where T : IC{CallFooFromIA<T>();
    CallFooFromIB<T>();}void CallFooFromIA<T>() where T : IA{Console.WriteLine(T.Foo());
}void CallFooFromIB<T>() where T : IB{Console.WriteLine(T.Foo());
}interface IA{virtual static int Foo()
    {return 1;}
}interface IB{virtual static int Foo()
    {return 2;}
}interface IC : IA, IB{static int IA.Foo()
    {return 3;}    static int IB.Foo()
    {return 4;}
}struct Bar1 : IC
{public static int Foo()
    {return 5;}
}struct Bar2 : IC
{static int IA.Foo()
    {return 6;}
}struct Bar3 : IC
{static int IB.Foo()
    {return 7;}
}struct Bar4 : IA, IB {}

同时,.NET 7 也利用形象和虚静态方法,对根底库中的数值类型进行了改良。在 System.Numerics 中新增了大量的用于数学的泛型接口,容许用户利用泛型编写通用的数学计算代码:

using System.Numerics;V Eval<T, U, V>(T a, U b, V c) 
    where T : IAdditionOperators<T, U, U>    where U : IMultiplyOperators<U, V, V>{return (a + b) * c;
}

Console.WriteLine(Eval(3, 4, 5)); // 35Console.WriteLine(Eval(3.5f, 4.5f, 5.5f)); // 44

▌泛型 attribute

C# 11 正式容许用户编写和应用泛型 attribute,因而咱们能够不再须要应用 Type 来在 attribute 中存储类型信息,这不仅反对了类型推导,还容许用户通过泛型束缚在编译时就能对类型进行限度。

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]class FooAttribute<T> : Attribute where T : INumber<T>
{public T Value { get;}    public FooAttribute(T v)
{Value = v;}
}

[Foo<int>(3)] // ok[Foo<float>(4.5f)] // ok[Foo<string>("test")] // errorvoid MyFancyMethod() {}

▌ref 字段和 scoped ref

C# 11 开始,开发者能够在 ref struct 中编写 ref 字段,这容许咱们将其余对象的援用存储在一个 ref struct 中:

int x = 1;
Foo foo = new(ref x);
foo.X = 2;
Console.WriteLine(x); // 2ref struct Foo
{    public ref int X;    
    public Foo(ref int x)
    {X = ref x;}
}

能够看到,下面的代码中将 x 的援用保留在了 Foo 中,因而对 foo.X 的批改会反映到 x 上。

如果用户没有对 Foo.X 进行初始化,则默认是空援用,能够利用 Unsafe.IsNullRef 来判断一个 ref 是否为空:

ref struct Foo
{public ref int X;    public bool IsNull => Unsafe.IsNullRef(ref X);    
    public Foo(ref int x)
    {X = ref x;}
}

这里能够发现一个问题,那就是 ref field 的存在,可能会使得一个 ref 指向的对象的生命周期被扩大而导致谬误,例如:

Foo MyFancyMethod(){    int x = 1;
    Foo foo = new(ref x);    return foo; // error}ref struct Foo
{public Foo(ref int x) {}}

上述代码编译时会报错,因为 foo 援用了局部变量 x,而局部变量 x 在函数返回后生命周期就完结了,然而返回 foo 的操作使得 foo 的生命周期比 x 的生命周期更长,这会导致有效援用的问题,因而编译器检测到了这一点,不容许代码通过编译。

然而上述代码中,尽管 foo 的确援用了 x,然而 foo 对象自身并没有长期持有 x 的援用,因为在构造函数返回后就不再持有对 x 的援用了,因而这里按理来说不应该报错。于是 C# 11 引入了 scoped 的概念,容许开发者显式标注 ref 的生命周期,标注了 scoped 的 ref 示意这个援用的生命周期不会超过以后函数的生命周期:

Foo MyFancyMethod(){    int x = 1;
    Foo foo = new(ref x);    return foo; // ok}ref struct Foo
{public Foo(scoped ref int x) {}}

这样一来,编译器就晓得 Foo 的构造函数不会使得 Foo 在构造函数返回后依然持有 x 的援用,因而上述代码就能平安通过编译了。如果咱们试图让一个 scoped ref 逃逸出以后函数的话,编译器就会报错:

ref struct Foo
{public ref int X;    public Foo(scoped ref int x)
    {X = ref x; // error}
}

如此一来,就实现了援用平安。

利用 ref 字段,咱们能够很不便地实现各种零开销设施,例如提供一个多种办法拜访色彩数据的 ColorView:

using System.Diagnostics.CodeAnalysis;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;var color = new Color {R = 1, G = 2, B = 3, A = 4};
color.RawOfU32[0] = 114514;
color.RawOfU16[1] = 19198;
color.RawOfU8[2] = 10;
Console.WriteLine(color.A); // 74[StructLayout(LayoutKind.Explicit)]struct Color
{[FieldOffset(0)] public byte R;
    [FieldOffset(1)] public byte G;
    [FieldOffset(2)] public byte B;
    [FieldOffset(3)] public byte A;

    [FieldOffset(0)] public uint Rgba;    public ColorView<byte> RawOfU8 => new(ref this);    public ColorView<ushort> RawOfU16 => new(ref this);    public ColorView<uint> RawOfU32 => new(ref this);
}ref struct ColorView<T> where T : unmanaged{private ref Color color;    public ColorView(ref Color color)
    {this.color = ref color;}

    [DoesNotReturn] private static ref T Throw() => throw new IndexOutOfRangeException();    public ref T this[uint index]
    {[MethodImpl(MethodImplOptions.AggressiveInlining)]        get
        {            unsafe
            {return ref (sizeof(T) * index >= sizeof(Color) ?                    ref Throw() :                    ref Unsafe.Add(ref Unsafe.AsRef<T>(Unsafe.AsPointer(ref color)), (int)index));
            }
        }
    }
}

在字段中,ref 还能够配合 readonly 一起应用,用来示意不可批改的 ref,例如:

  • ref int:一个 int 的援用
  • readonly ref int:一个 int 的只读援用
  • ref readonly int:一个只读 int 的援用
  • readonly ref readonly int:一个只读 int 的只读援用

这将容许咱们确保援用的平安,使得援用到只读内容的援用不会被意外更改。

当然,C# 11 中的 ref 字段和 scoped 反对只是其齐全状态的一部分,更多的相干内容仍在设计和探讨,并在后续版本中推出。

▌文件部分类型

C# 11 引入了新的文件部分类型可拜访性符号 file,利用该可拜访性符号,容许咱们编写只能在以后文件中应用的类型:

// A.csfile class Foo{// ...}

file struct Bar
{// ...}

如此一来,如果咱们在与 Foo 和 Bar 的不同文件中应用这两个类型的话,编译器就会报错:

// A.csvar foo = new Foo(); // okvar bar = new Bar(); // ok// B.csvar foo = new Foo(); // errorvar bar = new Bar(); // error

这个个性将可拜访性的粒度准确到了文件,对于代码生成器等一些要放在同一个我的项目中,然而又不想被其他人接触到的代码而言将会特地有用。

▌required 成员

C# 11 新增了 required 成员,标记有 required 的成员将会被要求应用时必须要进行初始化,例如:

var foo = new Foo(); // errorvar foo = new Foo { X = 1}; // okstruct Foo
{public required int X;}

开发者还能够利用 SetsRequiredMembers 这个 attribute 来对办法进行标注,示意这个办法会初始化 required 成员,因而用户在应用时能够不须要再进行初始化:

using System.Diagnostics.CodeAnalysis;var p = new Point(); // errorvar p = new Point { X = 1, Y = 2}; // okvar p = new Point(1, 2); // okstruct Point
{    public required int X;    public required int Y;

    [SetsRequiredMembers]    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

利用 required 成员,咱们能够要求其余开发者在应用咱们编写的类型时必须初始化一些成员,使其可能正确地应用咱们编写的类型,而不会遗记初始化一些成员。

运算改良

▌checked 运算符

C# 自古以来就有 checked 和 unchecked 概念,别离示意检查和不查看算术溢出:

byte x = 100;byte y = 200;unchecked{byte z = (byte)(x + y); // ok}

checked
{byte z = (byte)(x + y); // error}

在 C# 11 中,引入了 checked 运算符概念,容许用户别离实现用于 checked 和 unchecked 的运算符:

struct Foo
{public static Foo operator +(Foo left, Foo right) {...}    public static Foo operator checked +(Foo left, Foo right) {...}
}var foo1 = new Foo(...);var foo2 = new Foo(...);var foo3 = unchecked(foo1 + foo2); // 调用 operator +var foo4 = checked(foo1 + foo2); // 调用 operator checked +

对于自定义运算符而言,实现 checked 的版本是可选的,如果没有实现 checked 的版本,则都会调用 unchecked 的版本。

▌无符号右移运算符

C# 11 新增了 >>> 示意无符号的右移运算符。此前 C# 的右移运算符 >> 默认是有符号的右移,即:右移操作保留符号位,因而对于 int 而言,将会有如下后果:

1 >> 1 = -11 >> 2 = -11 >> 3 = -11 >> 4 = -1// ...

而新的 >>> 则是无符号右移运算符,应用后将会有如下后果:

1 >>> 1 = 21474836471 >>> 2 = 10737418231 >>> 3 = 5368709111 >>> 4 = 268435455// ...

这省去了咱们须要无符号右移时,须要先将数值转换为无符号数值后进行计算,再转换回来的麻烦,也能防止不少因而导致的意外谬误。

▌移位运算符放开类型限度

C# 11 开始,移位运算符的右操作数不再要求必须是 int,类型限度和其余运算符一样被放开了,因而联合下面提到的形象和虚静态方法,容许咱们申明泛型的移位运算符了:

interface ICanShift<T> where T : ICanShift<T>
{abstract static T operator <<(T left, T right);    abstract static T operator >>(T left, T right);
}

当然,上述的场景是该限度被放开的次要目标。然而,置信不少读者读到这里心中都可能会萌发一个邪恶的想法,没错,就是 cin 和 cout!尽管这种做法在 C# 中是不举荐的,但该限度被放开后,开发者的确能编写相似的代码了:

using static OutStream;using static InStream;int x = 0;
_ = cin >> To(ref x); // 有 _ = 是因为 C# 不容许运算式不通过赋值而独自成为一条语句_ = cout << "hello" << "" <<"world!";public class OutStream{public static OutStream cout = new();    public static OutStream operator <<(OutStream left, string right)
    {Console.WriteLine(right);        return left;
    }
}public class InStream{    public ref struct Ref<T>
    {public ref T Value;        public Ref(ref T v) => Value = ref v;
    }    public static Ref<T> To<T>(ref T v) => new (ref v);    public static InStream cin = new();    public static InStream operator >>(InStream left, Ref<int> right)
    {var str = Console.Read(...);
        right.Value = int.Parse(str);
    }
}

▌IntPtr、UIntPtr 反对数值运算

C# 11 中,IntPtr 和 UIntPtr 都反对数值运算了,这极大的不便了咱们对指针进行操作:

UIntPtr addr = 0x80000048;
IntPtr offset = 0x00000016;
UIntPtr newAddr = addr + (UIntPtr)offset; // 0x8000005E

当然,如同 Int32 和 int、Int64 和 long 的关系一样,C# 中同样存在 IntPtr 和 UIntPtr 的等价简写,别离为 nint 和 nuint,n 示意 native,用来示意这个数值的位数和以后运行环境的内存地址位数雷同:

nuint addr = 0x80000048;nint offset = 0x00000016;nuint newAddr = addr + (nuint)offset; // 0x8000005E

模式匹配改良

▌列表模式匹配

C# 11 中新增了列表模式,容许咱们对列表进行匹配。在列表模式中,咱们能够利用 [] 来包含咱们的模式,用 _ 代指一个元素,用 .. 代表 0 个或多个元素。在 .. 后能够申明一个变量,用来创立匹配的子列表,其中蕴含 .. 所匹配的元素。

例如:

var array = new int[] { 1, 2, 3, 4, 5};if (array is [1, 2, 3, 4, 5]) Console.WriteLine(1); // 1if (array is [1, 2, 3, ..]) Console.WriteLine(2); // 2if (array is [1, _, 3, _, 5]) Console.WriteLine(3); // 3if (array is [.., _, 5]) Console.WriteLine(4); // 4if (array is [1, 2, 3, .. var remaining])
{Console.WriteLine(remaining[0]); // 4
    Console.WriteLine(remaining.Length); // 2}

当然,和其余的模式一样,列表模式同样是反对递归的,因而咱们能够将列表模式与其余模式组合起来应用:

var array = new int[] { 1, 2, 3, 4, 5};if (array is [1, 2, 3, 4, 5]) Console.WriteLine(1); // 1if (array is [1, 2, 3, ..]) Console.WriteLine(2); // 2if (array is [1, _, 3, _, 5]) Console.WriteLine(3); // 3if (array is [.., _, 5]) Console.WriteLine(4); // 4if (array is [1, 2, 3, .. var remaining])
{Console.WriteLine(remaining[0]); // 4
    Console.WriteLine(remaining.Length); // 2}

▌对 Span<char> 的模式匹配

在 C# 中,Span<char> 和 ReadOnlySpan<char> 都能够看作是字符串的切片,因而 C# 11 也为这两个类型增加了字符串模式匹配的反对。例如:

int Foo(ReadOnlySpan<char> span){if (span is "abcdefg") return 1;    return 2;
}

Foo("abcdefg".AsSpan()); // 1Foo("test".AsSpan()); // 2

如此一来,应用 Span<char> 或者 ReadOnlySpan<char> 的场景也可能十分不便地进行字符串匹配了,而不须要利用 SequenceEquals 或者编写循环进行解决。

字符串解决改良

▌原始字符串

C# 中自初便有 @ 用来示意不须要本义的字符串,然而用户还是须要将 ” 写成 “” 能力在字符串中蕴含引号。C# 11 引入了原始字符串个性,容许用户利用原始字符串在代码中插入大量的无需转移的文本,不便开发者在代码中以字符串的形式塞入代码文本等。

原始字符串须要被至多三个 ” 包裹,例如 “”” 和 “”””” 等等,前后的引号数量要相等。另外,原始字符串的缩进由前面引号的地位来确定,例如:

var str = """
    hello
    world
    """;

此时 str 是:

hello
world

而如果是上面这样:

var str = """
    hello
    world
""";

str 则会成为:

    hello
    world

这个个性十分有用,例如咱们能够十分不便地在代码中插入 JSON 代码了:

var json = """{"a": 1,"b": {"c":"hello","d":"world"},"c": [1, 2, 3, 4, 5]
    }
    """;
Console.WriteLine(json);/*
{
    "a": 1,
    "b": {
        "c": "hello",
        "d": "world"
    },
    "c": [1, 2, 3, 4, 5]
}
*/

▌UTF-8 字符串

C# 11 引入了 UTF-8 字符串,咱们能够用 u8 后缀来创立一个 ReadOnlySpan<byte>,其中蕴含一个 UTF-8 字符串:

var str1 = "hello world"u8; // ReadOnlySpan<byte>var str2 = "hello world"u8.ToArray(); // byte[]

UTF-8 对于 Web 场景而言十分有用,因为在 HTTP 协定中,默认编码就是 UTF-8,而 .NET 则默认是 UTF-16 编码,因而在解决 HTTP 协定时,如果没有 UTF-8 字符串,则会导致大量的 UTF-8 和 UTF-16 字符串的互相转换,从而影响性能。

有了 UTF-8 字符串后,咱们就能十分不便的创立 UTF-8 字面量来应用了,不再须要手动调配一个 byte[] 而后在外面一个一个硬编码咱们须要的字符。

▌字符串插值容许换行

C# 11 开始,字符串的插值局部容许换行,因而如下代码变得可能:

var str = $"hello, the leader is {group
                                    .GetLeader()
                                    .GetName()}.";

这样一来,当插值的局部代码很长时,咱们就能不便的对代码进行格式化,而不须要将所有代码挤在一行。

其余改良

▌struct 主动初始化

C# 11 开始,struct 不再强制构造函数必须要初始化所有的字段,对于没有初始化的字段,编译器会主动做零初始化:

struct Point
{public int X;    public int Y;    public Point(int x)
{X = x;        // Y 主动初始化为 0}
}

▌反对对其余参数名进行 nameof

C# 11 容许了开发者在参数中对其余参数名进行 nameof,例如在应用 CallerArgumentExpression 这一 attribute 时,此前咱们须要间接硬编码相应参数名的字符串,而当初只须要应用 nameof 即可:

void Assert(bool condition, [CallerArgumentExpression(nameof(condition))] string expression = "")
{// ...}

这将容许咱们在进行代码重构时,批改参数名 condition 时主动批改 nameof 外面的内容,不便的同时缩小出错。

▌主动缓存静态方法的委托

C# 11 开始,从静态方法创立的委托将会被主动缓存,例如:

void Foo(){Call(Console.WriteLine);
}void Call(Action action){action();
}

此前,每执行一次 Foo,就会从 Console.WriteLine 这一静态方法创立一个新的委托,因而如果大量执行 Foo,则会导致大量的委托被反复创立,导致大量的内存被调配,效率极其低下。在 C# 11 开始,将会主动缓存静态方法的委托,因而无论 Foo 被执行多少次,Console.WriteLine 的委托只会被创立一次,节俭了内存的同时大幅晋升了性能。

总结

从 C# 8 开始,C# 团队就在不断完善语言的类型零碎,在确保动态类型平安的同时大幅晋升语言表达力,从而让类型零碎成为编写程序的得力助手,而不是碍手碍脚的限度。

本次更新还欠缺了数值运算相干的内容,使得开发者利用 C# 编写数值计算方法时更加得心应手。

另外,模式匹配的摸索旅程也终于靠近序幕,引入列表模式之后,剩下的就只有字典模式和流动模式了,模式匹配是一个十分弱小的工具,容许咱们像对字符串应用正则表达式那样十分不便地对数据进行匹配。

总的来说 C# 11 的新个性和改良内容十分多,每一项内容都对 C# 的应用体验有着不小的晋升。在将来的 C# 中还打算着角色和扩大等更加令人激动的新个性,让咱们刮目相待。

长按辨认二维码关注微软中国 MSDN                         

点击理解 C# 新增性能~

退出移动版