关于.NET 5和C#9.0中的新项目

下午好。



自从.NET诞生以来,我们就一直在使用它。从最初的版本到最新的.NET Core 3.1,我们都有使用该框架的所有版本编写的解决方案。



我们一直在密切关注的.NET的历史一直在我们眼前发生:计划于11月发布的.NET 5版本刚刚以Release Candidate 2的形式发布。我们很久以来就警告说,第五个版本将是划时代的:它将以划时代的结尾: .NET精神分裂症,当框架有两个分支时:经典和核心。现在它们将在狂喜中合并,并且将有一个连续的.NET。



发布了RC2您已经可以充分使用它了-在发行之前不会进行任何新的更改,只会修复发现的错误。此外:RC2已经有一个专用于.NET官方网站



并且,我们向您概述了.NET 5和C#9中的创新。所有带有代码示例的信息均来自.NET平台开发人员的官方博客(以及许多其他来源),并经过了个人验证。



新的本机和新的类型



C#和.NET同时添加了本机类型:



  • C#的nint和nuint
  • 它们在BCL中对应的System.IntPtr和System.UIntPtr


添加这些类型的重点是使用低级API的操作。诀窍在于,这些类型的实际大小已经在运行时确定,并且取决于系统的位:对于32位,它们的大小将为4个字节,而对于64位,则分别为8个字节。



您很可能在实际工作中不会遇到这些类型。但是,还有另一种新类型:Half。此类型仅在BCL中存在,在C#中尚无类似类型。它是浮点值的16位类型。在不需要地狱精度的情况下,它可以派上用场,并且您可以赢得一些用于存储值的内存,因为float和double类型占用4和8个字节。最有趣的是到目前为止,对于这种类型算术运算未定义,并且即使不显式将其强制转换为float或double,也不能添加Half类型的两个变量。也就是说,这种类型的目的现在纯粹是功利主义的,以节省空间。但是,他们计划在下一个.NET和C#版本中为其添加算术运算。在一年以内。



局部功能的属性



以前,它们是被禁止的,这带来了一些不便。特别是,不可能挂起局部函数参数的属性。现在,您可以为它们设置属性,包括函数本身及其参数。例如,像这样:



#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }

    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}


静态Lambda表达式



该功能的目的是确保lambda表达式不能捕获表达式本身之外的任何上下文和局部变量。通常,它们可以捕获本地上下文的事实通常在开发中很有用。但这有时可能导致难以捕获的错误。



为了避免此类错误,现在可以用static关键字标记lambda表达式。在这种情况下,它们将无法访问任何局部上下文:从局部变量到此变量和基数。



这是一个非常全面的用法示例:



static void SomeFunc(Func<int, int> f)
{
    Console.WriteLine(f(5));
}

static void Main(string[] args)
{
    int y1 = 10;
    const int y2 = 10;
    SomeFunc(i => i + y1);          //  15
    SomeFunc(static i => i + y1);   //  : y1    
    SomeFunc(static i => i + y2);   //  15
}


请注意,常量可以很好地捕获静态lambda。



GetEnumerator作为扩展方法



现在,GetEnumerator方法可以是扩展方法,即使您以前无法枚举,也可以通过它迭代foreach。例如-元组。



这是一个示例,当可以使用为其编写的扩展方法通过foreach遍历ValueTuple时:



static class Program
{
    public static IEnumerator<T> GetEnumerator<T>(this ValueTuple<T, T, T, T, T> source)
    {
        yield return source.Item1;
        yield return source.Item2;
        yield return source.Item3;
        yield return source.Item4;
        yield return source.Item5;
    }

    static void Main(string[] args)
    {
        foreach(var item in (1,2,3,4,5))
        {
            System.Console.WriteLine(item);
        }
    }
}


此代码将数字从1到5打印到控制台。



Lambda表达式和匿名函数的参数中的丢弃模式



微改进。如果您不需要lambda表达式或匿名函数中的参数,则可以用下划线替换它们,从而忽略以下内容:



