仓颉语言 -- 包

1、包的概述

随着项目规模的不断扩大,仅在一个超大文件中管理源代码会变得十分困难。这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效

在仓颉编程语言中,是编译的最小单元,每个包可以单独输出 AST 文件静态库文件动态库文件等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。一个包中可以包含多个源文件

模块是若干包的集合,是第三方开发者发布的最小单元。一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的 main ,该 main 没有参数参数类型为 Array<String>,返回类型为整数类型Unit 类型

2、包的声明

在仓颉编程语言中,包声明以关键字 package 开头,后接 root 包至当前包由 . 分隔路径上所有包的包名。包名必须是合法标识符。例如:

package pkg1      // root 包 pkg1
package pkg1.sub1 // root 包 pkg1 的子包 sub1

包声明必须在源文件的非空非注释的首行,且同一个包中的不同源文件的包声明必须保持一致

// file 1
// Comments are accepted
package test
// declarations...

// file 2
let a = 1 // Error, package declaration must appear first in a file
package test
// declarations...

仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点。例如包的源代码位于 src/directory_0/directory_1 下,root 包名pkg 则其源代码中的包声明应为 package pkg.directory_0.directory_1

需要注意的是:

  • 包所在的文件夹名必须与包名一致
  • 源码根目录默认名为 src
  • 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名 default
    假设源代码目录结构如下:
// The directory structure is as follows:
src
`-- directory_0
    |-- directory_1
    |    |-- a.cj
    |    `-- b.cj
    `-- c.cj
`-- main.cj

则 a.cj、b.cj、c.cj、main.cj 中的包声明可以为:

// a.cj
// in file a.cj, the declared package name must correspond to relative path directory_0/directory_1.

package default.directory_0.directory_1
// b.cj
// in file b.cj, the declared package name must correspond to relative path directory_0/directory_1.

package default.directory_0.directory_1
// c.cj
// in file c.cj, the declared package name must correspond to relative path directory_0.

package default.directory_0
// main.cj
// file main.cj is in the module root directory and may omit package declaration.

main() {
    return 0
}

另外,包声明不能引起命名冲突:子包不能和当前包的顶层声明同名

以下是一些错误示例:

// a.cj
package a
public class B { // Error, 'B' is conflicted with sub-package 'a.B'
    public static func f() {}
}

// b.cj
package a.B
public func f {}

// main.cj
import a.B // ambiguous use of 'a.B'

main() {
    a.B.f()
    return 0
}

3、顶层声明的可见性

仓颉中,可以使用访问修饰符来控制对类型变量函数等顶层声明的可见性仓颉有 4 种访问修饰符:privateinternalprotectedpublic,在修饰顶层元素时不同访问修饰符的语义如下。

  • private 表示仅当前文件内可见。不同的文件无法访问这类成员。
  • internal 表示仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员,当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。
  • protected 表示仅当前模块内可见。同一个包的文件可以不导入就访问这类成员,不同包但是在同一个模块内的其它包可以通过导入访问这些成员,不同模块的包无法访问这些成员。
  • public 表示模块内外均可见。同一个包的文件可以不导入就访问这类成员,其它包可以通过导入访问这些成员。

在这里插入图片描述
不同顶层声明支持的访问修饰符和默认修饰符默认修饰符是指在省略情况下的修饰符语义,这些默认修饰符也允许显式写出)规定如下:

  • pacakge 支持使用 internal、protected、public,默认修饰符为 public。
  • import 支持使用全部访问修饰符,默认修饰符为 private。
  • 其他顶层声明支持使用全部访问修饰符,默认修饰符为 internal。

仓颉的访问级别排序为 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()
}
  • 变量声明
// 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.
  • 类声明中继承的类
// a.cj
package a
open class C1 {}
public class C2 <: C1 {} // Error, public declaration C2 cannot use internal type C1.
  • 类型实现的接口
// a.cj
package a
interface I {}
public enum E <: I { A } // Error, public declaration uses internal types.
  • 泛型类型的类型实参
// a.cj
package a
public class C1<T> {}
class C2 {}
public let v1 = C1<C2>() // Error, public declaration v1 cannot use internal type C2.
  • where 约束中的类型上界
// a.cj
package a
interface I {}
public class B<T> where T <: I {}  // Error, public declaration B cannot use internal type I.

值得注意的是:

  • 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()
}
  • public 修饰的顶层声明能使用匿名函数,或者任意顶层函数,包括 public 修饰的类型和没有 public 修饰的顶层函数。
public var t1: () -> Unit = { => } // Ok.
func f1(): Unit {}
public let t2 = f1 // Ok.

