C#9.0新特性学习

C#9.0引入了init关键字用于只初始化属性,保证对象创建后不再改变。记录(records)提供了一种更安全的不可变对象模型,支持基于值的相等性。with表达式允许创建新对象时仅修改部分属性。此外,文章还讨论了类型推导、顶层程序、模式匹配等新特性的应用。
摘要由CSDN通过智能技术生成

目录

init关键字

只初始化属性设置器 — init关键字

init属性访问器和只读字段 

记录 / Records

with表达式

基于值的相等

继承 / Inheritance

位置记录 / Positional records

顶层程序(Top-Level Programs)

增强的模式匹配

简单类型模式

关系模式

逻辑模式

类型推导new表达式

返回值类型支持协变

本地大小的整型——nint和nuint

静态匿名方法

模块初始化器

本地函数支持Attribute

扩展的分部方法

Lambda弃元参数

类型推导的条件表达式

方法指针

禁止发出localsinit标记

扩展GetEnumerator支持foreach循环


init关键字

只初始化属性设置器 — init关键字

在位对象设置初始化时通常需要设置{get;set},因此对于初始化来说,属性必须是可变的。也就是必须有set,当某些情况下不需要set出现时,则可以使用以下的方式。

class MyTest
    {
        public string username { get; init; }
        public string password { get; init; }
    }

这种方式允许让类在初始化时进行赋值,第二次赋值的时候则不会允许。

init属性访问器和只读字段 

因为init访问器只能在初始化时被调用,所以在init属性访问器中可以改变封闭类的只读字段。

public string username
    { 
        get => username; 
        init => username= (value ?? "username is null");
    }

记录 / Records

 传统面向对象的编程的核心思想是一个对象有着唯一标识,封装着随时可变的状态。C#也是一直这样设计和工作的。但是一些时候,你就非常需要刚好对立的方式。原来那种默认的方式往往会成为阻力,使得事情变得费时费力。如果你发现你需要整个对象都是不可变的,且行为像一个值,那么你应当考虑将其声明为一个record类型。 

class record  MyTest
    {
        public string username { get; init; }
        public string password { get; init; }
    }

一个record仍然是一个类,但是关键字record赋予这个类额外的几个像值的行为。通常说,records由他们的内容来界定,不是他们的标识。从这一点上讲,records更接近于结构,但是他们依然是引用类型。

with表达式

var myTest= new MyTest{ username= "admin", password= "123456" };
var test= myTest with { password= "123" };

with表达式使用初始化语法来说明新对象在哪里与原来对象不同。with表达式实际上是拷贝原来对象的整个状态值到新对象,然后根据对象初始化器来改变指定值。这意味着属性必须有init或者set访问器,才能用with表达式进行更改。

基于值的相等

所有对象都从object类型继承了Equals(object),这是静态方法Object.Equals(object, object) 用来比较两个非空参数的基础。

结构重写了这个方法,​通过递归调用每个​结构字段的Equals方法,从而有了“基于值的相等”,Recrods也是这样。这意味着只要他们的值保持一致,两个record对象可以不是同一个对象就会相等。

继承 / Inheritance

记录(record)可以从其他记录(record)继承

with表达式和值相等性与记录的继承结合的很好,因为他们考虑到了整个运行时对象,不只是静态的已知类型。

位置记录 / Positional records

有时,有更多的位置定位方式对一个记录是很有用的,在那里,记录的内容是通过构造函数的参数传入,并且通过位置解构函数提取出来。可能会在记录中定义自己的构造和解构函数(注意不是析构函数)。

顶层程序(Top-Level Programs)

在C#9.0中Program.cs中的结构发生了变化。可以用顶层用如下代码代替写原来的主程序。

//旧的
using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}
//新的
using System;

Console.WriteLine("Hello World!");

本地函数作为语句的另一种形式,也是允许在顶层程序代码中使用的。在顶层代码段外部的任何地方调用他们都会产生错误。

增强的模式匹配

public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
       ...
       
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

简单类型模式

当前,进行类型匹配的时候,一个类型模式需要声明一个标识符(即使这标识符是一个弃元_),像上面代码中的DeliveryTruck_ 。但是在C#9.0中,可以只写类型。

DeliveryTruck => 10.00m

关系模式

C#9.0 提出了关系运算符<,<=等对应的模式。所以你现在可以将上面模式中的DeliveryTruck部分写成一个嵌套的switch表达式。

DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
}

这里 >5000和<3000就是关系模式

逻辑模式

你可以用逻辑操作符and,or 和not将模式进行组合,这里的操作符用单词来表示,是为了避免与表达式操作符引起混淆。例如,上面嵌套的的switch可以按照升序排序。

DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
}

类型推导new表达式

类型推导是从一个表达式所在的位置根据上下文获得它的类型时使用的一个术语。例如null和lambda表达式总是涉及到类型推导的。

在C#中,new表达式总是要求一个具体指定的类型(除了隐式类型数组表达式)。现在,如果表达式被指派给一个明确的类型时,可以忽略new关键字后面的类型。

MyTest myTest = new ("admin", "123456");

数组的方式:

MyTest[] myTest = {new ("admin1", "123456"),new ("admin2", "123456"),new ("admin3", "123456")};

返回值类型支持协变

有时候,在子类的一个重写方法中返回一个更具体的、且不同于父类方法的返回类型更为有用,C# 9.0对这种情况提供了支持。如下列子中,子类Tiger的在重写父类Animal的GetFood方法时,返回值使用了Meat而不是Food,就更为形象具体。 

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}

本地大小的整型——nint和nuint