Func<int, int, int> someFunc1 = (_, _) => {return 5;};
Func<int, int, int> someFunc2 = (int _, int _) => {return 5;};
Func<int, int, int> someFunc3 = delegate (int _, int _) {return 5;};


C#中的顶级语句



这是简化的C#代码结构。现在,编写最简单的代码看起来真的很简单:



using System;

Console.WriteLine("Hello World!");


而且所有这些都可以编译。也就是说,现在您无需创建应在其中放置控制台输出指令的方法,您无需描述应在其中放置方法的任何类,也无需定义应在其中创建该类的名称空间。



顺便说一下,将来,C#开发人员正在考虑使用简化的语法开发主题,并尝试摆脱使用System的麻烦。在明显的情况下。同时,您可以通过以下简单的编写来摆脱它:



System.Console.WriteLine("Hello World!");


这实际上将是一个单行工作程序。



可以使用更复杂的选项:



using System;
using System.Runtime.InteropServices;

Console.WriteLine("Hello World!");
FromWhom();
Show.Excitement("Top-level programs can be brief, and can grow as slowly or quickly in complexity as you'd like", 8);

void FromWhom()
{
    Console.WriteLine($"From {RuntimeInformation.FrameworkDescription}");
}

internal class Show
{
    internal static void Excitement(string message, int levelOf)
    {
        Console.Write(message);

        for (int i = 0; i < levelOf; i++)
        {
            Console.Write("!");
        }

        Console.WriteLine();
    }
}


实际上,编译器本身会将所有这些代码包装在必要的名称空间和类中,而您只是一无所知。



当然,此功能有局限性。最主要的是,这只能在一个项目文件中完成。通常,在以前以Main(string [] args)函数形式创建程序入口点的文件中执行此操作是有意义的。同时,不能在此处定义Main函数本身-这是第二个限制。实际上,这种具有简化语法的文件本身就是Main函数,它甚至隐式包含args变量,这是一个带有参数的数组。也就是说,此代码还将编译并显示数组的长度:



System.Console.WriteLine(args.Length);


通常,此功能不是最重要的功能,但出于演示和培训目的,它非常适合自己。详细信息在这里



if语句中的模式匹配



想象一下,您需要检查对象变量不是某种类型。到现在为止,必须这样写:



if (!(vehicle is Car)) { ... }


但是,使用C#9.0,您可以人工编写:



if (vehicle is not Car) { ... }


还可以紧凑地记录一些支票:



if (context is {IsReachable: true, Length: > 1 })
{
    Console.WriteLine(context.Name);
}


这种新的表示法等同于好的旧表示法,如下所示:



if (context is object && context.IsReachable && context.Length > 1 )
{
    Console.WriteLine(context.Name);
}


或者,您也可以用相对较新的方式编写相同的内容(但这已经是昨天了):



if (context?.IsReachable && context?.Length > 1 )
{
    Console.WriteLine(context.Name);
}


在新语法中,还可以使用布尔运算符和(或不可以加上)括号来确定优先级:



if (context is {Length: > 0 and (< 10 or 25) })
{
    Console.WriteLine(context.Name);
}


这些只是常规if中模式匹配的改进。我们添加到switch表达式的模式匹配中的内容-继续阅读。



改进了开关表达式中的模式匹配



switch表达式(不要与switch语句混淆)在模式匹配方面有巨大的改进。让我们看一下官方文档中的示例例子致力于在特定时间计算特定交通工具的票价。这是第一个示例:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c           => 2.00m,
    Taxi t          => 3.50m,
    Bus b           => 5.00m,
    DeliveryTruck t => 10.00m,
    { }             => throw new ArgumentException("Unknown vehicle type", nameof(vehicle)),
    null            => throw new ArgumentNullException(nameof(vehicle))
};


switch语句的最后两行是新的。花括号代表任何不为null的对象。现在,您可以使用match关键字来匹配null。



