C# 12 作为 .NET 8 的一部分引入了一组引人注目的新功能!在这篇文章中,我们将探讨其中一个功能,特别是主要构造函数,解释其用法和相关性。然后,我们将演示一个示例重构,以展示如何将其应用到你的代码中,并讨论其好处和潜在的缺陷。这将帮助你了解更改的影响并帮助影响你对该功能的采用。
主构造函数
主构造函数被认为是“日常 C#”开发人员功能。它们允许你在一个简洁的声明中定义类或结构及其构造函数。这可以帮助你减少需要编写的样板代码量。如果你一直在关注 C# 版本,你可能熟悉记录类型,其中包括主构造函数的第一个示例。
与记录类型的区别
记录类型作为类或结构的类型修饰符引入,它简化了构建简单类(如数据容器)的语法。记录可以包括主构造函数。该构造函数不仅生成一个支持字段,而且还为每个参数公开一个公共属性。与传统的类或结构类型不同,在传统的类或结构类型中,主构造函数参数可以在整个类定义中访问,而记录被设计为透明的数据容器。他们本质上支持基于价值的平等,这与他们作为数据持有者的预期角色相一致。因此,它们的主要构造函数参数可以作为属性访问是合乎逻辑的。
重构示例
.NET 提供了许多模板,如果你曾经创建过 Worker Service,你可能已经看到过以下 Worker 类模板代码:
namespace Example.Worker.Service
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
}
前面的代码是一个简单的 Worker 服务,每秒记录一条消息。目前,Worker 类有一个构造函数,需要一个 ILogger<Worker> 实例作为参数,并将其分配给相同类型的只读字段。此类型信息位于两个位置,即构造函数的定义中,以及字段本身。这是 C# 代码中的常见模式,但可以使用主构造函数进行简化。
值得一提的是,Visual Studio Code 中不提供此特定功能的重构工具,但你仍然可以手动重构主构造函数。要在 Visual Studio 中使用主构造函数重构此代码,可以使用“使用主构造函数(并删除字段)”重构选项。右键单击 Worker 构造函数,选择“快速操作和重构...”(或按 Ctrl + .),然后选择“使用主构造函数”(并删除字段)。
现在生成的代码类似于以下 C# 代码:
namespace Example.Worker.Service
{
public class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
}
就这样,你已经成功重构了 Worker 类以使用主构造函数! ILogger<Worker> 字段已被删除,并且构造函数已替换为主构造函数。这使得代码更加简洁,更容易阅读。记录器实例现在在整个类中可用(因为它在范围内),而不需要单独的字段声明。
其他注意事项
主构造函数可以删除在构造函数中分配的手写字段声明,但有一个警告。如果你将字段定义为只读,那么它们在功能上并不完全等效,因为非记录类型的主构造函数参数是可变的。因此,当你使用这种重构方法时,请注意你正在更改代码的语义。如果要保持只读行为,请就地使用字段声明并使用主构造函数参数分配该字段:
namespace Example.Worker.Service;
public class Worker(ILogger<Worker> logger) : BackgroundService
{
private readonly ILogger<Worker> _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
额外的构造函数
当你定义主构造函数时,你仍然可以定义其他构造函数。然而,这些构造函数是必需的;调用主构造函数。调用主构造函数可确保主构造函数参数在类声明中的所有位置进行初始化。如果需要定义其他构造函数,则必须使用 this 关键字调用主构造函数。
namespace Example.Worker.Service
{
// Primary constructor
public class Worker(ILogger<Worker> logger) : BackgroundService
{
private readonly int _delayDuration = 1_000;
// Secondary constructor, calling the primary constructor
public Worker(ILogger<Worker> logger, int delayDuration) : this(logger)
{
_delayDuration = delayDuration;
}
// Omitted for brevity...
}
}
并不总是需要额外的构造函数。让我们进行一些额外的重构以包含一些其他功能!
奖励重构
主构造函数非常棒,但是我们还可以做更多的事情来改进代码。
C# 包含文件范围的命名空间。它们是一个非常好的功能,可以减少嵌套级别并提高可读性。继续前面的示例,将光标放在命名空间名称的末尾,然后按 ;键(Visual Studio Code 不支持此操作,但你也可以手动执行此操作)。这会将命名空间转换为文件范围的命名空间。
经过一些额外的编辑,最终的重构代码如下:
namespace Example.Worker.Service;
public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1_000, stoppingToken);
}
}
}
除了重构文件范围的命名空间之外,我还添加了 seal 修饰符,因为在多种情况下都有性能优势。最后,我还使用数字分隔符功能更新了传递到 Task.Delay 的数字文字,以提高可读性。你知道还有很多方法可以简化你的代码吗?查看 C# 中的新增功能以了解更多信息!