校园网站建设成本,wordpress tag超链接,网上商城app开发,wordpress维护服务前言.NET 7 的开发还剩下一个多月就要进入 RC#xff0c;C# 11 的新特性和改进也即将敲定。在这个时间点上#xff0c;不少新特性都已经实现完毕并合并入主分支C# 11 包含的新特性和改进非常多#xff0c;类型系统相比之前也有了很大的增强#xff0c;在确保静态类型安全的… 前言.NET 7 的开发还剩下一个多月就要进入 RCC# 11 的新特性和改进也即将敲定。在这个时间点上不少新特性都已经实现完毕并合并入主分支C# 11 包含的新特性和改进非常多类型系统相比之前也有了很大的增强在确保静态类型安全的同时大幅提升了语言表达力。那么本文就按照方向从 5 个大类来进行介绍一起来提前看看 C# 11 的新特性和改进都有什么。1. 类型系统的改进抽象和虚静态方法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 ICanAddT where T : ICanAddT
{ abstract static T operator (T left, T right);
}这样我们就可以给自己的类型实现该接口了例如实现一个二维的点 Pointrecord struct Point(int X, int Y) : ICanAddPoint
{ // 隐式实现接口方法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) : ICanAddPoint
{ // 显式实现接口方法static Point ICanAddPoint.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 AddT(T left, T right) where T : ICanAddT{ return left right;
}对于不是运算符的情况则可以利用泛型参数来调用接口上的抽象和静态方法void CallFoo1T() where T : IFoo{T.Foo1();
}Bar.Foo1(); // errorCallFooBar(); // okstruct Bar : IFoo
{ // 显式实现接口方法static void IFoo.Foo1(){ return 7;}
}此外接口可以基于另一个接口扩展因此对于抽象和虚静态方法而言我们可以利用这个特性在接口上实现多态。CallFooBar1(); // 5 5CallFooBar2(); // 6 4CallFooBar3(); // 3 7CallFooFromIABar4(); // 1CallFooFromIBBar4(); // 2void CallFooT() where T : IC{CallFooFromIAT();CallFooFromIBT();
}void CallFooFromIAT() where T : IA{Console.WriteLine(T.Foo());
}void CallFooFromIBT() 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 EvalT, U, V(T a, U b, V c) where T : IAdditionOperatorsT, U, U where U : IMultiplyOperatorsU, V, V{ return (a b) * c;
}Console.WriteLine(Eval(3, 4, 5)); // 35Console.WriteLine(Eval(3.5f, 4.5f, 5.5f)); // 44泛型 attributeC# 11 正式允许用户编写和使用泛型 attribute因此我们可以不再需要使用 Type 来在 attribute 中存储类型信息这不仅支持了类型推导还允许用户通过泛型约束在编译时就能对类型进行限制。[AttributeUsage(AttributeTargets.Method, AllowMultiple true)]class FooAttributeT : Attribute where T : INumberT
{ public T Value { get; } public FooAttribute(T v){Value v;}
}[Fooint(3)] // ok[Foofloat(4.5f)] // ok[Foostring(test)] // errorvoid MyFancyMethod() { }ref 字段和 scoped refC# 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 字段我们可以很方便地实现各种零开销设施例如提供一个多种方法访问颜色数据的 ColorViewusing 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 ColorViewbyte RawOfU8 new(ref this); public ColorViewushort RawOfU16 new(ref this); public ColorViewuint RawOfU32 new(ref this);
}ref struct ColorViewT 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.AsRefT(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 成员我们可以要求其他开发者在使用我们编写的类型时必须初始化一些成员使其能够正确地使用我们编写的类型而不会忘记初始化一些成员。2. 运算改进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 ICanShiftT where T : ICanShiftT
{ 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 RefT{ public ref T Value; public Ref(ref T v) Value ref v;} public static RefT ToT(ref T v) new (ref v); public static InStream cin new(); public static InStream operator (InStream left, Refint 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 和 nuintn 表示 native用来表示这个数值的位数和当前运行环境的内存地址位数相同nuint addr 0x80000048;nint offset 0x00000016;nuint newAddr addr (nuint)offset; // 0x8000005E3. 模式匹配改进列表模式匹配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]); // 4Console.WriteLine(remaining.Length); // 2}当然和其他的模式一样列表模式同样是支持递归的因此我们可以将列表模式与其他模式组合起来使用var array new string[] { hello, ,, world, ~ };if (array is [hello, _, { Length: 5 }, { Length: 1 } elem, ..])
{Console.WriteLine(elem); // ~}除了在 if 中使用模式匹配以外在 switch 中也同样能使用var array new string[] { hello, ,, world, ! };switch (array)
{ case [hello, _, { Length: 5 }, { Length: 1 } elem, ..]: // ...break; default: // ...break;
}var value array switch{[hello, _, { Length: 5 }, { Length: 1 } elem, ..] 1,_ 2};Console.WriteLine(value); // 1对 Spanchar 的模式匹配在 C# 中Spanchar 和 ReadOnlySpanchar 都可以看作是字符串的切片因此 C# 11 也为这两个类型添加了字符串模式匹配的支持。例如int Foo(ReadOnlySpanchar span){ if (span is abcdefg) return 1; return 2;
}Foo(abcdefg.AsSpan()); // 1Foo(test.AsSpan()); // 2如此一来使用 Spanchar 或者 ReadOnlySpanchar 的场景也能够非常方便地进行字符串匹配了而不需要利用 SequenceEquals 或者编写循环进行处理。4. 字符串处理改进原始字符串C# 中自初便有 用来表示不需要转义的字符串但是用户还是需要将 写成 才能在字符串中包含引号。C# 11 引入了原始字符串特性允许用户利用原始字符串在代码中插入大量的无需转移的文本方便开发者在代码中以字符串的方式塞入代码文本等。原始字符串需要被至少三个 包裹例如 和 等等前后的引号数量要相等。另外原始字符串的缩进由后面引号的位置来确定例如var str helloworld;此时 str 是hello
world而如果是下面这样var str helloworld
;str 则会成为helloworld这个特性非常有用例如我们可以非常方便地在代码中插入 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 后缀来创建一个 ReadOnlySpanbyte其中包含一个 UTF-8 字符串var str1 hello worldu8; // ReadOnlySpanbytevar str2 hello worldu8.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()}.;这样一来当插值的部分代码很长时我们就能方便的对代码进行格式化而不需要将所有代码挤在一行。5. 其他改进struct 自动初始化C# 11 开始struct 不再强制构造函数必须要初始化所有的字段对于没有初始化的字段编译器会自动做零初始化struct Point
{ public int X; public int Y; public Point(int x){X x; // Y 自动初始化为 0}
}支持对其他参数名进行 nameofC# 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# 中还计划着角色和扩展等更加令人激动的新特性让我们拭目以待。