C# 构造函数重载到 JS 单一 constructor:分派机制详解

C# 多个构造函数 → JS 只有一个 constructor:签名分派机制详解

把构造函数签名的哈希值作为 JS 调用的第一个参数——这是一种编译期确定的精确分派,不依赖参数个数或运行时类型。

一、问题:C# 里你可以写 5 个构造函数,JS 里只能有 1 个

C# 的构造函数重载是编译期决定的:

class Person
{
    public Person() { }
    public Person(string name) { }
    public Person(string name, int age) { }
}

每个 new Person(...) 调哪个重载在编译期就已经确定了。但 JavaScript 的 class 只有一个 constructor

Jazor 的策略:把构造函数签名的稳定标识作为 JS 调用的第一个参数,在分派器里精确匹配。

二、调用约定:签名哈希作为第一个参数

Jazor 为每个构造函数重载生成一个 helper 名——基于签名的稳定哈希,比如 $ctor_3a2f。在调用 new Person("Alice") 时,编译端知道该调哪个重载,于是生成的 JS 调用变成:

new Person("$ctor_7b1e", "Alice")

"$ctor_7b1e" 就是 Person(string name) 这个签名的 helper 名。它是第一个参数。

三、分派器内部:读取 arguments[0] 做精确匹配

生成的 constructor 长这样:

class Person {
    constructor() {
        let $args = arguments;
        let $ctor = $args[0];          // 取出签名标识

        // 按签名精确匹配
        if ($ctor === "$ctor_3a2f") {
            // Person() — 无参
            return this.$ctor_3a2f();
        }

        if ($ctor === "$ctor_7b1e") {
            let name = $args[1];       // 从 arguments 中解出真实参数
            super(name);               // 如果有基类构造器
            return this.$ctor_7b1e(name);
        }

        if ($ctor === "$ctor_9c4d") {
            let name = $args[1], age = $args[2];
            return this.$ctor_9c4d(name, age);
        }

        throw new Error("No matching constructor overload for Person.");
    }

    $ctor_3a2f() { /* Person() 的逻辑 */ }
    $ctor_7b1e(name) { /* Person(string name) 的逻辑 */ }
    $ctor_9c4d(name, age) { /* Person(string name, int age) 的逻辑 */ }
}

关键几步:

  1. arguments 被缓存到 $args,再从中取出 $args[0] 作为 $ctor
  2. $ctor 跟每个 helper 名做严格相等比较(===。因为 helper 名是编译期确定的稳定字符串,匹配无歧义。
  3. 匹配命中后,BuildConstructorDispatcherParameterBindings$args[1]$args[2]……依次解出并绑定到参数名——带默认值补齐。
  4. 调用 this.$ctor_<hash>(...) 执行真正的构造逻辑,然后 return

四、参数绑定:从 arguments 解包到具名变量

分派器不只是 if/else 链。每条分支还需要把 arguments 里的匿名参数解包到 helper 方法的具名参数。这是 BuildConstructorDispatcherParameterBindings 做的事:

private IEnumerable<Statement> BuildConstructorDispatcherParameterBindings(
    MemberConstructorLowering lowering,
    Identifier argsIdentifier)
{
    // 对于构造函数的每个参数:
    //   let paramName = $args[index] ?? defaultValue;
    //  声明成 VariableDeclaration
}

如果某个参数有默认值,在这里补齐:

let name = $args[1];                     // 必选参数
let timeout = $args[2] !== undefined ? $args[2] : 30;  // 带默认值

这样分派器逻辑保持简洁——不做类型检查,只做身份匹配和参数解包。

五、super(...) 的签名传递

派生类中 : base(name) 对应的 super() 调用也需要传入基类构造器的签名 key:

class Student : Person
{
    public Student(string name, int grade) : base(name) { }
}

如果基类 Person 也有多个构造函数,Jazor 生成的 super() 调用会在参数列表前面插入基类对应 helper 的签名:

// 生成在 $ctor_d5a8 helper 里:
$ctor_d5a8(name, grade) {
    super("$ctor_7b1e", name);    // "base(name)" 变成 super("$ctor_7b1e", name)
    // Student 自己的逻辑
}
// 对应的逻辑在 CreateConstructorInitializerArguments 里:
if (baseConstructorSymbol is not null &&
    HasMultipleExplicitConstructors(baseConstructorSymbol.ContainingType))
    arguments.Insert(0, new StringLiteral(
        GetMemberConstructorHelperName(baseConstructorSymbol), ...));

只有当基类有多个显式构造函数时,才插入签名 key。 如果基类只有一个构造函数(或隐式声明的),不需要签名分派,super() 直接传参即可。

六、与旧版参数个数分派的对比

早期版本用 arguments.length 分派:

// 旧版:按参数个数分派
if (arguments.length === 0) return this.$ctor_3a2f();
if (arguments.length === 1) return this.$ctor_7b1e(arguments[0]);
if (arguments.length === 2) return this.$ctor_9c4d(arguments[0], arguments[1]);

新版签名分派解决了旧版的三个问题:

场景 旧版(arguments.length 新版(签名 key)
同参数个数的不同重载 无法区分,直接报错 $ctor key 不同,精确分派
带默认参数 传 0 个 vs 传 1 个参数分到不同分支 签名 key 唯一,默认值在 helper 内补齐
基类多构造函数 super() 传参个数可能模糊 super("$ctor_key", ...) 精确分派

七、仍然拒绝的:ref/out 参数和 this(...)

两条旧约束在新版里同样存在:

  1. ref/out/in/params 参数BuildConstructorDispatcherParameterBindings 依赖"每个参数对应 arguments[n+1] 的一个位置"。ref/out 需要包装数组,params 会吞掉可变数量的参数——这些都会破坏分派器的参数解包逻辑。

  2. this(...) 构造函数链this("default") 等价于"在当前类的分派器里再走一次分派",这在语义上意味着 constructor 需要递归调用自己。Jazor 选择直接拒绝。

八、小结

Jazor 当前版本的构造函数分派策略:

编译期:为每个重载生成稳定哈希 → helper 名 $ctor_<hash>
调用点:new Foo("$ctor_<hash>", arg1, arg2, ...)
分派器:读 arguments[0] → === 匹配辅助名 → 解包 arguments[1..n] → 调用 this.$ctor_<hash>
基类:super("$ctor_baseHash", arg1, arg2, ...)

这个策略的核心优点是:分派完全由编译期信息驱动,不依赖运行时类型检查。 签名 key 是编译期算出来的稳定字符串,=== 比较无歧义。同一个重载在不同编译运行中产出的 JS 代码完全一致——因为哈希是签名内容的确定性函数。

项目地址:https://github.com/devhxj/Jazor

posted @ 2026-05-11 15:16  形而下晓寒  阅读(12)  评论(0)    收藏  举报