这还不是全部。请注意,对于每个到对象的映射,必须创建一个变量:c代表Car,t代表Taxi,依此类推。但是不使用这些变量。在这种情况下,您现在可以在C#8.0中使用丢弃模式:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car _           => 2.00m,
    Taxi _          => 3.50m,
    Bus _           => 5.00m,
    DeliveryTruck _ => 10.00m,
    // ...
};


但是从C#的第九个版本开始,在这种情况下您什么也不能写:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car             => 2.00m,
    Taxi            => 3.50m,
    Bus             => 5.00m,
    DeliveryTruck   => 10.00m,
    // ...
};


switch表达式的改进还不止于此。现在更容易编写更复杂的表达式。例如,通常返回的结果必须取决于传递的对象的属性值。现在,与if的组合相比,可以更短,更方便地编写它:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car { Passengers: 0 } => 2.00m + 0.50m,
    Car { Passengers: 1 } => 2.0m,
    Car { Passengers: 2 } => 2.0m - 0.50m,
    Car => 2.00m - 1.0m,
    // ...
};


请注意开关中的前三行:实际上,检查了Passengers属性的值,如果相等,则返回相应的结果。如果不匹配,则将返回常规变量的值(开关内的第四行)。顺便说一句,仅当传递的车辆对象不为null并且是Car类的实例时才检查属性值。也就是说,您在检查时不必担心Null Reference Exception。



但这还不是全部。现在,在switch表达式中,您甚至可以编写表达式以更方便地进行匹配:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,
    // ...
};


不仅如此。开关表达式语法已扩展到嵌套开关表达式,以使我们更容易描述复杂的条件:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },
    // ...
};


结果,如果您完全粘合了已经给出的所有代码示例,则可以同时获得所有描述的创新的图片:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },

    Taxi t => t.Fares switch
    {
        0 => 3.50m + 1.00m,
        1 => 3.50m,
        2 => 3.50m - 0.50m,
        _ => 3.50m - 1.00m
    },

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,

    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException(nameof(vehicle))
};


但这也不是全部。这是另一个示例:一个普通的函数,该函数使用switch表达式机制根据经过的时间确定负载:早/晚高峰时间,白天和晚上:



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };


如您所见,在C#9.0中,还可以在比较时使用比较运算符<,>,<=,> =以及逻辑运算符和和(或不)。



但是,该死,这还不是终点。现在,您可以在switch表达式中使用...元组。这是一个完整的代码示例,它根据周几,一天中的时间和行进方向(往返城市)来计算票价的特定系数:



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
{
    DayOfWeek.Saturday => false,
    DayOfWeek.Sunday => false,
    _ => true
};

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
{
    < 6 or > 19 => TimeBand.Overnight,
    < 10 => TimeBand.MorningRush,
    < 16 => TimeBand.Daytime,
    _ => TimeBand.EveningRush,
};

public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true) => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime, true) => 1.50m,
    (true, TimeBand.Daytime, false) => 1.50m,
    (true, TimeBand.EveningRush, true) => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight, true) => 0.75m,
    (true, TimeBand.Overnight, false) => 0.75m,
    (false, TimeBand.MorningRush, true) => 1.00m,
    (false, TimeBand.MorningRush, false) => 1.00m,
    (false, TimeBand.Daytime, true) => 1.00m,
    (false, TimeBand.Daytime, false) => 1.00m,
    (false, TimeBand.EveningRush, true) => 1.00m,
    (false, TimeBand.EveningRush, false) => 1.00m,
    (false, TimeBand.Overnight, true) => 1.00m,
    (false, TimeBand.Overnight, false) => 1.00m,
};


PeakTimePremiumFull方法使用元组进行匹配,这在新版本的C#9.0中成为可能。顺便说一句,如果您仔细查看代码,则有两种优化建议:



  • 最后八行返回相同的值;
  • 白天和黑夜的流量具有相同的系数。


结果,使用丢弃模式可以大大减少方法代码:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true)  => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime,     _)     => 1.50m,
    (true, TimeBand.EveningRush, true)  => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight,   _)     => 0.75m,
    (false, _,                   _)     => 1.00m,
};


