C# 12 在 .NET 8 中引入了一系列令人兴奋的新功能!本文将探讨其中的一项特性——主构造函数(Primary Constructors),解释其用法和重要性。接着,我们将通过一个示例重构来展示如何在实际代码中应用这个特性,讨论其优点和潜在风险。这将帮助您理解这个变化的影响,并决定是否采纳该功能。
主构造函数 1️⃣
对于 C# 开发者来说,主构造函数是日常开发中的一个实用特性。它们允许你在单个简洁的声明中定义类( class
)或结构体( struct
)及其构造函数,从而减少冗余代码。如果您跟进了 C# 的版本更新,可能已经对记录类型( record
)有所了解,它们就是使用主构造函数的第一个实例。
与记录类型的区别
记录类型作为 class
或 struct
的一种简化类型修饰符,用于构建像数据容器这样的简单类。记录类型可以包含主构造函数,它不仅会生成一个后台字段,还会为每个参数提供一个公共属性。与传统的类或结构体不同,记录类型的主构造函数参数在整个类定义中都是可访问的,但设计初衷是作为透明的数据容器。它们天然支持基于值的相等性,符合数据持有者的角色。因此,将主构造函数参数作为属性暴露是合理的。
重构示例 ✨
.NET 提供了许多模板,如果您创建过工作服务,可能已经见过以下 Worker
类的模板代码:
namespace Example.Worker.Service
{
public class Worker : BackgroundService
{
private readonly ILogger _logger;
public Worker(ILogger 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
实例作为参数的构造函数,并将其赋值给同类型的 readonly
字段。这种类型信息在构造函数定义和字段本身都有体现,这是 C# 代码中的常见模式,但可以通过主构造函数进行简化。
值得注意的是,Visual Studio Code 目前并没有针对这个特定特性的重构工具,但你可以手动进行重构。在 Visual Studio 中,右键点击 Worker
构造函数,选择“快速操作和重构…”(或者按 + ),然后选择“使用主构造函数(并移除字段)”。
重构后的代码看起来像这样:
namespace Example.Worker.Service
{
public class Worker(ILogger 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
字段已被移除,构造函数被替换为主构造函数,使代码更简洁、易读。logger
实例现在在整个类中可见(因为其作用域),无需单独声明字段。
其他考虑 🤔
主构造函数可以移除手写字段声明,但在某种程度上并不完全等效,特别是当您将字段定义为 readonly
时。对于非记录类型的主构造函数参数,它们是可变的。因此,在使用这种重构方法时,请注意代码的行为变化。如果想保持 readonly
行为,可以在主构造函数参数位置上使用字段声明:
namespace Example.Worker.Service
{
public class Worker(ILogger logger) : BackgroundService
{
private readonly ILogger _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
{
// 主构造函数
public class Worker(ILogger logger) : BackgroundService
{
private readonly int _delayDuration = 1_000;
// 第二个构造函数,调用主构造函数
public Worker(ILogger logger, int delayDuration) : this(logger)
{
_delayDuration = delayDuration;
}
// 为了简洁略去...
}
}
通常情况下,您不一定需要额外的构造函数。让我们来进行一些额外的重构,包括其他一些功能!
奖励重构 🎉
主构造函数确实很棒,但我们还能做更多来优化代码。
C# 支持文件范围的命名空间,这是一个能减少嵌套层次、提高可读性的出色功能。在之前的例子中,将光标放在命名空间名称的末尾,然后按下键(这在 Visual Studio Code 中暂不支持,但您可以手动完成)。这将把命名空间转换为文件范围的命名空间。
经过一些额外编辑,最终重构后的代码如下:
namespace Example.Worker.Service;
public sealed class Worker(ILogger 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);
}
}
}
除了重构为文件范围的命名空间外,我还添加了 sealed
修饰符,因为这在某些情况下有性能优势。最后,我还更新了传递给 Task.Delay
的数字字面量,使用了分隔符,以提高可读性。
译自:https://devblogs.microsoft.com/dotnet/csharp-primary-constructors-refactoring/
Comments