- 文档
- 编程语言
【免费下载链接】cangjie_docs
仓颉编程语言开发者文档。
本文以仓颉编程语言官方开发者文档《顶层声明的可见性》为主体,系统讲解作用于类型、变量、函数等顶层声明的四种访问修饰符(private、internal、protected、public)的语义边界、默认规则与编译期校验。阅读完本文,你将能够独立规划包与模块的对外暴露面,正确设计库的公开 API,并规避"公开声明使用低可见性类型"等典型编译错误。
前置概念:包、模块与顶层声明
在理解访问修饰符之前,需要先明确仓颉语言中两个核心的组织单元:
- 包(package)是仓颉的编译最小单元。每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物;每个包有自己的名字空间,同一个包内不允许有同名的顶层定义或声明(函数重载除外),一个包可包含多个源文件,且同一个包的源码文件必须在同一目录。详见 包的概述。
- 模块(module)是若干包的集合,是第三方开发者发布的最小单元。没有父包的包称为 root 包,root 包及其子包(包括子包的子包)构成的整棵树即为一个模块。模块的程序入口只能位于其根目录下,顶层最多只能有一个作为程序入口的
main。详见 包和模块管理。
在仓颉的顶层作用域(源文件中不被任何大括号包裹的最外层作用域)中,可以定义变量、函数和自定义类型(如struct、class、enum和interface等),这些统称为顶层声明。其中变量和函数分别被称为全局变量和全局函数,main则是编译为可执行文件时的程序入口。相关背景可参考 程序结构。
由于包是编译单元、模块是发布单元,访问修饰符的可见性范围正是围绕"文件 → 包及子包 → 模块 → 所有包"这四个层级展开的。
四种访问修饰符的顶层语义
仓颉语言有 4 种访问修饰符:private、internal、protected、public,它们都是语言关键字(见 keyword 文档)。当用于修饰顶层元素时,各修饰符的语义如下:
private:仅当前文件内可见。不同的文件无法访问这类成员。internal:仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员;当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。protected:仅当前模块内可见。同一个包的文件可以不导入就访问这类成员;不同包但同属一个模块内的其他包,可以通过导入访问这些成员;不同模块的包无法访问这些成员。public:模块内外均可见。同一个包的文件可以不导入就访问这类成员;其他包可以通过导入访问这些成员。
四种修饰符的可见范围可汇总为下表:
| 修饰符 | 文件 | 包及子包 | 模块 | 所有包 |
|---|---|---|---|---|
private | Y | N | N | N |
internal | Y | Y | N | N |
protected | Y | Y | Y | N |
public | Y | Y | Y | Y |
可以这样理解这张表的读法:private的可见范围最小,仅限单个文件;internal将范围放宽到当前包及其子包(同一包内免导入,子包需导入);protected进一步放宽到整个模块(同包免导入,同模块其他包需导入);public则完全对外开放(同包免导入,其他包需导入)。可见性范围是逐级递增的包含关系。
一个直观的代码示例(摘自原文档,可正常编译):
package a private func f1() { 1 } // f1 仅在当前文件内可见 func f2() { 2 } // f2 仅当前包及子包内可见 protected func f3() { 3 } // f3 仅当前模块内可见 public func f4() { 4 } // f4 当前模块内外均可见注意上例中f2未写任何访问修饰符,它采用的是其他顶层声明的默认修饰符internal(详见下一节),因此其可见范围是"当前包及子包"。
与 class 成员访问修饰符的语义差异
顶层声明的protected语义(仅模块内可见)与class成员的protected语义(模块内可见 + 当前类子类中可见)存在差异,二者的适用语境不同,不可混用。class成员(包括成员变量、成员属性、构造函数、成员函数)同样支持这四种访问修饰符,缺省含义为internal,其中protected除了在模块内可见外,还允许当前类的子类访问。详见 class 成员的访问修饰符。
不同顶层声明支持的修饰符与默认修饰符
并非所有顶层声明都支持全部四种访问修饰符,仓颉对三类顶层声明分别规定了可用的修饰符集合与默认修饰符(默认修饰符指省略修饰符时的语义,这些默认修饰符也允许显式写出):
package:支持使用internal、protected、public,默认修饰符为public。注意package声明本身不支持private——包声明是文件所属命名空间的标识,其可见性天然与包名绑定。import:支持使用全部访问修饰符,默认修饰符为private。这意味着不写修饰符的import等价于private import,导入的内容仅在当前文件内可访问。- 其他顶层声明(函数、变量、自定义类型等):支持使用全部访问修饰符,默认修饰符为
internal。上例中的func f2()即属此类。
| 顶层声明类别 | 支持的修饰符 | 默认修饰符 |
|---|---|---|
package | internal、protected、public | public |
import | private、internal、protected、public | private |
| 其他顶层声明 | private、internal、protected、public | internal |
默认修饰符对库设计的影响
默认修饰符的组合规则对日常开发有直接影响:
- 顶层函数、全局变量、自定义类型默认是
internal,即默认只对当前包及其子包可见。如果希望被其他包导入使用,必须显式写出public。这是仓颉与许多语言"默认公开"策略的重要区别。 - 包声明默认
public,与 包名声明 中"包声明必须出现在源文件非空非注释的首行"的语法要求配合使用。 import默认private,若希望将导入的内容"传递"给其他包使用,需要显式使用internal、protected或public修饰(即重导出)。例如public import a.b.f可以把子包b中定义的函数f通过包a重导出,使外部只需import a.f即可使用。详见 包的导入。
访问级别排序与"声明不得高于所用类型"规则
仓颉的访问级别排序为:public > protected > internal > private。
一个核心约束是:一个声明的访问修饰符不得高于该声明中用到的类型的访问修饰符的级别。这保证了对外可见的声明不会"泄露"内部不可见的类型——否则外部使用者将无法完整引用该声明的签名。以下四类场景均受此规则约束(以下示例均来自原文档,编译均报错):
函数声明中的参数与返回值
// a.cj package a class C {} public func f1(a1: C) // Error, public declaration f1 cannot use internal type C. { return 0 } public func f2(a1: Int8): C // Error, public declaration f2 cannot use internal type C. { return C() } public func f3(a1: Int8) // Error, public declaration f3 cannot use internal type C. { return C() }上例中class C未加修饰符,默认级别为internal,而f1、f2、f3均为public,它们的形参类型或返回类型用到了C,即"public 声明使用了 internal 类型",编译器据此报错。注意f3的形参和返回类型都是Int8,仅仅因为函数体内部构造并返回了C(),同样触发了该错误——这与下文"public 声明在函数体内可以使用本包任意类型"的规则需要仔细区分:签名(参数/返回值)中直接出现的类型受级别约束,而函数体内局部使用的类型不受签名级别约束。
变量声明
// a.cj package a class C {} public let v1: C = C() // Error, public declaration v1 cannot use internal type C. public let v2 = C() // Error, public declaration v2 cannot use internal type C.显式标注类型(v1)和依赖类型推断(v2)的public变量声明,只要其类型最终落到internal级别,都会编译报错。
泛型类型的类型实参
// a.cj package a public class C1<T> {} class C2 {} public let v1 = C1<C2>() // Error, public declaration v1 cannot use internal type C2.public声明的变量以internal级别的C2作为泛型实参实例化C1<C2>,同样违规。
where约束中的类型上界
// a.cj package a interface I {} public class B<T> where T <: I {} // Error, public declaration B cannot use internal type I.where约束中的类型上界也属于"声明中用到的类型",public类的泛型约束若引用internal级别的接口I,编译器同样报错。这与 扩展的导入导出 中"扩展是否导出由被扩展类型与泛型约束的访问修饰符共同决定"的规则同源:泛型约束的可见性直接决定依赖该约束的声明能否对外暴露。
规则的本质
上述规则的共同本质是:对外暴露的声明签名中出现的每一个类型(含泛型实参、约束上界),其可见级别都必须不低于该声明本身。在设计库的公开 API 时,应优先确保公开类型及其依赖类型均为public,或将使用低可见性类型的声明降级为internal/protected。
public 声明在使用上的灵活空间
与"签名类型"的严格约束相对,仓颉为public声明预留了充分的实现自由度。以下场景全部合法(可编译通过):
函数体与初始化表达式可用本包任意类型
public修饰的声明在其初始化表达式或者函数体里面,可以使用本包可见的任意类型,包括被public修饰的类型和不被public修饰的类型:
// a.cj package a class C1 {} func f1(a1: C1) { return 0 } public func f2(a1: Int8) // OK. { var v1 = C1() return 0 } public let v1 = f1(C1()) // OK. public class C2 // OK. { var v2 = C1() }这里f2的签名中只出现Int8,函数体内局部使用internal级别的C1是允许的;public let v1的初始化表达式调用internal级别的f1并传入C1()也是允许的;public class C2的成员变量v2使用C1同样 OK——成员的类型不在"顶层声明签名"的检查范围内。
匿名函数与任意顶层函数
public修饰的顶层声明能使用匿名函数,或者任意顶层函数,包括被public修饰和不被public修饰的顶层函数:
public var t1: () -> Unit = {=>} // OK. func f1(): Unit {} public let t2 = f1 // OK. public func f2() // OK. { return f1 }public var t1以匿名函数初始化、public let t2引用internal级别的顶层函数f1、public func f2的函数体返回f1,均为合法用法。函数作为一等公民可以被自由引用,其可见级别不构成对public声明的约束。
内置类型默认是 public
内置类型诸如Rune和Int64等,默认的修饰符是public。因此基于内置类型的public声明天然满足"声明级别不高于所用类型级别"的约束:
var num = 5 public var t3 = num // OK.这也解释了为什么几乎所有公开 API 都可以放心使用Int64、String、Bool等内置类型作为形参和返回值。此外,String、Range等类型能直接使用,还因为编译器会自动为源码隐式导入core包中所有public修饰的声明(详见 包的导入 的"隐式导入 core 包"一节)。
同包内 private 同名自定义类型的编译期限制
限制的由来
同一个包内,private修饰的同名自定义类型(如struct、class、enum和interface等),在某些场景下不支持,不支持场景由编译器进行报错。这与"包内不允许同名顶层定义"的一般规则形成补充:private类型因可见范围仅限单文件,本可在不同文件中各自独立存在,但当它们被包内其他公开可见的声明间接"导出"时,会在产物层面产生名字冲突。
复现示例
例如在以下程序中,example1.cj与example2.cj文件包名相同(均为package test),在example1.cj文件中定义了private修饰的类A,在example2.cj文件中定义了private修饰的结构体A:
// example1.cj package test private class A {} public class D<T> { private let a: A = A() }// example2.cj package test private struct A {} public class C<T> { private let a: A = A() }运行以上程序,将输出:
error: currently, it is not possible to export two private declarations with the same name规避建议
该报错的关键在于两个private同名类型都被包内的public泛型类以成员形式间接承载,编译器在导出产物时无法区分两者。规避手段包括:
- 在同一包内避免使用同名的
private自定义类型,即使它们位于不同文件; - 若确需同名,应至少将其中的一个提升为
internal或更高的可见级别,或改用不同的标识符; - 若仅需在单文件内使用,可考虑将类型定义与使用方放在同一文件中以隔离冲突面。
与导入 / 重导出机制的联动
顶层声明的可见性直接决定了import的可用性。仓颉的导入规则(详见 包的导入)包括:
- 只允许导入当前文件可见的顶层声明或定义,导入不可见的声明或定义会在导入处报错;
- 禁止通过
import导入当前源文件所在包的声明或定义; - 禁止包间的循环依赖导入,若存在循环依赖,编译器会报错。
结合可见性语义可以得到完整的使用准则:
- 同一包内的
internal/protected/public成员可直接访问(免导入),private成员仅限本文件; - 子包访问父包(或模块内其他包)的
internal/protected成员必须显式import; - 跨模块只能访问
public成员,同样需要显式import; - 若希望把从别处导入的成员继续传递给下游包使用,应使用
public import/protected import/internal import进行重导出(但包本身不可被重导出,public import a.b会报错)。
编译验证与实战建议
用 cjc 快速验证
以上所有代码示例均可使用仓颉编译命令cjc验证。cjc的基本用法为cjc [option] file...,默认将给定源代码文件编译为名为main的可执行文件(详见 cjc 使用):
cjc a.cj对于多文件、多包的工程,可配合cjpm进行整包编译与产物管理;编译选项细节参见 compile_options。
面向库作者的检查清单
在编写将被其他包或模块消费的仓颉代码时,建议按以下清单自查:
- 默认意识:未写修饰符的顶层函数/变量/类型是
internal(仅包及子包可见);需要对外发布时显式写public。 - 签名级别:
public声明的形参、返回值、泛型实参、where约束上界中出现的每个类型,级别都不得低于public。 - 实现自由度:
public声明的函数体、初始化表达式内部,可以放心使用本包任意级别(含private、internal)的类型与函数。 - 同名冲突:同一包内避免多个
private同名自定义类型被公开声明间接导出。 - 导入传递:需要把导入的成员传递给下游时,用带访问修饰符的
import重导出;默认的private import只对本文件可见。 - 模块边界:
protected与internal都不可跨越模块边界,跨模块只能依赖public。
扩展场景的关联提醒
顶层声明可见性同时是 扩展的导入导出 规则的基石:直接扩展是否导出,由被扩展类型与泛型约束的访问修饰符共同决定;当所有泛型约束都是导出类型时该扩展才被导出。在package a.b中,private interface I1、internal interface I2、protected interface I3会分别导致基于它们的扩展对包外、子包外不可见。设计扩展 API 时,需要与顶层声明的可见性一并规划。
综上,仓颉的顶层声明访问控制以"文件 → 包及子包 → 模块 → 所有包"为坐标系,通过四种访问修饰符精确划分了每一层声明的可见边界,再辅以"声明级别不得高于所用类型级别"的编译期校验,从语言层面保障了模块封装与 API 安全。掌握本文的语义表、默认修饰符规则与典型错误模式,即可在仓颉工程中游刃有余地设计包内实现细节与对外公开接口。
- 文档
- 编程语言
【免费下载链接】cangjie_docs
仓颉编程语言开发者文档。
相关推荐
仓颉语言顶层声明可见性详解:public/protected/internal/private 四种访问修饰符的语义、默认规则与编译约束
仓颉语言顶层声明可见性详解:public/protected/internal/private 四种访问修饰符的语义、默认规则与编译约束 仓颉(Cangjie)
文档编程语言Swift 访问控制完全指南:用 open、public、package、internal、fileprivate、private 精确管理代码可见性
Swift 访问控制完全指南:用 open、public、package、internal、fileprivate、private 精确管理代码可见性 导读 访
文档教程Slang 声明可见性完全解析:public / internal / private 的判定规则、语言版本默认值与过滤时机
Slang 声明可见性完全解析:public / internal / private 的判定规则、语言版本默认值与过滤时机 本文以 Slang 官方设计文档
编译器图形学编程语言
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考