好吧,如果您更仔细地看,那么您也可以减少此选项,将系数1.0视为一般情况:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.Overnight, _) => 0.75m,
    (true, TimeBand.Daytime, _) => 1.5m,
    (true, TimeBand.MorningRush, true) => 2.0m,
    (true, TimeBand.EveningRush, false) => 2.0m,
    _ => 1.0m,
};


以防万一,让我澄清一下:比较是按照列出的顺序进行的。在第一个匹配项上,将返回相应的值,并且不进行进一步的比较。





开关表达式中的更新元组也可以在C#8.0中使用。撰写本文的毫无价值的开发人员变得更加聪明。





最后,这是另一个疯狂的示例,演示了用于与元组和对象属性匹配的新语法:



public static bool IsAccessOkOfficial(Person user, Content content, int season) => 
    (user, content, season) switch 
{
    ({Type: Child}, {Type: ChildsPlay}, _)          => true,
    ({Type: Child}, _, _)                           => false,
    (_ , {Type: Public}, _)                         => true,
    ({Type: Monarch}, {Type: ForHerEyesOnly}, _)    => true,
    (OpenCaseFile f, {Type: ChildsPlay}, 4) when f.Name == "Sherlock Holmes"  => true,
    {Item1: OpenCaseFile {Type: var type}, Item2: {Name: var name}} 
        when type == PoorlyDefined && name.Contains("Sherrinford") && season >= 3 => true,
    (OpenCaseFile, var c, 4) when c.Name.Contains("Sherrinford")              => true,
    (OpenCaseFile {RiskLevel: >50 and <100 }, {Type: StateSecret}, 3) => true,
    _                                               => false,
};


这一切看起来很不寻常。为了全面理解,建议您看一下source,其中有完整的代码示例。



新的以及基本改进的目标类型



很久以前,在C#中,可以写var而不是类型名,因为类型本身可以根据上下文确定(实际上,这称为目标类型)。即,代替以下条目:



SomeLongNamedType variable = new SomeLongNamedType();


可以更紧凑地编写:



var variable = new SomeLongNamedType()


并且编译器将猜测变量本身的类型。多年来,实施了相反的语法:



SomeLongNamedType variable = new ();


特别感谢以下事实:该语法不仅在声明变量时有效,而且在编译器可以立即猜测类型的许多其他情况下也有效。例如,将参数传递给方法并从该方法返回值时:



var result = SomeMethod(new (2020,10,01));

//...

public Car SomeMethod(DateTime p)
{
    //...

    return new() { Passengers = 2 };
}


在此示例中,当调用SomeMethod时,DateTime类型的参数由简写语法创建。从方法返回的值以相同的方式创建。



定义集合时,从此语法真正受益的地方是:



List<DateTime> datesList = new()
{
    new(2020, 10, 01),
    new(2020, 10, 02),
    new(2020, 10, 03),
    new(2020, 10, 04),
    new(2020, 10, 05)
};

Car[] cars = 
{
    new() {Passengers = 2},
    new() {Passengers = 3},
    new() {Passengers = 4}
};


列出集合的元素时无需写完整的类型名,使代码更简洁。



目标类型运算符 和?:



三元运算符?:在C#9.0中进行了改进。以前,它要求完全符合返回类型,但现在更聪明了。这是一个表达式的示例,该表达式在该语言的早期版本中无效,但在第九种语法中是合法的:



int? result = b ? 0 : null; // nullable value type


以前,需要将其显式地从零转换为int?..现在没有必要了。



另外,在该语言的新版本中,允许使用以下构造:



Person person = student ?? customer; // Shared base type


客户和学生类型,尽管派生自Person,但在技术上有所不同。该语言的先前版本不允许您在没有显式类型转换的情况下使用此类构造。现在,编译器可以很好地理解其含义。



覆盖方法的返回类型



