Always keep a beginn|

石起起

园龄:1年10个月粉丝:1关注:0

2025-02-05 17:54阅读: 11评论: 0推荐: 0

C# 版本 11 新增特性

发布时间:2022 年 11 月

C# 11 中增加了以下功能:

C# 11 引入了_泛型数学_以及支持该目标的几个功能。 可以为所有数字类型编写一次数值算法。 有更多功能可简化 struct 类型处理,例如所需成员和自动默认结构。 使用原始字符串文本、字符串内插中的换行符和 UTF-8 字符串文本可以更轻松地处理字符串。 文件本地类型等功能使源生成器更简单。 最后,列表模式添加了对模式匹配的更多支持。


参考文章:
C# 11 中的新增功能 - C# 指南 - C# | Microsoft Learn
⭐⭐ C# 11 的新特性和改进前瞻 - hez2010 - 博客园
C# 11 中的新增功能 - 个人文章 - SegmentFault 思否

C++11 新特性总结 - fengMisaka - 博客园
C# 11 的新特性和改进前瞻,.NET新特性 - 小可爱的专栏 - TNBLOG


原始字符串字面量

原始字符串字面量是字符串字面量的一种新格式。 原始字符串字面量可以包含任意文本,包括空格、新行、嵌入引号和其他特殊字符,无需转义序列。 原始字符串字面量以至少三个双引号 (""") 字符开头。 它以相同数量的双引号字符结尾。 通常,原始字符串字面量在单个行上使用三个双引号来开始字符串,在另一行上用三个双引号来结束字符串。 左引号之后、右引号之前的换行符不包括在最终内容中:

string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
                more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;

右双引号左侧的任何空格都将从字符串字面量中删除。 原始字符串字面量可以与字符串内插结合使用,以在输出文本中包含大括号。 多个 $ 字符表示有多少个连续的大括号开始和结束内插:

var location = $$"""
   You are at {{{Longitude}}, {{Latitude}}}
   """;

前面的示例指定了两个大括号开始和结束内插。 第三个重复的左大括号和右大括号包括在输出字符串中。

可以在编程指南中关于字符串的文章中,以及关于字符串字面量内插字符串的语言参考文章中详细了解原始字符串字面量。

💡 注意上面的尾部的三引号,也是有缩进的。如果缩进与文本缩进是一样的,则文本左侧不会有缩进,反之如果三引号没有缩进,则文本里会被添加进缩进,如下所示:

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

泛型数学支持

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 = -1
-1 >> 2 = -1
-1 >> 3 = -1
-1 >> 4 = -1
// ...

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

-1 >>> 1 = 2147483647
-1 >>> 2 = 1073741823
-1 >>> 3 = 536870911
-1 >>> 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 和 intInt64 和 long 的关系一样,C# 中同样存在 IntPtr 和 UIntPtr 的等价简写,分别为 nint 和 nuint,n 表示 native,用来表示这个数值的位数和当前运行环境的内存地址位数相同:

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

泛型属性 💡 应该叫泛型特性

可以声明基类为 System.Attribute 的泛型类。 此功能为需要 System.Type 参数的属性提供了更方便的语法。 以前需要创建一个属性,该属性将 Type 作为其构造函数参数:

// Before C# 11:
public class TypeAttribute : Attribute
{
   public TypeAttribute(Type t) => ParamType = t;

   public Type ParamType { get; }
}

并且为了应用该属性,需要使用 typeof 运算符:

[TypeAttribute(typeof(string))]
public string Method() => default;

使用此新功能,可以改为创建泛型属性:

// C# 11 feature:
public class GenericAttribute<T> : Attribute { }

然后指定类型参数以使用该属性:

[GenericAttribute<string>()]
public string Method() => default;

应用属性时,必须提供所有类型参数。 换句话说,泛型类型必须完全构造。 在上面的示例中,可以省略空括号 (( 和 )),因为特性没有任何参数。

public class GenericType<T>
{
   [GenericAttribute<T>()] // Not allowed! generic attributes must be fully constructed types.
   public string Method() => default;
}

类型参数必须满足与 typeof 运算符相同的限制。 不允许使用需要元数据注释的类型。 例如,不允许将以下类型用作类型参数:

  • dynamic
  • string?(或任何可为 null 的引用类型)
  • (int X, int Y)(或使用 C# 元组语法的任何其他元组类型)。

这些类型不会直接在元数据中表示出来。 它们包括描述该类型的注释。 在所有情况下,都可以改为使用基础类型:

  • object(对于 dynamic)。
  • string,而不是 string?
  • ValueTuple<int, int>,而不是 (int X, int Y)

示例:

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

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

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()}.";

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

列表模式

列表模式扩展了模式匹配,以匹配列表或数组中的元素序列。 例如,当 sequence 为数组或三个整数(1、2 和 3)的列表时,sequence is [1, 2, 3] 为 true。 可以使用任何模式(包括常量、类型、属性和关系模式)来匹配元素。 弃元模式 (_) 匹配任何单个元素,新的范围模式 (..) 匹配零个或多个元素的任何序列。

可以在语言参考的模式匹配文章中了解有关列表模式的更多详细信息。 💡 应当重点去学习模式匹配的概念


C# 11 中新增了列表模式,允许我们对列表进行匹配。在列表模式中,我们可以利用 [ ] 来包括我们的模式,用 _ 代指一个元素,用 .. 代表 0 个或多个元素。在 .. 后可以声明一个变量,用来创建匹配的子列表,其中包含 .. 所匹配的元素。

例如:

var array = new int[] { 1, 2, 3, 4, 5 };
if (array is [1, 2, 3, 4, 5]) Console.WriteLine(1); // 1
if (array is [1, 2, 3, ..]) Console.WriteLine(2); // 2
if (array is [1, _, 3, _, 5]) Console.WriteLine(3); // 3
if (array is [.., _, 5]) Console.WriteLine(4); // 4
if (array is [1, 2, 3, .. var remaining])
{
    Console.WriteLine(remaining[0]); // 4
    Console.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

文件本地类型 💡 应该叫文件局部类型

C# 11 引入了新的文件局部类型可访问性符号 file,利用该可访问性符号,允许我们编写只能在当前文件中使用的类型:

// A.cs

file class Foo
{
    // ...
}

file struct Bar
{
    // ...
}

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

// A.cs
var foo = new Foo(); // ok
var bar = new Bar(); // ok

// B.cs
var foo = new Foo(); // error
var bar = new Bar(); // error

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

必需的成员 💡 requied 成员

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

var foo = new Foo(); // error
var foo = new Foo { X = 1 }; // ok

struct Foo
{
    public required int X;
}

开发者还可以利用 SetsRequiredMembers 这个 attribute 来对方法进行标注,表示这个方法会初始化 required 成员,因此用户在使用时可以不需要再进行初始化:

using System.Diagnostics.CodeAnalysis;

var p = new Point(); // error
var p = new Point { X = 1, Y = 2 }; // ok
var p = new Point(1, 2); // ok

struct Point
{
    public required int X;
    public required int Y;

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

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

自动默认结构 (struct 自动初始化)

C# 11 编译器可以确保在执行构造函数的过程中,将 struct 类型的所有字段初始化为其默认值。 此更改意味着,任何未由构造函数初始化的字段或自动属性都由编译器自动初始化。 现在,构造函数未明确分配所有字段的结构可以进行编译,并且未显式初始化的任何字段都设置为其默认值。 有关此更改如何影响结构初始化的详细信息,请阅读有关结构的文章。


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

struct Point
{
    public int X;
    public int Y;

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

常量 string 上的模式匹配 Span<char>

见文章: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()); // 1
Foo("test".AsSpan()); // 2

如此一来,使用 Span<char> 或者 ReadOnlySpan<char> 的场景也能够非常方便地进行字符串匹配了,而不需要利用 SequenceEquals 或者编写循环进行处理。

扩展的 nameof 范围

C# 11 允许了开发者在参数中对其他参数名进行 nameof,例如在使用 CallerArgumentExpression 这一 attribute 时,此前我们需要直接硬编码相应参数名的字符串,而现在只需要使用 nameof 即可:

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

这将允许我们在进行代码重构时,修改参数名 condition 时自动修改 nameof 里面的内容,方便的同时减少出错。

数值 IntPtr

现在 nint 和 nuint 类型的别名分别为 System.IntPtr 和 System.UIntPtr


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

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

当然,如同 Int32 和 intInt64 和 long 的关系一样,C# 中同样存在 IntPtr 和 UIntPtr 的等价简写,分别为 nint 和 nuint,n 表示 native,用来表示这个数值的位数和当前运行环境的内存地址位数相同:

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

ref 字段和 scoped ref

C# 11 开始,开发者可以在 ref struct 中编写 ref 字段,这允许我们将其他对象的引用存储在一个 ref struct 中:

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

ref 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# 标准现在包含以下项:

  • 允许转换(但不是必需的)以使用已包含这些引用的现有委托实例。

以前版本的标准禁止了编译器重用为方法组转换而创建的委托对象。 C# 11 编译器缓存从方法组转换创建的委托对象,并重用该单个委托对象。 此功能最初在 Visual Studio 2022 版本 17.2 中作为预览功能提供,在 .NET 7 预览版 2 中首次提供。


自动缓存静态方法的委托#

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

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

void Call(Action action)
{
    action();
}

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

警告波 7

为 C# 添加的任何新关键字都是小写 ASCII 字符。 此警告可确保任何类型都不会与将来的关键字冲突。 下面的代码生成 CS8981:

public class lowercasename
{
}

可以通过重命名类型以包含至少一个非小写 ASCII 字符(例如大写字符、数字或下划线)来解决此警告。

本文作者:石起起

本文链接:https://www.cnblogs.com/myshiqiqi/p/18699918

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   石起起  阅读(11)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起