包的导入
一. 概述
在仓颉编程语言中,当你需要利用其他包中的顶层声明或定义时,可以通过import语句来实现。此语句遵循import fullPackageName.itemName的格式,其中fullPackageName代表需要引用的包的完整路径名,而itemName则指定了你希望从该包中导入的具体声明或定义的名字。
重要的是,import语句在源文件中的位置有着明确的要求:它必须出现在包声明(如果存在的话)之后,同时位于任何其他的声明或定义之前。这样的规则确保了代码的结构清晰,使得导入的依赖明确可见,有利于维护和代码阅读。
通过上述方式,仓颉编程语言支持灵活的模块化和代码复用,使得开发者能够方便地构建和组织复杂的项目结构。
package a
import std.math.*
import package1.foo
import {package1.foo, package2.bar}
二. 如何导入
在仓颉编程语言中,当你需要从一个包(fullPackageName)中导入多个顶层声明或定义(itemName)时,可以使用一种更紧凑的语法来简化导入过程。这种语法允许你以集合的形式指定多个项,格式为 import fullPackageName.{itemName[, itemName]}。其中,fullPackageName 是包含这些声明的包的完整路径名,而大括号内的 itemName 列表则是你希望从该包中导入的声明或定义的名字,多个名字之间用逗号分隔。星号()在这里仅用于表示列表的延续,实际上在编写时并不需要逐个列出所有项名后的星号,而是根据需要列出所有要导入的项名即可。
使用这种语法,你可以一次性从同一个包中导入多个项,从而使你的导入语句更加简洁。这样做不仅提高了代码的可读性,还减少了因重复书写包名而可能产生的错误。
import package1.{foo, bar, fuzz}
这等价于
import package1.foo
import package1.bar
import package1.fuzz
在仓颉编程语言中,除了可以通过 import fullPackageName.itemName 的精确语法来导入一个特定包中的顶层声明或定义外,还存在一种更为宽泛的导入方式。这种方式允许你使用 import packageName.* 的语法,一次性将 packageName 包中所有可见的顶层声明或定义全部导入到当前命名空间下。这样做的好处是,你可以快速访问该包中的所有公开成员,无需每次引用时都重复包名前缀。
然而,需要注意的是,虽然这种方式提供了便利,但也可能导致命名冲突或使代码的可读性降低,因为从多个包中导入所有内容可能会让命名空间变得拥挤,难以追踪某个特定声明的来源。因此,在实际开发中,建议谨慎使用 import packageName.* 语法,尤其是在大型项目中。
import package1.*
import {package1.*, package2.*}
在仓颉编程语言中,关于import
语句的使用,有几点重要的注意事项需要明确:
- 访问修饰符:
import
语句可以被private
、internal
、protected
、public
等访问修饰符修饰,以控制导入成员的可见性范围。如果不显式指定访问修饰符,则默认等同于private import
,即导入的成员仅在当前文件内可见。 - 作用域级别:导入的成员在作用域上的优先级低于当前包声明的成员。这意味着,如果在当前包中也存在与导入成员同名的声明,那么当前包的成员将覆盖导入的成员。
- 包名和模块名一致性:当已导出的包的模块名或包名在后续被篡改,导致与导出时指定的名称不一致时,尝试导入该包或模块将会引发错误。这确保了导入的准确性和可靠性。
- 可见性要求:只允许导入当前文件可见的顶层声明或定义。如果尝试导入不可见的声明或定义(例如,由于访问权限限制而隐藏的声明),则会在导入处产生编译错误。
- 自引用禁止:禁止通过
import
语句导入当前源文件所在包的声明或定义。这是因为当前文件已经直接位于该包内,无需通过导入来访问其内部成员。 - 循环依赖禁止:仓颉编程语言禁止包之间的循环依赖导入。如果编译器检测到包之间存在循环依赖关系,将会报错并阻止项目的构建。这有助于维护项目结构的清晰性和可维护性,避免复杂的依赖关系导致的编译错误和运行时问题。
// pkga/a.cj
package pkga // Error, packages pkga pkgb are in circular dependencies.
import pkgb.*
class C {}
public struct R {}
// pkgb/b.cj
package pkgb
import pkga.*
// pkgc/c1.cj
package pkgc
import pkga.C // Error, 'C' is not accessible in package 'pkga'.
import pkga.R // OK, R is an external top-level declaration of package pkga.
import pkgc.f1 // Error, package 'pkgc' should not import itself.
public func f1() {}
// pkgc/c2.cj
package pkgc
func f2() {
/* OK, the imported declaration is visible to all source files of the same package
* and accessing import declaration by its name is supported.
*/
R()
// OK, accessing imported declaration by fully qualified name is supported.
pkga.R()
// OK, the declaration of current package can be accessed directly.
f1()
// OK, accessing declaration of current package by fully qualified name is supported.
pkgc.f1()
}
在仓颉编程语言中,关于导入的声明或定义与当前包中顶层声明或定义的重名处理,有明确的规则:
- 如果导入的声明或定义与当前包中的顶层声明或定义重名,并且这些声明不构成函数重载(即它们不是同名的函数且参数列表不同),则导入的声明或定义会被当前包中的声明或定义所遮盖。这意味着在当前包的作用域内,访问该名称时将直接引用到当前包中的声明或定义,而忽略导入的同名项。
- 如果导入的声明或定义与当前包中的顶层声明或定义重名,并且这些重名的声明是函数且构成函数重载(即它们具有相同的名称但参数列表不同),则在函数调用时,仓颉编程语言会根据函数重载的规则进行函数决议。这包括根据调用时提供的参数类型、数量和顺序来确定最匹配的函数实现。在这种情况下,编译器会考虑当前包中的函数和导入的函数,以选择最合适的函数进行调用。
// pkga/a.cj
package pkga
public struct R {} // R1
public func f(a: Int32) {} // f1
public func f(a: Bool) {} // f2
// pkgb/b.cj
package pkgb
import pkga.*
func f(a: Int32) {} // f3
struct R {} // R2
func bar() {
R() // OK, R2 shadows R1.
f(1) // OK, invoke f3 in current package.
f(true) // OK, invoke f2 in the imported package
}
三. 使用 import as 对导入的名字重命名
为了处理不同包之间可能存在的顶层声明重名问题,仓颉编程语言支持使用import as语法对导入的名字进行重命名。这种机制允许开发者在导入不同包中的同名顶层声明时,通过指定一个别名来避免命名冲突。即使没有直接的命名冲突,开发者也可以使用import as来重命名导入的内容,以增加代码的可读性或满足特定的编码风格。
使用import as进行重命名后,当前包中只能使用重命名后的新名字来引用该声明,原名将不再可用。如果重命名后的名字与当前包顶层作用域中的其他名字发生冲突,且这些名字对应的声明均为函数类型,则编译器会根据函数重载的规则来处理这些冲突;否则,将报告重定义的错误,因为不允许在同一作用域内存在两个同名的非函数重载声明。
此外,仓颉编程语言还支持import pkg as newPkgName的形式,允许开发者对包名本身进行重命名。这一特性特别有用,在解决不同模块中可能存在的同名包命名冲突问题时,通过为包指定一个唯一的别名,可以确保每个包都能被正确且无歧义地引用。
// a.cj
package p1
public func f1() {}
// d.cj
package p2
public func f3() {}
// b.cj
package p1
public func f2() {}
// c.cj
package pkgc
public func f1() {}
// main.cj
import p1 as A
import p1 as B
import p2.f3 as f // OK
import pkgc.f1 as a
import pkgc.f1 as b // OK
func f(a: Int32) {}
main() {
A.f1() // OK, package name conflict is resolved by renaming package name.
B.f2() // OK, package name conflict is resolved by renaming package name.
p1.f1() // Error, the original package name cannot be used.
a() // Ok.
b() // Ok.
pkgc.f1() // Error, the original name cannot be used.
}
这种情况可以通过 import as 定义别名或者 import fullPackageName 导入包作为命名空间。
// a.cj
package p1
public class C {}
// b.cj
package p2
public class C {}
// main1.cj
package pkga
import p1.C
import p2.C
main() {
let _ = C() // Error
}
// main2.cj
package pkgb
import p1.C as C1
import p2.C as C2
main() {
let _ = C1() // Ok
let _ = C2() // Ok
}
// main3.cj
package pkgc
import p1
import p2
main() {
let _ = p1.C() // Ok
let _ = p2.C() // Ok
}
在开发大型项目时,经常遇到包间依赖复杂的情况。假设包p2
广泛使用了从包p1
中导入的声明,当另一个包p3
想要使用p2
提供的功能时,如果p3
还需要知道并导入p2
所依赖的p1
中的具体声明,这无疑增加了项目的复杂性和维护难度。为了简化这一过程,仓颉编程语言允许通过特定的import
机制来实现声明的“重导出”。
在仓颉编程语言中,import
语句可以被private
、internal
、protected
、public
等访问修饰符修饰。这些修饰符不仅控制了导入内容在当前包或模块中的可见性,还决定了这些内容是否可以被其他包或模块通过当前包间接访问。
- public import:当
import
被public
修饰时,导入的成员将被视为当前包对外提供的一部分接口,其他包可以通过导入当前包来间接访问这些从其他包(如p1
)导入的声明,前提是这些导入的成员在当前包中没有因为名称冲突或被遮盖而不可用。 - protected import:类似于
protected
访问修饰符在其他上下文中的用法,被protected
修饰的import
使得导入的成员在当前模块内部(包括其子包)可访问,但对外部包不可见。 - internal import:此修饰符下的导入内容在当前包及其子包(递归地包括所有子包的子包)内可访问。然而,需要注意的是,这里的“外部都可访问”描述对于
internal import
并不准确;实际上,它仅限于包内和子包内,非当前包的直接访问仍需要显式导入。 - private import:默认的
import
修饰符,如果不显式指定其他修饰符,则等同于private import
。这意味着导入的内容仅在当前文件内部可见,对其他文件或包不可访问。
重要的是要理解,即使某个导入被标记为public
、protected
或internal
,它也只是在当前包或模块的上下文中提供了访问权限的调整,而并非自动将导入的声明暴露给所有外部包。外部包仍然需要显式地通过当前包来访问那些被重导出的内容。
package a
public let x = 0
public import a.b.f