在C#9.0中,允许重写被重写方法的返回类型。仅有一个要求:新类型必须继承自原始类型(协变)。这是一个例子:



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


在Tiger类中,已将GetFood方法的返回值从“食物”转换为“肉”。现在可以从食品中提取肉了。



初始化属性不是真正的只读成员



该语言的新版本中出现了一个有趣的功能:init-properties。这些属性只能在对象的初始初始化期间设置。似乎存在只读类成员,但实际上,它们是不同的事物,可让您解决不同的问题。要了解有什么区别,以及初始化属性的优点是什么,下面是一个示例:



Person employee = new () {
    Name = "Paul McCartney",
    Company = "High Technologies Center",
    CompanyAddress = new () {
        Country = "Russia",
        City = "Izhevsk",
        Line1 = "246, Karl Marx St."
    }
}


这种用于声明类实例的语法非常方便,尤其是当类属性中有更多对象时。但是这种语法有局限性:相应的类属性必须是mutable这是因为这些属性的初始化发生调用构造函数之后。也就是说,示例中的Person类应这样声明:



class Person {
    //...
    public string Name {get; set;}
    public string Company {get; set;}
    public Address CompanyAddress {get; set;}
    //...
}


但是,实际上,Name属性是不可变的。当前,使此只读属性成为唯一方法是声明一个私有setter:



class Person {
    //...
    public string Name {get; private set;}
    //...
}


但是在这种情况下,我们立即通过给花括号内的属性赋值来失去使用方便的语法声明类实例的能力。而且我们只能通过将参数传递给类构造函数来设置Name属性的值。现在想象一下CompanyAddress属性在含义上也是不可变的。总的来说,我多次遇到这种情况,我总是不得不在两种弊端之间做出选择:



  • 花哨的构造函数,带有一堆参数,但具有只读类的所有属性;
  • 方便的语法来创建对象,但具有读写类的所有属性,我必须记住这一点,不要在某个地方意外更改它们。


此时,某人可能会想起该类的只读成员,并建议对Person类进行如下样式设置:



class Person {
    //...
    public readonly string Name;
    public readonly string Company;
    public readonly string CompanyAddress;
    //...
}


我要回答的是,这种方法不仅不符合风水学的要求,而且还不能解决方便初始化的问题:只读成员也只能在构造函数中设置,例如带有私有设置程序的属性。



但是在C#9.0中,此问题得以解决:如果将一个属性定义为一个init属性,则既可以方便地创建对象,也可以使用将来实际上不可变的属性:



class Person {
    public string Name { get; init; }
    public string Company { get; init; }
    public Address CompanyAddress { get; init; }
}


顺便说一下,在init-properties中,就像在构造函数中一样,您可以初始化只读类成员,并且可以这样编写:



public class Person
{
    private readonly string name;
       
    public string Name
    { 
        get => name; 
        init => name = (value ?? throw new ArgumentNullException(nameof(Name)));
    }
}


记录是合法的DTO



在我看来,延续不变属性的主题是语言的创新:记录类型。此类型旨在方便地创建整个不可变的结构,而不仅仅是属性。出现单独类型的原因很简单:根据所有规范进行工作,我们不断创建DTO以隔离应用程序的不同层。 DTO通常只是字段的集合,没有任何业务逻辑。而且,通常,这些字段的值在此DTO的生命周期内不会更改。



.



DTO – Data Transfer Object. (DAL, BL, PL) - . «». -DTO' DAL BL, , DTO-, , DTO-, - ( HTML- JSON-).



— DTO-, - -, .



DTO- - . DTO-, AutoMapper - .



, DTO- .



因此,经过很多年,C#开发人员终于获得了真正需要的改进:他们将DTO模型合法化为单独的记录类型。



到目前为止,我们创建的所有DTO模型(并且我们大量创建了它们)都是普通的类。对于编译器和运行时,它们与所有其他类没有什么不同,尽管它们在经典意义上没有什么不同。很少有人为DTO模型使用结构-由于各种原因,这并不总是可以接受的。