public func f2() // Ok.
{
  return f1
}
  • 内置类型诸如 Rune、Int64 等也都默认是 public 的
var num = 5
public var t3 = num // Ok.

4、包的导入

4.1 使用 import 语句导入其它包中的声明或定义

在仓颉编程语言中,可以通过 import fullPackageName.itemName 的语法导入其他包中的一个顶层声明或定义fullPackageName 为完整路径包名,itemName 为声明的名字。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。例如:

package a
import std.math.*
import package1.foo
import {package1.foo, package2.bar}

如果要导入的多个 itemName 同属于一个 fullPackageName,可以使用 import fullPackageName.{itemName[, itemName]*} 语法,例如:

import package1.{foo, bar, fuzz}

这等价于:

import package1.foo
import package1.bar
import package1.fuzz

除了通过 import fullPackagename.itemName 语法导入一个特定的顶层声明或定义外,还可以使用 import packageName.* 语法将 packageName 包中所有可见的顶层声明或定义全部导入。例如:

import package1.*
import {package1.*, package2.*}

需要注意:

  • import 可以被 privateinternalprotectedpublic 访问修饰符修饰。不写访问修饰符的 import 等价于 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
}

4.2 隐式导入 core 包

诸如 StringRange 等类型能直接使用,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明

4.3 使用 import as 对导入的名字重命名

不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,我们支持使用 import packageName.name as newName 的方式进行重命名来避免冲突没有名字冲突的情况下仍然可以通过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 语句处不报错;在使用处,会因为无法导入唯一的名字而报错。这种情况可以通过 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
}

4.4 重导出一个导入的名字

在功能繁多的大型项目的开发过程中,这样的场景是非常常见的:包 p2 大量地使用从包 p1 中导入的声明,当包 p3 导入包 p2 并使用其中的功能时,p1 中的声明同样需要对包 p3 可见。如果要求包 p3 自行导入 p2 中使用到的 p1 中的声明,这个过程将过于繁琐。因此希望能够在 p2 被导入时一并导入 p2 使用到的 p1 中的声明。

在仓颉编程语言中,import 可以被 privateinternalprotectedpublic 访问修饰符修饰。其中,public、protected 或者 internal 修饰的 import 可以把导入的成员重导出(如果这些导入的成员没有因为名称冲突或者被遮盖导致在本包中不可用)。其它包可以根据可见性直接导入并使用本包中用重导出的内容,无需从原包中导入这些内容。

  • private import 表示导入的内容仅当前文件内可访问,private 是 import 的默认修饰符,不写访问修饰符的 import 等价于 private import。
  • internal import 表示导入的内容在当前包及其子包(包括子包的子包)均可访问。非当前包访问需要显式 import。
  • protected import 表示导入的内容在当前 module 内都可访问。非当前包访问需要显式 import
  • public import 表示导入的内容外部都可访问。非当前包访问需要显式 import

在下面的例子中,b 是 a 的子包,在 a 中通过 public import 重导出了 b 中定义的函数 f。

package a

public let x = 0
public import a.b.f
internal package a.b

public func f() { 0 }
import a.f  // ok
let _ = f() // ok

需要注意的是,包不可以被重导出:如果被 import 导入的是包,那么该 import 不允许被 public、protected 或者 internal 修饰

public import a.b // Error, cannot re-export package

5、程序入口

仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main

如果模块采用生成可执行文件的编译方式,编译器只在源文件根目录下的顶层查找 main。如果没有找到,编译器将会报错;如果找到 main,编译器会进一步对其参数和返回值类型进行检查。需要注意的是,main 不可被访问修饰符修饰,当一个包被导入时,包中定义的 main 不会被导入

作为程序入口的 main 可以没有参数参数类型为 Array<String>,返回值类型为 Unit整数类型

没有参数的 main

// main.cj
main(): Int64 { // Ok.
    return 0
}

参数类型为 Array<String> 的 main

// main.cj
main(args: Array<String>): Unit { // Ok.
    for (arg in args) {
        println(arg)
    }
}

使用 cjc main.cj 编译完成后,通过命令行执行:./main Hello, World,将会得到如下输出:

Hello,
World

以下是一些错误示例:

// main.cj
main(): String { // Error, return type of 'main' is not 'Integer' or 'Unit'.
    return ""
}
// main.cj
main(args: Array<Int8>): Int64 { // Error, 'main' cannot be defined with parameter whose type is not Array<String>.
    return 0
}
// main.cj
main(args: Array<Int8>): Int64 { // Error, 'main' cannot be defined with parameter whose type is not Array<String>.
    return 0
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值