为了互操作和实现底层库的需要,C# 9.0 引入了两个新的上下文相关的关键字——有符号和无符号的两个整型类型,即nint和nuint。这两种类型的大小是32还是64的取决于所用平台。在被编译之后,这两个关键字会被转换为System.IntPtr 和System.UIntPtr。

nint常量值介于范围 [ int.MinValueint.MaxValue ],nuint的常量值介于范围[ uint.MinValueuint.MaxValue ],但是他们没有像int和uint那样MaxValue和MinValue这两个静态属性。同时,他们支持所有的一元{ +-~ } 和二元运算符 { +-*/%==!=<<=>>=&|^<<>> }. 

nint x = 3;
_ = nint.Equals(x, 3);

静态匿名方法

允许在Lambda表达式和匿名方法使用static关键字,用来防止访问所处范围的对象实例的状态或者本地变量。 

Action<int> nc = static delegate (int x) {Console.WriteLine("Anonymous Method: {0}", x);
Action<int> la = static (int a) => a++;

模块初始化器

为了使在库加载的时候,能以最小的开销做一些期望的一次性的初始化工作,并且可以使源代码生成器运行一些全局的初始化逻辑。C# 9.0引入了模块初始化器。模块初始化器被设计为一个Attribute。

using System;
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class ModuleInitializerAttribute : Attribute { }
}
  • 方法必须使静态的、无参的、返回值为void的函数。
  • 该方法不能是泛型或者包含在泛型类型里
  • 该方法必须是可从其包含模块里访问的。也就是说,方法的有效访问符必须是internal或者public,不能是本地方法。
using System.Runtime.CompilerServices;
class MyClass​
{
    [ModuleInitializer]
    internal static void Initializer​()
    {
        // ...
    }
}

本地函数支持Attribute

本地函数现在允许添加Attribute,并且参数和类型参数都是允许的。extern关键字也可以用于本地方法。这里,因为一个用的extern关键字,一个用的是ConditionalAttribute,所有都是static静态方法。 

private static void PrintEnds(Func<bool> condition, string str, string separator ="+")
{
    if (condition())
    {
        Console.Write(str);
    }
    else
    {
        Console.Write(@$"{str} {separator} ");
    }
​
    Log();
    Print();
​
    [DllImport("log")]
    extern static void Log();
​
    [Conditional("DEBUG")]
    static void Print()
    {
        Console.WriteLine("print is running");
    }
}

扩展的分部方法

为了使partial方法变成为更为通用的C#方法声明形式,C#9.0移除了partial方法签名的所有限制。分部方法将方法划分声明和定义两部分。

partial class MyClass
{
    // MyClass.Print的声明
    partial void Print(string message);
}
​
partial class MyClass
{
    // MyClass.Print的定义或实现
    partial void Print(string message) => Console.WriteLine(message);
}

Lambda弃元参数

允许弃元被用作lambda和匿名表达式的的参数,从而没有使用的参数可以不必被命名。

Func<int,int,nint> f= (_, _) => 0;
Func<int, int, nint> f2 = (int _,int _) => 0;
Func<int, int, nint> f3 = delegate (int _, int _) { return 0; };

类型推导的条件表达式

对于一个条件表达式 c ? e1 : e2,当存在下面情况的时候,我们定义了一个新的隐式条件表达式转换,它允许从条件表达式到任一类型T有一个隐式的转换。 

  • 对于e1和e2如果没有共同的类型。
  • 或者有共同的类型存在,但是e1或者e2的表达式没有到共同类型的隐式转换。

对来自于表达式转换为这个类型T的情况来说,有一个来自于从e1到T的转换,也有一个从e2到T的转换。如果条件表达式在e1和e2之间既没有共同类型,也没有相关适合的条件表达式转换,那么就会出现错误。

方法指针

在今天的C#中,当前的中间语言IL的指令集没能有效地被访问,如:ldftn(将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上)和calli(通过调用约定描述的参数调用在计算堆栈上指示的方法(作为指向入口点的指针))。方法指针就是提供了有效的方式来访问IL指令集接口,以便开发人员能编写高性能代码。

 C#允许你用delegate*来声明函数指针。

unsafeclass Example {

void Example(Action<int> a, delegate*<int, void> f) {
        a(42);
        f(42);
    }
}

禁止发出localsinit标记

编译器对所有带有局部变量的方法发出localsinit标记,告诉他们对所有局部变量进行初始化,以防止代码运行时产生不可预知的错误。允许通过SkipLocalsInitAttribute 属性来禁止发出localsinit标记。对于自动初始化局部变量,特别是使用stackalloc的数据的时候,他们所花的成本是非常明显的,对于性能要求特别高的程序,该特性是有重要意义的。

[SkipLocalsInit]
unsafe static void Print()
{
    Span<int> members = stackalloc int[100];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
     }
}

SkipLocalsInitAttribute可以用于方法,属性,模块,类,结构、接口和构造函数。应用于在容器(类、模块,包含嵌入函数的函数等)上时,其里面所有的方法都会受到影响。对于抽象函数和和没有传播到的覆盖/实现的方法,时无效的。

扩展GetEnumerator支持foreach循环

允许foreach循环能识别以扩展方法定义的GetEnumberator()方法,该方法的定义必须满足foreach模式的其他要求

  1. GetEnumerator()的返回值类型必须是类,结构或者接口,并且返回值类型有成员符合要求的方法MoveNext()和属性Current。
  2. GetEnumerator()不能有类型参数。

本文参考:C# 9.0新特性_c#9.0_wvtjplh的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值