现在我们可以定义记录(以下称为记录)-一种特殊的结构,旨在创建不可变的DTO模型。在通常意义上,记录在结构和类之间处于中间位置。它既是子类又是上层结构。记录仍然是具有所有后续后果的参考类型。记录几乎总是像常规类一样工作,它们可以包含方法,它们允许继承(但只能从其他记录而不是对象继承,尽管如果记录未显式继承任何东西,则它隐式继承自对象,就像C#中的所有内容一样)可以实现接口。而且,您根本不必使记录完全不变。那么意义在哪里,又有什么区别?



让我们创建一个条目:



public record Person 
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}


现在是一个使用示例:



Person p1 = new ("Paul", "McCartney");
Person p2 = new ("Paul", "McCartney");

System.Console.WriteLine(p1 == p2);


本示例将在控制台上显示true。如果Person是一个类,则将在控制台上打印false,因为对象是通过引用进行比较的:两个引用变量仅在引用同一对象时才相等。但是录音并非如此。记录将通过其所有字段(包括私有字段)的值进行比较。



继续前面的示例,让我们看下面的代码:



System.Console.WriteLine(p1);


对于类,我们将在控制台中收到该类的全名。但是对于条目,我们将在控制台中看到:



Person { LastName = McCartney, FirstName = Paul}


事实是,对于记录,ToString()方法被隐式覆盖,并且不显示类型名称,而是显示带有值公共字段的完整列表类似地,对于记录,==和!=运算符被隐式重新定义,这使得可以更改比较逻辑。



让我们玩一下记录继承:



public record Teacher : Person
{
    public string Subject { get; }

    public Teacher(string first, string last, string sub)
        : base(first, last) => Subject = sub;
}


现在让我们创建两个不同类型的帖子并进行比较:



Person p = new("Paul", "McCartney");
Teacher t = new("Paul", "McCartney", "Programming");

System.Console.WriteLine(p == t);


尽管Teacher记录是从Person继承的,但是p和t变量将不相等,false将被打印到控制台。这是因为不仅对记录的所有字段都进行了比较,而且还对类型进行了比较,并且此处的类型明显不同。



并且虽然允许比较继承的记录类型(但毫无意义),但原则上通常不允许比较不同的记录类型:



public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

public record Person2
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person2(string first, string last) => (FirstName, LastName) = (first, last);
}

// ...

Person p = new("Paul", "McCartney");
Person2 p2 = new("Paul", "McCartney");
System.Console.WriteLine(p == p2);    //  


条目似乎相同,但最后一行会出现编译错误。您只能比较相同类型或继承类型的记录。



记录的另一个不错的功能是with关键字,它使创建DTO模型的修改变得容易。看一个例子:



Person me = new("Steve", "Brown");
Person brother = me with { FirstName = "Paul" };


在此示例中,对于Brother记录,除了FirstName字段外,所有字段值都将从me记录中填充-将其更改为Paul。



到目前为止,您已经了解了创建记录的经典方法-具有构造函数,属性等的完整定义。但是现在还有一种简洁的方法:



public record Person(string FirstName, string LastName);

public record Teacher(string FirstName, string LastName,
    string Subject)
    : Person(FirstName, LastName);

public sealed record Student(string FirstName,
    string LastName, int Level)
    : Person(FirstName, LastName);


您可以以简写形式定义记录,编译器将为您创建属性和构造函数。但是,此功能还有一个附加功能-您不仅可以使用简写表示法来定义属性和构造函数,而且还可以在条目中添加自己的方法:



public record Pet(string Name)
{
    public void ShredTheFurniture() =>
        Console.WriteLine("Shredding furniture");
}

public record Dog(string Name) : Pet(Name)
{
    public void WagTail() =>
        Console.WriteLine("It's tail wagging time");

    public override string ToString()
    {
        StringBuilder s = new();
        base.PrintMembers(s);
        return $"{s.ToString()} is a dog";
    }
}


