C#12新功能合集一:使用主构造函数重构代码

Amy Peng

本文翻译于David Pine的这篇文章:Refactor your C# code with primary constructors

作为 .NET 8 一部分的 C# 12 引入了一组引人注目的新功能! 在这篇文章中,我们将探讨其中一个功能,特别是主构造函数,解释其用法和相关性。 然后,我们将演示一个重构示例,以展示如何将其应用到您的代码中,并讨论其好处和潜在的缺陷。 这将帮助您了解这一更改的影响并有助于您决定是否采用该功能 

主构造函数1️ 

主构造函数被认为是一项“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# 中的新增功能以了解更多信息 

后续步骤🚀 

在您自己的代码中尝试一下! 寻找机会使用主构造函数重构代码,并了解它如何简化您的代码库。 如果您使用的是 Visual Studio,请查看重构工具。 如果您使用的是 Visual Studio Code,您可以手动重构。 若要了解更多信息,请浏览以下资源 

如果您有任何技术问题,欢迎来Microsoft Q&A 提问。

0 comments

Leave a comment

Feedback usabilla icon