news 2026/9/11 21:17:43

Carbon 函数、函数类型与函数调用规范解析:从 `fn` 声明到 `Call` 接口的统一调用模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Carbon 函数、函数类型与函数调用规范解析:从 `fn` 声明到 `Call` 接口的统一调用模型

Carbon 函数、函数类型与函数调用规范解析:从fn声明到Call接口的统一调用模型

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

导读

本篇基于 Carbon Language 官方设计提案 proposals/p002875-functions-function-types-and-function-calls.md,完整讲解 Carbon 中函数声明的类型语义、函数调用语法、直接调用(direct call)的参数推导流程,以及以Call接口为核心的间接调用/可调用对象(callable)统一模型。通过阅读本文,你将掌握:函数为什么是"无状态且类型唯一"的一等公民、绑定方法(bound method)的类型结构、泛型参数如何通过Call约束为可调用类型、以及用户类如何重载函数调用运算符。文中所有关键语义均结合当前仓库中的编译器实现(toolchain/check/call.cpptoolchain/sem_ir/)与预置测试进行交叉印证,适合希望深入理解 Carbon 语言语义或参与工具链开发的读者。

背景:为什么要规范函数与函数调用

函数是 Carbon 定义执行语义的基本构件(primitive building block),但长期以来仓库缺少对"函数是什么、函数调用如何工作"的正式规范。该提案(对应 Pull Request #2875)要回答几个悬而未决的问题:

  • Carbon 是否拥有一等公民(first-class)的函数类型?
  • 类型如何重载函数调用这一操作?
  • 可调用实体(callable entity)如何归类——例如参数化类名Vector这样的实体,究竟是函数还是别的什么?

该提案假定读者已经理解 C/C++ 中函数与函数指针的工作方式,在此认知基础上给出 Carbon 自己的答案。值得注意的是,提案写作时的诸多设计决策后来已在当前仓库的编译器中落地,本文会逐一给出对应的源码位置加以印证。

提案总览:函数声明引入唯一无状态类型

提案的核心主张可以概括为两条:

  1. 函数声明引入一个新且唯一的无状态类型(stateless type),称为函数类型(function type)。函数名被绑定为该函数类型的一个值。
  2. 函数调用采用类 C 的语法:一个命名可调用实体的表达式后跟一个形似元组的参数列表。

提案将可调用实体划分为以下几类:

  • 函数,以及更一般的函数类型的值;
  • 绑定方法,例如my_vector.Begin
  • Lambda(待 Carbon 引入之后);
  • 参数化实体,例如泛型类Vector或泛型接口AddWith
  • 被约束为可调用的依赖类型的值;
  • 用户自定义的重载函数调用语法的类类型。

在前四类情况下,先使用**参数推导(argument deduction)确定被推导参数的值,再使用模式匹配(pattern matching)**把参数模式绑定到实参值。而在其余情况下,则由内置的Call接口决定调用语义:调用F(args)被改写(desugar)为F.(Call(ArgTypes).Op)(args)。注意这个改写结果本身又是一个绑定成员函数调用,而Call(ArgTypes)是对参数化实体的调用,因此该改写只执行一次。

函数与函数类型

声明即引入新类型

看一个典型的泛型函数声明:

fn FT:! type -> T { return x; }

它引入一个名为F的值,其类型是唯一的函数类型。不同的函数拥有不同的函数类型,即使它们的签名完全相同。函数类型是空且平凡(empty, trivial)的类型,除了询问函数值的类型之外,没有其他方式可以命名一个函数类型。

函数值是一等公民

函数值是普通的(regular)值,可以存入变量、作为参数传递给其他函数:

// Compile-time function. fn TypeOfT:! type -> type { return T; } // ✅ `F` is a first-class value with a first-class type. let template FType:! type = TypeOf(F); var my_f: FType = F;

这里借助编译期函数TypeOf将函数F的类型捕获到FType,随后my_f就能以该函数类型存储F。在另一个函数里可以直接调用它:

fn G() -> i32 { // ✅ `my_f` has function type `FType`. This is a direct call to `F`. return my_f(1); }

由于函数类型是唯一的,把函数作为泛型参数传递时,泛型不会因签名相同而混淆两个不同函数:

fn Sorttemplate F:! type, T:! type*, f: F); fn Compare(a: i32, b: i32) -> Ordering { return a.(Ordered.Compare)(b); } fn SortInts(v: Vector(i32)*) { Sort(v, Compare); }

孤儿规则中的归属

就孤儿规则(orphan rule)而言,函数类型被视为由引入该函数值的函数声明所声明(declared)。也就是说,函数类型在孤儿规则中的"归属方"是它对应的函数声明,而非其所在的类或包。

绑定方法(Bound Methods)

对每个与方法对应的函数类型,还存在一个对应的绑定方法类型(bound method type)。当对类类型的对象执行成员访问以取得某个方法时,得到的是一个绑定方法值(bound method value),其类型即绑定方法类型。

语义上:

  • 绑定方法类型描述的是方法调用中的被调用方(callee);
  • 绑定方法值则描述该调用中self参数的具体取值。

示例:

class HasMember { // `HasMember.F` has a stateless function type, with signature // `self: Self -> i32`. fn Fself: Self -> i32; } fn F(h1: HasMember, h2: HasMember) -> i32 { // ✅ `h1.F` is a bound method value whose type is a bound method type, // with signature `(n: i32) -> i32`. var hf: auto = h1.F; // ✅ `h1.F` and `h2.F` are of the same bound method type. hf = h2.F; // ✅ Same as `h2.F(4)`. return hf(4); }

注意两点:h1.Fh2.F属于同一种绑定方法类型(因此可以互相赋值);对该绑定方法值调用hf(4)等价于h2.F(4),即self已绑定为h2

仓库实现印证:编译器在语义 IR 层面为绑定方法定义了专门的指令种类与单例类型,见 toolchain/sem_ir/inst_kind.def 中的BoundMethodBoundMethodType,以及 toolchain/sem_ir/typed_insts.h 中的结构体BoundMethod(持有object_idfunction_decl_id)。在检查阶段,toolchain/sem_ir/function.cpp 的TryGetCalleeAsBoundMethod会从被调用表达式中解包出绑定方法并取出其object_id(即self对象)与function_decl_id,随后调用逻辑据此把self参数重新接回调用——这与提案中"绑定方法值描述self参数"的语义完全一致。

调用语法(Call Syntax)

调用形如a(b, c, d)a(b, c, d,),其中:

  • a是被调用方(callee),可以是一个名字、字面量、成员访问,或是用括号包裹的更复杂表达式;
  • bcd是任意数量的实参表达式,用逗号分隔;如果实参列表非空,末尾可选地允许一个(但不要求)尾随逗号。

调用语法在句法上等价于"一个主表达式后跟一个元组字面量"。二者的区别在于:元组字面量必须带尾随逗号才能形成单元素元组(b,);而调用语法中a(b)a(b,)都是合法的。

直接调用(Direct Calls)

判定条件与检查流程

当被调用方满足以下任一条件时,该调用表达式就是直接调用(direct call)

  • 是一个参数化实体的名字,例如泛型类或泛型接口;
  • 具有函数类型或绑定方法类型。

直接调用存在一个调用签名(call signature),用于把给定实参与被调用方声明的隐式参数(implicit parameters)和显式参数(explicit parameters)进行核对,流程如下:

  1. 参数推导:将声明的参数类型与实际实参类型比较,推导出让类型相等的隐式参数值;
  2. 然后按顺序处理显式参数列表中的每个绑定(binding),把所有已推导的参数值代入该参数:
    • 若参数是template :!绑定,实参表达式被转换为与绑定同类型、同模板常量表达式(template constant expression)阶段;
    • 若参数是符号化:!绑定,实参表达式被转换为与绑定同类型、同符号常量表达式(symbolic constant expression)阶段;
    • 否则,参数与实参进行模式匹配。
  3. 如果某参数是:!绑定,则其对应的转换后实参表达式会被求值,其值会在处理后续参数之前加入已推导参数值列表。

仓库实现印证:该流程在 toolchain/check/call.cpp 中有着几乎逐条对应的实现:

  • ResolveCalleeInCall(call.cpp)首先校验实参数目(arity)是否与显式参数个数一致,不一致时报CallArgCountMismatch诊断;随后当实体是泛型时调用DeduceGenericCallArguments(声明见 toolchain/check/deduce.h)执行参数推导;
  • 转换实参、匹配参数的工作由ConvertCallArgs完成(见PerformCallToFunction中对它的调用,call.cpp)。

调用表达式的结果类型

调用表达式的结果取决于被调用方的种类:

  • 若被调用方是参数化实体(如泛型类或泛型接口),结果是对应的具体实例(某个类或接口),整个调用是一个类型为type的值表达式;
  • 若被调用方是函数值,调用是一个初始化表达式(initializing expression),其类型为函数替换后的返回类型;求值时调用函数并产生其返回值;
  • 若被调用方是绑定方法值,行为与函数值相同,唯一区别是所调用函数的self参数被绑定为绑定方法值中的self值。

仓库实现印证ResolveCalleeInCall内部使用EntityKind枚举区分四类被调用实体:FunctionGenericClassGenericInterfaceGenericNamedConstraint(call.cpp)。四种情形各有专门的处理函数:

  • 泛型类调用Vector(i32)PerformCallToGenericClass,产出ClassType(call.cpp);
  • 泛型接口/命名约束调用走PerformCallToGenericInterfaceOrNamedConstaint,产出 facet 类型(call.cpp);
  • 函数调用走PerformCallToFunction,推导出SpecificFunctionSpecificImplFunction作为具体化后的被调用方,并最终构造Call指令(call.cpp);
  • 非函数的被调用方(含泛型类型、C++ 模板名等)走PerformCallToNonFunction,对不可调用值报CallToNonCallable诊断(call.cpp)。

PerformCall的统一入口会先尝试把被调用方视为函数,再回退到非函数路径(PerformCallHelper,call.cpp),与提案"先做直接调用判定、再做改写"的设计顺序一致。

泛型可调用参数与Call接口

泛型参数可以用Call接口约束为"可调用类型":

interface Call(Args: type) { let Result:! type; fn Opself: Self -> Result; }

TODOCall应当是可变的(variadic)。目前先把它建模为接收一个元组类型,并把Call.Op建模为接收一个元组值。

例如,排序函数可以这样约束比较器参数:

fn Sort[T:! type, F:! Call((T, T)) where .Result = Ordering] (v: Vector(T)*, cmp: F) {

一个非直接调用(non-direct call)表达式会被改写为对Call(Args).Op的调用,其中Args是调用实参元组的类型:

// In Sort... auto ord: auto = cmp((*v)[i], (*v)[j]); // ... is translated into ... auto ord: auto = cmp.(Call((T, T)).Op)((*v)[i], (*v)[j]);

设计上有两个关键约束值得注意:

  • 调用实参的类型被建模为接口参数(interface parameter),这使得Call接口能够建模函数重载;
  • 返回类型是关联类型(associated type)而非参数——Carbon不允许按返回类型重载,也不希望类型信息从调用表达式出现的上下文向内传播到调用本身。

函数类型与Call的关系

函数类型对Call的自动实现

每个函数类型或绑定方法类型,对"直接调用该函数/绑定方法可接受的所有运行时实参类型集合"实现Call接口。Call.Op的行为就是使用给定实参列表去调用该函数或绑定方法。

Select为例,编译器相当于为其生成了下面的实现:

fn SelectT:! type -> T { return if b then x else y; } // Generated: impl forall [T:! type, BType:! ImplicitAs(bool)] Select as Call((BType, T, T)) where .Result = T { fn Opself: Self) { let (b: bool, x: T, y: T) = args; return Select(b, x, y); } }

对于不涉及推导参数的类型,允许隐式转换。其意图是让impl在函数支持直接调用的同样情形下支持间接调用,且语义一致。例如i64函数可以被当作接受i32的可调用对象传入:

fn TakeI32FnF:! Call(i32); fn I64Fn(n: i64); fn Run() { // ✅ `I64Fn` can be called with an `i32`, because // `i32 impls ImplicitAs(i64)`. TakeI32Fn(I64Fn); }

局限与边界情形

通过Call接口发起的调用,实参一律作为值表达式(value expressions),调用本身一律是初始化表达式(initializing expression)。如果函数用var接收参数,或未来语言机制允许直接函数调用不是初始化表达式,就需要额外的转换;在这些转换不可行时(例如实参或返回值不可拷贝),Call接口被视为未被实现。提案预期在未来提案中重新审视这些约束,把函数调用接口扩展为与fn声明同等的一般性。

另外,Call接口只建模"给定参数类型的任意运行时值都能传入"的函数调用。只要函数签名的显式实参列表中含编译期参数,或者调用实参与函数参数之间存在可反驳的模式匹配(refutable pattern matching),该函数类型就不会实现Call

fn RuntimeT:! type; fn CompileTime(T:! type, x: T); // 🤷 Undecided whether this is valid. fn RefutablePattern(1 as i32); fn Run() { // ✅ Calls `Runtime(0)`. Runtime.(Call(i32).Op)(0); // ❌ Can't call `CompileTime` this way, it can't implement `Call(type, i32)` // because the type would be passed at runtime. CompileTime.(Call(type, i32).Op)(i32, 0); // ❌ Can't call `RefutablePattern` this way, it can't implement `Call(i32)` // for arbitrary i32 arguments. RefutablePattern.(Call(i32).Op)(0); }

重载调用运算符(Overloaded Call Operator)

用户可以通过为类型实现Call接口来重载函数调用运算符的含义:

class Func(Arg:! type) { impl as Call((Arg,)) where .Result = () { fn Opself: Self) { Print("hello, world"); } } } fn Run() { let f: Func(i32) = {}; // ✅ Prints "hello, world". f(42); }

对被调用方的类型没有任何额外约束,只需满足实现接口的常规约束即可。下面的例子甚至展示了为"单字段结构体字面量"实现调用运算符——合法但明显不推荐:

class X { var n: i32; } // ✅ OK, but inadvisable. impl {.a: X} as Call(()) where .Result = i32 { fn Opself: Self) -> i32 { return self.a.n; } } fn Run() -> i32 { // Returns 1. return {.a = {.n = 1} as X}(); }

编译期参数的绕行方案

Carbon 无法直接定义"接收编译期参数"的函数式可调用类类型——这与 C++ 中无法为类类型的值x定义operator()使x<T>()在编译期把T传给operator()的情形类似。但可以通过把编译期值放入实参的类型中来绕行:

class Wrap(T:! type) {} class Callable { impl forall [T:! Printable] as Call((Wrap(T), T)) where .Result = () { fn Opself: Self, T)) { let (_: auto, v: T) = args; Print(v); } } } fn CallItF:! Call(Wrap(i32), i32) { f({} as Wrap(i32), 0); } fn Run() { CallIt({} as Callable); }

设计依据(Rationale)

提案所依据的项目原则与目标如下:

原则层面

  • 原则:唯一的静态开放扩展机制(one static open extension mechanism):重载调用通过实现接口来支持;
  • 原则:倾向为同一件事只提供一种方式(one way):把可调用对象传给泛型函数只有一种显然的方式,并且无论实参是函数、可调用对象还是(未来的)lambda,它都能高效工作。

目标层面

  • 性能关键的软件(Performance-critical software):函数传递的效率不低于可调用对象的传递;
  • 易读、易懂、易写的代码:C/C++ 的签名式函数类型出了名地难读,Carbon 通过不引入签名式函数类型来规避该问题;
  • 与现有 C++ 代码互操作与迁移:该设计为函数指针打下了基础(见下文未来工作),C++ 函数指针与成员函数指针都可以建模为实现了Call的值。

备选方案(Alternatives Considered)

方案一:签名式函数类型

可以让每个函数签名拥有独立类型(如 C/C++ 那样),这能为函数指针提供一个不依赖泛型、类型擦除或花哨表示优化的故事。

但其主要缺点在 C++ 中屡见不鲜:把函数传给模板泛型时,效率低于把函数对象传给同一模板。例如 C++ 中:

std::vector<int> v; bool cmp(int a, int b) { return simple_calculation(a, b); }

调用ranges::sort(v, cmp)可能远不如ranges::sort(v, [](int a, int b) { return cmp(a, b); })高效——后者是直接调用,前者传入函数指针导致间接调用。

让函数产生唯一类型,意味着对函数、lambda 与函数式对象的调用具有更相似的语义和接近的效率属性。

方案二:让直接调用与间接调用行为统一

让直接调用与间接调用完全一致在理论上更理想,但实际并不可行,原因有三:

  1. 间接调用需要被改写为另一个调用表达式——语义的根基终究必须建立在某处的一次真实函数调用之上,而不是无穷地把一个调用改写为另一个;
  2. 项目已选定接口作为唯一的静态开放扩展机制,任何通用的函数调用重载机制都必须落在interface/impl的边界之内;
  3. 若试图在impl查询中标注"每个实参是否可编译期传递",这样的查询必须能回退到"实参运行时传递",可能导致寻找匹配impl时出现指数级搜索。

未来工作(Future Work)

函数重载(Overloading)

重载在提案范围之外,但设计须为重载预留合理路径。重载函数拥有多个不同的签名(隐式与显式参数集合),在提案框架下意味着:一组重载函数引入单一的函数类型,该类型支持以不同方式被调用

重载选择可能采用"逐一检查候选,命中即止"或"检查全部再择优"两种策略,当前意图是前者。对应到本提案,一个重要后果是:重载函数集合对应的函数类型拥有Call(...)接口的多个参数化实现,具体调用所选中的实现必须遵循重载机制选出的规则。例如若按"首个匹配"选择,下面的占位语法:

overloaded fn AbsT:! Unsigned -> T { return x; } overloaded fn AbsT:! Floating -> T { return x < 0 ? -x : x }

会被合成为如下行为:

match_first { impl forall [T:! Unsigned] Abs as Call((T,)) where .Result = T { ... } impl forall [T:! Floating] Abs as Call((T,)) where .Result = T { ... } }

对于同时满足Unsigned & Floating的类型,前者被选中,与重载选择行为一致。

按表达式类别与阶段重载

值得考虑是否允许按被调用方与实参的表达式类别(expression category)与表达式阶段(expression phase)重载调用;对编译期实参,或许还希望按常量值重载。

其他语言已有先例:Rust 提供FnFnMutFnOnce区分可调用对象传入重载调用时的不同方式;C++ 允许operator()*thisconst、非const甚至&&接收。Carbon 可以仿照 Rust 提供多个Call接口处理不同self参数种类(建模方式类似下标所用的IndexWithIndirectIndexWith接口)。更通用、更宏大的方案是把调用签名作为调用接口的一部分来描述:

impl MyCallable as call(T:! type, x: T) -> T;

它可能是某种"以接口表达签名"的简写语法,也可能是一种新的一等语言原语。相关探索正在 issue #3154 中进行(本提案写作时)。

可变参数(Variadics)

待 Carbon 支持可变参数后,重载调用机制应改用可变参数,而非用元组类型近似可变参数列表。

Lambda

预期 lambda 在本提案模型下与函数行为基本一致,主要区别是 lambda 可以有状态(stateful),而函数无状态。

函数指针(Function Pointers)

出于与 C++ 互操作以及廉价存储无状态函数引用的目的,应当支持函数指针。函数指针可建模为:

alias FunctionPtr(Args:! type, Result:! type) = DynPtr(Stateless & Call(Args) where .Result = Result);

其中:

  • Stateless是描述"空、无身份概念、实例可平凡创建与销毁"类型的接口;
  • DynPtr是对给定 facet 类型执行类型擦除与运行时派发的类型;
  • DynPtr带优化:DynPtr(Stateless & I)I是恰好含一个关联函数的接口时,表示为指向该函数的指针。

仓库中的实现现状与延伸阅读

本提案的大部分核心设计已在当前仓库编译器落地,可作为继续深入阅读的入口:

  • 调用检查主逻辑:toolchain/check/call.cpp ——PerformCall/PerformCallHelper统一入口、ResolveCalleeInCall参数推导与 arity 校验、四类实体的调用处理(PerformCallToFunctionPerformCallToGenericClassPerformCallToGenericInterfaceOrNamedConstaintPerformCallToNonFunction);
  • 参数推导:toolchain/check/deduce.h(DeduceGenericCallArguments的声明);
  • 绑定方法在语义 IR 中的表示:toolchain/sem_ir/typed_insts.h 的BoundMethod/BoundMethodType,toolchain/sem_ir/inst_kind.def,以及 toolchain/sem_ir/function.cpp 的TryGetCalleeAsBoundMethod
  • 调用相关的设计文档:docs/design/generics/details.md(孤儿规则、动态类型)、docs/project/principles/static_open_extension.md、docs/project/goals.md。

需要说明的是,Carbon 语言目前仍处于实验性阶段(参见 README.md),本文描述的语义以当前仓库的实现与上述提案为准;Call的可变参数化、按表达式类别/阶段重载等能力仍在探索中,后续提案可能进一步演进。

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:16:43

QGC地面站配置PX4飞控全攻略:从连接到试飞的完整流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:15:45

2026智能汽车芯片选型:认证穿透力、工具链与产能确定性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华