在这种情况下,记录的属性和构造函数也会自动创建。样板代码越来越少,但仅适用于帖子。这不适用于类和结构。



除了已经说过的一切,编译器还可以自动为记录创建一个解构函数:



var person = new Person("Bill", "Wagner");

var (first, last) = person; //    
Console.WriteLine(first);
Console.WriteLine(last);


但是,在IL级别,记录仍然是一类。但是,有一种怀疑尚未找到答案:可以肯定的是,在运行时级别,记录将在某处进行优化。由于很可能会事先知道特定记录是不可变的,因此很有可能。至少在多线程环境中,这为优化提供了机会,并且开发人员甚至不需要为此付出特殊的努力。



同时,我们正在将所有DTO模型从类重写为记录。



.NET源代码生成器



源发生器(以下简称发生器)是一个非常有趣的功能。生成器是在编译阶段执行的一段代码,具有分析已经编译的代码的能力,并且可以生成也将被编译的其他代码。如果还不清楚,那么这里是一个可能需要发电机的示例。



想象一下您在ASP.NET Core中编写的C#/.NET Web应用程序。启动此应用程序时,需要进行大量的初始化后台工作,以分析此应用程序的组成以及应执行的操作。反射经常被使用。结果,从启动应用程序到开始处理第一个请求的时间可能过长,这在高负载的服务中是不可接受的。生成器可以帮助减少时间:即使在编译阶段,它也可以分析已经编译的应用程序,并生成必要的代码,这些代码将在启动时更快地对其进行初始化。



还有大量的库在运行时使用反射来确定所用对象的类型(其中有很多顶级的Nuget包)。这为使用生成器的优化开辟了广阔的空间,此功能的作者期望库开发人员进行适当的改进。



代码生成器是一个新主题,太不寻常了,无法满足本文的范围。此外,您还可以看到最简单的“世界您好!”的示例。发电机在此评论



与代码生成器关联的两个新功能如下所述。



部分方法



C#中的局部类已经存在了很长时间,其原始目的是将某个设计人员生成的代码与程序员编写的代码分开。在C#9.0中已量身定制了部分方法。他们看起来像这样:



public partial class MyClass
{
    public partial int DoSomeWork(out string p);
}
public partial class MyClass
{
    public partial int DoSomeWork(out string p)
    {
        p = "test";
        System.Console.WriteLine("Partial method");
        return 5;
    }
}


这个替代示例演示了部分方法与普通方法基本没有什么不同:它们可以返回值,可以接受变量,并且可以具有访问修饰符。



根据可用的信息,部分方法将与打算使用它们的代码生成器紧密相关。



模块初始化器



引入此功能的三个原因:



  • 允许库在启动时以某种方式进行一次一次性初始化,而所需的开销最小,并且无需用户明确调用;
  • 静态构造函数的现有功能不太适合此角色,因为下雨时间必须首先弄清楚是否使用了带有静态构造函数的类(这些是规则),这会产生可测量的延迟;
  • 代码生成器必须具有不需要显式调用的某种初始化逻辑。


实际上,对于要包含在发行版中的功能而言,最后一点似乎已具有决定性意义。结果,我们想出了一个新属性,需要覆盖初始化的方法:



using System.Runtime.CompilerServices;
class C
{
    [ModuleInitializer]
    internal static void M1()
    {
        // ...
    }
}


该方法有一些限制:



  • 它必须是静态的;
  • 它必须没有参数;
  • 它不应该返回任何东西;
  • 它不应该与泛型一起使用;
  • 必须从包含的模块可以访问它,即:

    • 它必须是内部的或公共的
    • 它不必是本地方法


它的工作方式是这样的:只要编译器找到所有标有ModuleInitializer属性的方法,它就会生成调用它们的特殊代码。初始化方法的调用顺序无法指定,但是每次编译时都相同。



结论



在已经发布了该帖子之后,我们注意到,该帖子更专注于C#9.0语言的新闻,而不是.NET本身的新闻。但是结果很好。



All Articles