PAYATHON 2026

C# 中如何在复杂的类组合层次中管理和传递大量参数?

技术老齐

建议保留构造函数注入,把配置数据和对象组装分开处理:用含义明确的配置对象组织参数,由工厂或组合根(Composition Root)创建底层对象,再将它们传给上层类。

用户仍然可以在入口处设置某个 Gear 的半径,Car 的构造函数只需接收它直接依赖的 Gearbox。这样,底层参数就不必经过每一层业务类的构造函数。

为什么会出现逐层传参

当 LevelOne 创建 LevelTwo,LevelTwo 又创建 LevelThree 时,每一层都负责组装下层对象,因此需要知道创建下层对象所需的参数。

对象之间存在组合关系,并不意味着它们必须负责创建彼此。一个对象可以持有另一个对象,由外部代码完成创建并传入:

var gear = new Gear(radiusMm);
var gearbox = new Gearbox(gear);
var car = new Car(gearbox);

Car 只需要知道自己依赖 Gearbox,无须了解齿轮半径。这些细节由负责组装对象的代码处理。

从 LevelOne 到 LevelTen 也是如此:先创建 LevelTen,再逐步组装上层对象。参数只出现在使用它的对象和相应的创建代码中。

三种方案怎么选

方案主要影响建议
全局静态字段隐藏依赖,共享可变状态会影响不同任务、窗口和测试不适合作为用户参数的默认存储方式
通过 GetInstance() 访问单例仍然依赖全局访问入口,难以同时使用不同配置不适合保存每次分析任务的参数
构造函数注入依赖明确,便于独立创建和测试对象保留,并调整参数分组和对象组装职责

用户可以修改的参数不适合命名为 Constants。const 表示编译期常量,static readonly 也不会自动解决全局依赖问题。固定的常量可以独立定义,用户设置则需要明确作用范围。

“单例生命周期”和“全局访问”也需要区分。通过构造函数注入共享实例,可以明确表达依赖;各个类自行调用 Settings.GetInstance(),则会隐藏依赖。即使使用依赖注入容器,也要根据配置属于整个应用、某个文档还是某次任务,决定它的生命周期。

先按用途整理参数

可以按领域含义定义 GearSettings、GearboxSettings、CarSettings 等类型。科学分析软件中也可以有 SolverSettings、CalibrationSettings。

分组要对应实际用途。把十五个互不相关的参数塞进一个 Context,只是缩短了签名,理解和维护起来仍然困难。

配置树也不必与对象树逐层对应。配置描述用户需要控制的内容,运行时对象还可能包含缓存、策略和辅助对象,这些细节通常无须暴露给用户。

处理流程可以按以下步骤安排:

  1. 从界面或配置文件收集设置,检查必填项、范围和单位。
  2. 由工厂读取设置,创建相关对象。
  3. 让业务对象通过构造函数接收自己的直接依赖。
  4. 在任务开始时确定本次使用的参数,避免界面在计算过程中修改这些参数。

C# 示例:配置齿轮半径,再创建 Car

配置类允许界面修改设置,创建后的领域对象通过只读属性保存状态。这个示例只有一个目标齿轮,实际项目可以按齿轮编号或用途扩展。

using System;

public sealed class GearSettings
{
    public double RadiusMm { get; set; } = 25.0;
}

public sealed class GearboxSettings
{
    public GearSettings TargetGear { get; set; } = new GearSettings();
}

public sealed class CarSettings
{
    public GearboxSettings Gearbox { get; set; } = new GearboxSettings();
}

public sealed class Gear
{
    public double RadiusMm { get; }

    public Gear(double radiusMm)
    {
        if (double.IsNaN(radiusMm) ||
            double.IsInfinity(radiusMm) ||
            radiusMm <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(radiusMm), "齿轮半径必须是有限的正数,单位为毫米。");
        }

        RadiusMm = radiusMm;
    }
}

public sealed class Gearbox
{
    public Gear TargetGear { get; }

    public Gearbox(Gear targetGear)
    {
        TargetGear = targetGear
            ?? throw new ArgumentNullException(nameof(targetGear));
    }
}

public sealed class Car
{
    public Gearbox Gearbox { get; }

    public Car(Gearbox gearbox)
    {
        Gearbox = gearbox
            ?? throw new ArgumentNullException(nameof(gearbox));
    }
}

public static class CarFactory
{
    public static Car Create(CarSettings settings)
    {
        if (settings == null)
            throw new ArgumentNullException(nameof(settings));

        var gearboxSettings = settings.Gearbox;
        if (gearboxSettings == null)
            throw new ArgumentException("缺少变速箱配置。", nameof(settings));

        var gearSettings = gearboxSettings.TargetGear;
        if (gearSettings == null)
            throw new ArgumentException("缺少目标齿轮配置。", nameof(settings));

        var gear = new Gear(gearSettings.RadiusMm);
        var gearbox = new Gearbox(gear);

        return new Car(gearbox);
    }
}

在入口处收集用户输入,再创建对象:

var settings = new CarSettings();
settings.Gearbox.TargetGear.RadiusMm = 32.5;

Car car = CarFactory.Create(settings);

Car 的构造函数不需要齿轮半径参数,Gearbox 也无须转发这个值。CarFactory 负责读取配置和组装对象,Gear 自己校验半径。

CarFactory 是静态类,但没有保存可变的全局配置。每次调用都显式传入设置,因此可以创建参数不同的多个 Car。

层次很深时,拆分创建逻辑

对象数量增加后,可以让 CarFactory 调用 GearboxFactory,再由后者调用更细的工厂。每个工厂只接收自己需要的配置分支,例如:

Gearbox gearbox = gearboxFactory.Create(settings.Gearbox);
Car car = new Car(gearbox);

配置在创建代码之间传递,无须穿过每一层业务对象。顶层工厂可以只协调几个子工厂,将具体的创建细节交给它们处理。

依赖注入容器可以管理服务依赖和对象生命周期,参数如何分组仍需自行设计。对于用户输入、任务参数和运行时才确定的数据,使用显式工厂往往更容易表达。业务类如果为了减少构造函数参数而自行从 IServiceProvider 获取依赖,就会形成 Service Locator,再次隐藏依赖关系。

构造过程需要分阶段选择组件或填写参数时,可以考虑 Builder。一次调用就能创建完整对象的情况,普通工厂通常已经够用。

构造函数有十五个参数,是否一定要拆类

如果大部分参数只是继续传给子对象,应当调整组装职责;属于同一组算法设置的参数,可以用专门的参数对象组织。一个类直接依赖十五个独立服务时,则值得检查它是否承担了过多职责。

参数数量只能提示问题,不能单独决定设计。科学计算算法可能确实需要较多控制参数。这时应明确每个参数的含义、单位、默认值和有效范围,无须为了缩短签名强行拆类。

用户在运行中修改参数怎么办

示例中的 Gear 复制了半径值,因此创建完成后,继续修改 settings 不会改变已有齿轮。科学分析任务通常适合这种做法:一次计算使用固定配置,并将配置与结果一起保存,便于复现。

参数需要动态生效时,应明确何时生效,以及如何更新:

  • 下次任务生效:保存新设置,在启动下一次任务时创建运行对象。
  • 当前对象立即生效:提供有业务含义的更新方法,并处理校验、缓存失效和线程同步。
  • 多个参数必须一起改变:先校验完整的新配置,再统一切换,避免任务读到修改到一半的状态。

后台计算期间,不应让任务无约束地共享读取界面中的可变设置。配置项较多时,可以在任务边界生成独立的不可变快照。只读属性本身并不能保证它引用的集合或子对象也不可变。

备注:内容仅供参考。