☰
仓颉编程语言操作符重载完全指南:从 operator 修饰符到索引与函数调用操作符
2026/10/15 15:46:04 网站建设 项目流程
  • 文档
  • 编程语言

【免费下载链接】cangjie_docs

仓颉编程语言开发者文档。

项目地址:https://gitcode.com/Cangjie/cangjie_docs
点击查看免费下载

仓颉(Cangjie)编程语言允许开发者通过**操作符重载(Operator Overloading)**为自定义类型赋予内置操作符的能力,使class、struct、enum、interface等类型在使用+、-、[]、()等运算符时自动调用对应的操作符函数。本文以仓颉官方开发指南中 操作符重载 一章为骨架,系统讲解操作符重载的语法规则、参数约定、可重载操作符清单与边界约束,并结合附录操作符、操作符函数以及扩展、接口、mut 函数、变长参数等相关章节的源码级佐证进行纵深展开。读完本文,你将能够独立为自定义类型设计并实现一元/二元操作符、索引操作符(取值与赋值)、函数调用操作符,并规避复合赋值、重定义冲突等典型陷阱。

一、什么是操作符重载:为类型补充"原生不支持"的运算能力

每种内置类型都自带一组默认支持的运算符。例如整数类型支持+、-、*、/等算术运算,Bool支持&&、||逻辑运算,具体清单参见基本操作符章节。但当你定义一个全新的用户类型(如坐标点Point、矩阵、复数、向量)时,该类型默认并不支持任何运算符——p1 + p2这类写法会直接编译报错。

仓颉通过操作符重载解决这一问题:如果你希望为某个类型实现其默认不支持的运算符,可以定义名称等于该运算符的函数。当该类型的实例使用这个运算符时,编译器会自动调用对应的操作符函数。这一机制是仓颉"领域易扩展"能力的重要组成部分——官方在语言基础介绍中即指出,操作符重载与尾随 lambda、属性等特性一起,使开发者可以"深度定制程序的语法和语义",有利于内嵌式领域专用语言(EDSL)的构建。

例如,JArray<T>(仓颉对 Java 数组的镜像类型)正是通过[]操作符重载暴露元素访问能力的,参见跨语言互操作章节:

public operator func [](index: Int32): T public operator func [](index: Int32, value!: T): Unit

二、操作符函数的定义规则:与普通函数的五处关键差异

操作符重载的本质是定义一个"操作符函数",其定义方式与普通函数相似,但存在如下五个硬性区别(见操作符重载):

  1. 必须加operator修饰符:定义操作符函数时,需要在func关键字前面添加operator关键字;
  2. 参数个数必须匹配:参数个数需要满足对应操作符的要求(详见附录操作符);
  3. 定义位置受限:操作符函数只能定义在class、interface、struct、enum和extend中;
  4. 禁止static:操作符函数具有实例成员函数的语义,所以禁止使用static修饰符;
  5. 不能是泛型函数:操作符函数不可携带类型参数。

此外,被重载后的操作符不会改变其固有的优先级和结合性。这意味着你不能通过重载改变a + b * c中先算*再算+的既定顺序——重载只改变"如何计算",不改变"何时计算"。完整操作符优先级与结合性表见附录操作符,其中优先级数值越小、优先级越高,如[]、()为 1 级,一元!、-为 3 级,*、/为 5 级,+、-为 6 级,关系操作符为 9 级,判等操作符为 10 级。

两种定义途径

定义操作符函数有两种方式:

  1. 类型内部直接定义:对于可以直接包含函数定义的类型(包括struct、enum、class和interface),可以直接在类型定义体内写出操作符函数;
  2. 通过extend扩展添加:使用extend方式为类型添加操作符函数。对于无法直接包含函数定义的类型(指除struct、class、enum、interface之外的其他类型),以及无法改变其实现的类型(如第三方定义的struct、class、enum、interface),只能采用这种方式。

第二种途径正是扩展机制的核心能力之一。根据扩展概述,扩展可以为当前package中可见的类型添加成员函数、操作符重载函数、成员属性和接口实现。同时要注意扩展的边界:扩展不能增加成员变量、不能访问被扩展类型的private成员,扩展中的函数和属性必须拥有实现,且不能用open、override、redef修饰。扩展章节给出了一个直接添加操作符函数的示例:

extend Boo { public operator func -() { println("Overload the operator addition function.") // 添加操作符重载函数 -x } }

三、参数约定:一元、二元操作符的定义与使用

操作符函数对参数类型的约定分两种情况:

  • 一元操作符:操作符函数没有参数,对返回值的类型没有要求;
  • 二元操作符:操作符函数只有一个参数,对返回值的类型没有要求。

官方文档用一个Point类型同时演示了两者:一元-对Point实例中两个成员变量x、y取负并返回新的Point对象;二元+对两个Point实例的x、y分别求和并返回新的Point对象:

open class Point { var x: Int64 = 0 var y: Int64 = 0 public init(a: Int64, b: Int64) { x = a y = b } public operator func -(): Point { Point(-x, -y) } public operator func +(right: Point): Point { Point(this.x + right.x, this.y + right.y) } }

定义完成后,即可在Point实例上直接使用一元-与二元+:

main() { let p1 = Point(8, 24) let p2 = -p1 // p2 = Point(-8, -24) let p3 = p1 + p2 // p3 = Point(0, 0) }

注意示例中操作符函数的语义:二元操作符的左操作数即this(隐式接收者),右操作数通过唯一的显式参数传入,因此p1 + p2等价于调用p1.+(p2)。这一签名模式贯穿所有可重载二元操作符,完整签名清单见附录操作符函数,例如+对应operator func +(other: T): R,==对应operator func ==(other: T): R。

操作符重载与mut的结合

对于值类型的struct,如果需要操作符函数原地修改实例本身,可以将其声明为mut函数。仓颉的 mut 函数 章节给出了mut修饰操作符重载函数的合法示例:

struct A { public mut func f(): Unit {} // OK public mut operator func +(rhs: A): A { // OK A() } public mut static func g(): Unit {} // Error, static member functions cannot be modified with 'mut' }

需要强调的是,mut只能修饰 interface、struct 及其扩展内的实例成员函数(class 是引用类型,实例成员函数无需mut即可修改成员),且操作符函数本身禁止static,二者规则在这里是叠加成立的。

四、索引操作符[]:取值形式与赋值形式

索引操作符[]是操作符重载中最灵活也最容易出错的部分,它分为取值(let a = arr[i])和赋值(arr[i] = a)两种形式,通过是否存在特殊的命名参数value来区分。两种形式无需同时重载——可以只重载赋值形式、只重载取值形式,或两者都重载。

4.1 取值形式

  • []内的参数序列对应操作符重载的非命名参数,可以是1 个或多个,类型任意;
  • 不允许有其他命名参数;
  • 返回类型可以是任意类型。

官方示例:

class A { operator func [](arg1: Int64, arg2: String): Int64 { return 0 } } func f() { let a = A() let b: Int64 = a[1, "2"] // b == 0 }

4.2 赋值形式

  • []内的参数序列同样对应非命名参数,可以是 1 个或多个、类型任意;
  • =右侧的表达式对应操作符重载的命名参数,有且只能有一个命名参数,其名称必须是value,且不能有默认值,value本身可以是任意类型;
  • 返回类型必须是Unit类型。

官方示例:

class A { operator func [](arg1: Int64, arg2: String, value!: Int64): Unit { return } } func f() { let a = A() a[1, "2"] = 0 }

特别注意:value只是一种特殊的标记,在索引操作符赋值时并不需要使用命名参数的形式调用——a[1, "2"] = 0中的0会自动绑定到value参数上,而不是写a[1, "2", value: 0]。

约束条件:除enum外的不可变类型(immutable 类型)不支持重载索引操作符的赋值形式。这符合值语义的安全考量——不可变类型不允许原地修改。

五、函数调用操作符():让对象"可调用"

函数调用操作符()的重载函数,输入参数和返回值类型可以是任意类型,它使一个实例可以像函数一样被调用:

open class A { public init() {} public operator func ()(): Unit {} } func test1() { let a = A() // OK, A() is call the constructor of A a() // OK, a() is to call the operator () overloading function }

注意上例中两种写法的区别:A()调用的是构造函数,而a()调用的是()操作符重载函数,二者在语法上并存、由编译器按上下文区分。

5.1this/super调用限制

不能使用this或super来调用()操作符重载函数。在成员函数内,this()与super()均被解释为构造器调用,因此会报错;只有构造器上下文中的this()、super()才是合法的:

open class A { public init() {} public init(x: Int64) { this() // OK, this() calls the constructor of A } public operator func ()(): Unit {} public func foo() { this() // Error, this() calls the constructor of A super() // Error } } class B <: A { public init() { super() // OK, super() calls the constuctor of the super class } public func goo() { super() // Error } }

5.2 枚举类型的构造器优先匹配规则

对于枚举类型,当构造器形式和()操作符重载函数形式都满足某个调用时,优先匹配构造器形式:

enum E { Y | X | X(Int64) public operator func ()(p: Int64) {} public operator func ()(p: Float64) {} } main() { let e = X(1) // OK, X(1) is to call the constructor X(Int64) X(1.0) // OK, X(1.0) is to call the operator () overloading function let e1 = X e1(1) // OK, e1(1) is to call the operator () overloading function Y(1) // OK, Y(1) is to call the operator () overloading function }

逐行解读这个示例:

  • X(1)精确匹配构造器X(Int64),走构造器路径;
  • X(1.0)无法匹配任何构造器(构造器参数是Int64),于是解析为()操作符重载(Float64版本);
  • e1(1)中e1是枚举值而非构造器引用,只能调用操作符重载;
  • Y(1)中Y本是无参构造器,Y(1)无法匹配构造器形式,故调用操作符重载。

从枚举章节可以确认,enum体内允许定义成员函数、操作符函数和成员属性,但要求构造器、成员函数、成员属性之间不能重名。

5.3 与变长参数语法的协同

仓颉的变长参数语法糖允许在()操作符重载与[]索引操作符重载的调用处直接展开参数序列(详见函数调用语法糖):

class Counter { var total = 0 init(data: Array<Int64>) { total = data.size } operator func ()(data: Array<Int64>) { total += data.size } } main() { let counter = Counter(1, 2) println(counter.total) // 2 counter(3, 4, 5) println(counter.total) // 5 }

这里counter(3, 4, 5)正是变长参数在()操作符重载调用处的应用。需要注意,变长参数不支持其他操作符重载、composition、pipeline 这几种调用方式。

六、可重载操作符全表(优先级从高到低)

仓颉只允许重载下表列出的操作符,且不支持自定义操作符——即不允许定义除下表中operator之外的其他操作符函数:

OperatorDescription
()Function call
[]Indexing
!NOT
-Negative
**Power
*Multiply
/Divide
%Remainder
+Add
-Subtract
<<Bitwise left shift
>>Bitwise right shift
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal
==Equal
!=Not equal
&Bitwise AND
^Bitwise XOR
\|Bitwise OR

对照附录操作符的全量操作符表可以发现,并非所有内置操作符都可重载:++/--自增自减、?.问号、区间../..=、is/as、??、|>、~>、=及全部复合赋值操作符(+=、-=等)都不在可重载之列。例如++被定义为+= 1的语法糖且仅限整数类型(见基本操作符),因此无法通过重载扩展到自定义类型。

七、三个必须规避的注意事项

7.1 复合赋值操作符的自动推导规则

一旦在某个类型上重载了除关系操作符(<、<=、>、>=、==、!=)之外的其他二元操作符,并且操作符函数的返回类型与左操作数的类型一致、或是其子类型,那么该类型自动支持对应的复合赋值操作符(如+对应+=)。反之,若返回类型与左操作数类型不一致且不是其子类型,则使用对应复合赋值符号时报类型不匹配错误:

open class MyClass { var x: Int64 = 0 public init(a: Int64) { x = a } public operator func +(right: MyClass): Int64 { // The above rules are not met this.x + right.x } } main() { var a = MyClass(5) var b = MyClass(3) a += b // Error, type incompatible in this compound assignment expression }

这是因为+=本质上要求"对左值原地应用+并回写",当+的返回类型与左操作数类型不符时,回写操作的类型检查无法通过。这也解释了为什么官方在基本操作符中强调复合赋值表达式要求两个操作数类型相同。

7.2 禁止自定义操作符

仓颉编程语言不支持自定义操作符,即不允许定义除上表所列之外的任何操作符函数。如果尝试定义operator func ***(...)之类的符号,将直接编译报错。这是语言层面的刻意设计——可重载集合是封闭的,保证了代码的可读性与优先级体系的稳定性。

7.3 与类型原生支持的操作符发生重定义冲突

对于类型T,如果T已默认支持某些可重载操作符,那么通过扩展方式再次为其实现相同签名的操作符函数将报重定义错误。例如:

  • 为数值类型重载其已支持的同签名算术操作符、位操作符或关系操作符;
  • 为Rune重载同签名的关系操作符;
  • 为Bool类型重载同签名的逻辑操作符、判等或不等操作符;

均会触发重定义错误。官方示例(注意此处还叠加了参数个数错误,二元操作符只允许一个显式参数):

extend Int64 { public operator func +(x: Int64, y: Int64): Int64 { // Error, invalid number of parameters for operator '+' x + y } }

该示例同时包含两个错误点:一是+的签名错误(二元操作符函数只允许一个参数,这里写了两个),二是即便签名正确,Int64已原生支持+,扩展重载也会造成重定义冲突。因此,操作符重载的正确使用场景是为不具备该运算能力的自定义类型补充运算语义,而不是试图改写内置类型的既有行为。这一边界与整数类型章节的说明互为印证:"本章所提及的某个类型支持的操作,均是指在没有操作符重载的前提下。"

八、操作符重载在接口与跨类型体系中的延伸

操作符重载不止出现在具体类型内部,它同样是接口契约的一部分,并可与扩展、接口实现组合出丰富的抽象能力。

8.1 接口中的操作符重载函数

在接口实现的要求中明确规定:对于成员函数和操作符重载函数,实现类型提供的函数实现必须与接口对应的函数名称相同、参数列表相同、返回类型相同。唯一的例外是:若接口中操作符重载函数的返回值类型是class类型,则实现函数的返回类型允许是其子类型。这意味着一组异构类型可以通过接口统一声明"可运算"的能力,再由各实现类型给出各自的运算语义。

8.2 索引操作符重载在标准库互操作中的实际应用

仓颉标准库与跨语言体系大量使用索引操作符重载来提供自然的数组式访问。除前文提到的JArray<T>外,Array<T>的arr[i]取值、arr[i] = v赋值同样建立在[]操作符重载之上。当你为自己的容器类型(如稀疏矩阵、字典包装类)重载[]时,用户就能获得与内置数组完全一致的读写体验——这正是索引操作符取值/赋值双形式设计的实战价值所在。

九、快速自查清单

编写操作符重载代码前,可对照以下清单逐项检查:

  • 是否在func前添加了operator修饰符?
  • 操作符是否属于可重载集合(见第六节表格),而非自定义符号?
  • 一元操作符参数数为 0,二元操作符参数数为 1?
  • 是否定义在class/interface/struct/enum/extend中,且未使用static?
  • 函数是否为非泛型(未携带类型参数)?
  • 索引赋值形式是否恰好有一个名为value的命名参数、无默认值、返回Unit?
  • 索引赋值形式是否作用在不可变类型上(除enum外均禁止)?
  • 成员函数内是否用this()/super()误调了()操作符重载?
  • 复合赋值(+=等)可用时,二元操作符返回类型是否为左操作数类型或其子类型?
  • 是否尝试通过extend重定义类型已原生支持的同签名操作符(重定义冲突)?

以上规则全部对应官方文档与操作符函数签名表,是仓颉编译器静态检查的权威依据。掌握操作符重载,意味着你可以为领域类型构造出"天然可读"的运算表达式——无论是数学库中的向量运算、集合框架中的索引访问,还是 DSL 中的领域语义,都将在这一机制之上变得自然、简洁且类型安全。

  • 文档
  • 编程语言

【免费下载链接】cangjie_docs

仓颉编程语言开发者文档。

项目地址:https://gitcode.com/Cangjie/cangjie_docs
点击查看免费下载

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询