仓颉语法-Collection类型、包

314 阅读14分钟

Collection类型

仓颉中的集合类型包括Array、ArrayList、HashSet、HashMap

  1. 他们都是泛型的,可以支持任意类型
  2. 他们都是引用类型。即赋值的时候相当于只是多了一个指针指向原数据
  3. Array 初始化之后,长度不可变,不能增删元素。其他三种可以增删元素

image.png

package cj_exercise.study

import std.collection.{ArrayList, HashSet, HashMap}

// 一个迭代器,内部维护迭代的次数,保存需要迭代的数据
class MyIteratorClass<T> <: Iterator<T> {
    var index: Int = 0
    private let dataList: Array<T>
    init(dataList: Array<T>) {
        this.dataList = dataList
    }

    public func next(): Option<T> {
        let v = dataList.get(index)
        index += 1
        return v
    }
}

// 自定义可迭代的类
class MyIterableClass<T> <: Iterable<T> {
    let list: Array<T>
    let name: String = 'MyIterableClass'
    init(list: Array<T>) {
        this.list = list
    }
    // 需要返回一个迭代器用来迭代数据
    // 这里迭代的数据是this.list
    public func iterator(): Iterator<T> {
        MyIteratorClass(this.list)
    }
}

public func collectionDemo() {
    func arrayListDemo() {
        // ArrayList 可变数组 适合于需要频繁增加和删除元素的场景
        // 类型,ArrayList 在作为表达式使用时不会拷贝副本,同一个 ArrayList 实例的所有引用都会共享同样的数据。
        // 初始化一个只能存放String类型的数组
        let array0 = ArrayList<String>()
        // 虽然array0是使用let修饰,但是由于ArrayList本身是可变的,所以可以调用append方法添加元素
        // 这里let的作用只是保证array0不能被重新赋值,但是修改array0里面的数据还是可以的
        // array0 = ArrayList<String>
        array0.append('hi')
        // 初始化一个100个容量的数组
        let array1 = ArrayList<Int>(100)
        // 使用现有Array创建一个数组
        let array2 = ArrayList<Int>([1, 2, 3])
        // 使用现有Collection创建一个数组
        let array3 = ArrayList<Int>(array2)
        // 创建一个容量为5,每个元素使用闭包构建的数组
        let array4 = ArrayList<Int>(5, {i => i + 1})
        // array4还可以使用尾随闭包实现
        let array5 = ArrayList<Int>(5) {i => i + 1}
        // 访问第一个元素
        // 使用下标访问,超出范围时会崩溃
        let e0 = array5[0]
        // 使用get方法访问,返回一个Option,超出范围时返回Option<T>.None
        let e0_0 = array5.get(0)
        if (let Some(e0) <- e0_0) {
            println('第一个元素值为 ${e0}')
        }
        // 遍历数组
        for (i in array5) {
            println(i)
        }
        // 判断是否包含数据
        array5.contains(5)
        // 修改第一个元素的值
        array5[0] = 10
        array5.set(0, 20)
        // 在尾部追加数据
        array5.append(10)
        array5.appendAll([1, 2, 3])
        // 在某个index处插入数据
        array5.insert(2, 2)
        array5.insertAll(2, [2, 3, 10])
        // 删除 第二个位置的 元素
        array5.remove(2)
        // 删除2到6位置的元素
        array5.remove(2..=6)
        // 把小于4的数据删除
        array5.removeIf {data => data < 4}
    }

    func hasSetDemo() {
        // HashSet<T> 类型来构造只拥有不重复元素的 Collection
        // T 必须是实现了 Hashable 和 Equatable<T> 接口的类型
        // 初始化一个只能存放String类型的set
        let set0 = HashSet<String>()
        // 虽然set0是使用let修饰,但是由于HashSet本身是可变的,所以可以调用put方法添加元素
        // 这里let的作用只是保证set0不能被重新赋值,但是修改set0里面的数据还是可以的
        // set0 = HashSet<String>()
        set0.put('hi')
        // 初始化一个100个容量的set
        let set1 = HashSet<Int>(100)
        // 使用现有Array创建一个set
        let set2 = HashSet<Int>([1, 2, 3])
        // 使用现有Collection创建一个set
        let set3 = HashSet<Int>(set2)
        // 创建一个容量为5,每个元素使用闭包构建的set
        let set4 = HashSet<Int>(5, {i => i + 1})
        // set4还可以使用尾随闭包实现
        let set5 = HashSet<Int>(5) {i => i + 1}
        // 遍历set
        for (i in set5) {
            println(i)
        }
        // 判断是否包含数据
        set5.contains(2)
        set5.containsAll([1, 2])
        // 在尾部追加数据
        set5.put(10)
        set5.putAll([1, 2, 3])
        // 删除 2 这个元素
        set5.remove(2)
        // 删除 1, 2 这两个元素
        set5.removeAll([1, 2])
        // 把小于4的数据删除
        set5.removeIf {data => data < 4}
    }

    func hasMapDemo() {
        // HashMap<K,V> 类型来构造元素为键值对的 Collection
        // K 必须是实现了 Hashable 和 Equatable<K> 接口的类型
        // V 表示 HashMap 的值类型,V 可以是任意类型
        // 初始化一个只能存放String类型的map
        let map0 = HashMap<String, Int>()
        // 虽然map0是使用let修饰,但是由于HashMap本身是可变的,所以可以调用put方法添加元素
        // 这里let的作用只是保证map0不能被重新赋值,但是修改map0里面的数据还是可以的
        // map0 = HashMap<String, Int>()
        map0.put('hi', 2)
        // 初始化一个100个容量的map
        let map1 = HashMap<String, Int64>(100)
        // 使用元素为元组类型的Array创建一个map
        let map2 = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
        // 使用现有Collection创建一个map
        let map3 = HashMap<String, Int64>(map2)
        // 创建一个容量为5,每个元素使用闭包构建的map
        let map4 = HashMap<String, Int64>(5, {i => ('key${i}', i)})
        // map5还可以使用尾随闭包实现
        let map5 = HashMap<String, Int64>(5) {i => ('key${i}', i)}
        // 遍历map
        for ((k, v) in map5) {
            println('key:${k} => value:${v}')
        }
        // 判断是否包含数据
        map5.contains('key3')
        map5.containsAll(['key1', 'key2'])

        // 通过key获取数据
        // 通过下标获取数据,对应key的值不存在的时候会崩溃
        let v0 = map5['key2']
        // 通过get获取数据,返回一个可选值
        let v1 = map5.get('key2')
        if (let Some(v) <- v1) {
            println('v1的值是 ${v}')
        }

        // 添加修改数据
        map5['key5'] = 5
        map5.put('key5', 5)
        map5.putAll([('key6', 6), ('key7', 7)])
        // 只有map5里面没有key8时,才添加
        map5.putIfAbsent('key8', 8)

        // 删除 key为key4的 元素
        map5.remove('key4')
        // 删除 key为key1和key2 的元素
        map5.removeAll(['key1', 'key2'])
        // 删除key以a开头并且 value小于5 的元素
        map5.removeIf() {k, v => k.startsWith('a') && v < 5}
    }

    func iterableDemo() {
        // Range、Array、ArrayList 都是通过 Iterable 来支持 for-in 语法
        let list = Array(80) {i => i}
        let myCls = MyIterableClass(list)
        for (i in myCls) {
            println('for-in myCls ${i}')
        }

        let it = myCls.iterator()
        while (let Some(i) <- it.next()) {
            println('while-let myCls ${i}')
        }
    }
    iterableDemo()
}

  1. 包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物

    • 每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。
    • 一个包中可以包含多个源文件。一般情况下一个文件夹对应一个包
  2. 模块是若干包的集合,是第三方开发者 发布的最小单元

    • 一个模块的程序入口只能在其根目录
    • 模块的顶层最多只能有一个作为程序入口的 main函数
      • main函数前面没有func关键字
      • 没有参数或参数类型为 Array
      • 返回类型为整数类型或 Unit 类型
  3. 包声明必须在源文件的非空非注释的首行

    • 包名是toml中package.name拼接从src目录到当前目录的由.分割路径的字符串
      • 比如 package cj_exercise.study。cj_exercise是模块的name名,study是src下的文件夹
    • 同一个包中的不同源文件的包声明必须保持一致。因为他们都处于同一文件夹下。而包名是.分割文件路径,只到最后的文件夹
    • 仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点
      • 包所在的文件夹名必须与包名一致。
      • 源码根目录默认名为 src。
      • 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名 模块名
      • 子包不能和当前包的顶层声明同名
  4. 包内顶层声明的可见性有4种,访问级别排序为 public > protected > internal > private

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

image.png

  1. 不同顶层声明支持的访问修饰符和默认修饰符。内置类型都是public的

    • pacakge 支持使用 internal、protected、public,默认修饰符为 public。
    • import 支持使用全部访问修饰符,默认修饰符为 private。
    • 其他顶层声明支持使用全部访问修饰符,默认修饰符为 internal
  2. 一个声明的访问修饰符 不得高于该声明中 用到的类型的访问修饰符 的级别

    • 函数声明的修饰符,不能大于其参数和返回值的修饰符
    • 变量声明的修饰符,不能大于其类型的修饰符
    • 子类声明的修饰符不能大于父类的修饰符
    • 类声明的修饰符不能大于其实现的接口的修饰符
    • 泛型本身的修饰符,不能大于其实参的修饰符
    • 泛型本身的修饰符,不能大于其where 约束的类型上界
  3. public 修饰的声明在其 初始化表达式或者函数体里面 可以使用本包可见的任意类型

  4. public 修饰的顶层声明能使用匿名函数,或者任意顶层函数

  5. 导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前

    • import 可以被 private、internal、protected、public 访问修饰符修饰(但自己定义的package只能使用private)。不写访问修饰符的 import 等价于 private import

    • 导入的成员的作用域级别低于当前包声明的成员

      • 导入的声明或定义如果和当前包中的顶层声明或定义重名且不构成函数重载,则导入的声明和定义会被遮盖
      • 导入的声明或定义如果和当前包中的顶层声明或定义重名且构成函数重载,函数调用时将会根据函数重载的规则进行函数决议
    • 如果包的模块名或者包名被篡改,所有导入的地方都需要修改,否则在导入时会报错。

    • 只允许导入当前文件可见的顶层声明或定义,导入不可见的声明或定义将会在导入处报错。

    • 禁止通过 import 导入当前源文件所在包的声明或定义

    • 禁止包间的循环依赖导入,如果包之间存在循环依赖,编译器会报错

  6. 编译器会隐式的导入 core 包中所有的 public 修饰的声明

  7. 可以对导入进行重命名,既可以对导入的某个声明重命名,也可以对整个导入的包进行重命名

    • import packageName.name as newName 对导入的某个声明重命名
    • import pkg as newPkgName 对导入的包名进行重命名
    • 对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用。
    • 如果重命名后的名字与当前包顶层作用域的其它名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误
  8. 导入的声明或变量还可以被重新导出

    • import 可以被 private、internal、protected、public 访问修饰符修饰
    • 被 public、protected 或者 internal 修饰的 import 可以把导入的成员重导出
    • private import 表示导入的内容仅当前文件内可访问,private 是 import 的默认修饰符,不写访问修饰符的 import 等价于 private import。
    • internal import 表示导入的内容在当前包及其子包(包括子包的子包)均可访问。非当前包访问需要显式 import。
    • protected import 表示导入的内容在当前 module 内都可访问。非当前包访问需要显式 import。
    • public import 表示导入的内容外部都可访问。非当前包访问需要显式 import
    • 包不可以被重导出,只有导出的变量或声明才能被重新导出:如果被 import 导入的是包,那么该 import 不允许被 public、protected 或者 internal 修饰
  9. 仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main

    • 只有模块的output-type = "executable"时,编译器才会查找main函数
    • 编译器只在源文件根目录下的顶层查找 main。如果没有找到,编译器将会报错
    • main 前面没有func并且不可被访问修饰符修饰,当一个包被导入时,包中定义的 main 不会被导入
    • main 可以没有参数或参数类型为 Array,返回值类型为 Unit 或整数类型
// 包是编译的最小单元
// 包声明必须在源文件的非空非注释的首行
// 包名是toml中package.name拼接从src目录到当前目录的由.分割路径的字符串
// 仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点

// pacakge 支持使用 internal、protected、public,默认修饰符为 public
package cj_exercise.study
// public package cj_exercise.study
// protected package cj_exercise.study
// internal package cj_exercise.study

// 导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前

// 通过 import fullPackageName.itemName 的语法导入其他包中的一个顶层声明或定义,fullPackageName 为完整路径包名,itemName 为声明的名字
// 导入collection中的单个声明或定义
import std.collection.ArrayList
import std.collection

// 如果要导入的多个 itemName 同属于一个 fullPackageName,可以使用 import fullPackageName.{itemName[, itemName]*} 语法
// 导入collection中的多个声明或定义
import std.collection.{HashMap, HashSet}

// 使用 import packageName.* 语法将 packageName 包中所有可见的顶层声明或定义全部导入
// 导入collection中的所有可导出的声明或定义
import std.collection.*

// import 支持使用全部访问修饰符,默认修饰符为 private
// import cj_exercise.study.subpackage

// import 可以被 private、internal、protected、public 访问修饰符修饰
// 不写访问修饰符的 import 等价于 private import
// import std.collection.all
// private import std.collection.all
// internal import std.collection.all
// protected import std.collection.all
// public import std.collection.all

// 自己定义的包,只能使用private或者默认。只有系统的包支持4中修饰符
// 包不可以被重导出,只有导出的变量或声明才能被重新导出
// 如果被 import 导入的是包,那么该 import 不允许被 public、protected 或者 internal 修饰

// 这里导入的是包,不能被 public、protected、internal 修饰
// public import cj_exercise.extends

// 这里导入的是声明或变量,能被 public、protected、internal 修饰
private import cj_exercise.extends.MyExtendsClass
internal import cj_exercise.extends.MyExtendsClass
protected import cj_exercise.extends.MyExtendsClass
public import cj_exercise.extends.MyExtendsClass

// 导入的成员的作用域级别低于当前包声明的成员
import cj_exercise.other.global_var
// 使用as为导入的变量取别名
import cj_exercise.other.global_var as other_global
import cj_exercise.other.importOverride

// 对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用
// let a  = global_var
let b = other_global

// 如果重命名后的名字与当前包顶层作用域的其它名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误
public func importOverride(i: Float32) {
    println(i)
}

private func testForImportOverride() {
    importOverride(2)
    importOverride(2.2)
}

// 下面再使用global_bar时,使用的是本包声明的global_var变量。它的优先级比导入的cj_exercise.other.global_var更高
let global_var = 'ssss'

// 其他顶层声明支持使用全部访问修饰符,默认修饰符为 internal

// 顶层声明的可见性
// 仅当前文件内可见。不同的文件无法访问这类成员
private var privateVariable = 1
// 仅当前包及子包(包括子包的子包)内可见。
// 同一个包内可以不导入就访问这类成员
// 当前包的子包(包括子包的子包)内可以通过导入来访问这类成员
// 默认是internal
struct InternalStruct {}
// internal struct InternalStruct {}
// 仅当前模块内可见
// 同一个包的文件可以不导入就访问这类成员
// 不同包但是在同一个模块内的其它包可以通过导入访问这些成员
// 不同模块的包无法访问这些成员
protected class ProtectedClass {}
// 模块内外均可见
// 同一个包的文件可以不导入就访问这类成员
// 其它包可以通过导入访问这些成员
public interface PublicInterface {}

// 在同一个包内不允许有同名的顶层定义或声明(函数重载除外)
const GLOBAL_INT: Int = 20

public func printInt(a: Int) {
    println(a)
}
// 函数重载
public func printInt(a: Int, other: Array<Int>) {
    println(a)
    println(other)
    // 可以使用全路径的用法
    let hashMap = collection.HashMap<String, Int>()
}

// 子包不能和当前包的顶层声明同名
// 我们创建了一个subpackage,并声明了子包package cj_exercise.study.subpackage
// 这里就不能再使用subpackage了。因为继续这样的话,编译器无法区分是study.subpackage子包,还是study包中的subpackage类
// class subpackage {}

// 一个包中可以包含多个源文件,这些源文件位于同一个文件夹下
// 同一个包中的不同源文件的包声明必须保持一致,即.分割的包名 的 最后一个字符串是文件夹名
// 比如我这里的study_equal、study_struct都位于study目录下,但他们都是study这个包的源文件

// 仓颉的访问级别排序为 public > protected > internal > private
// 一个声明的访问修饰符不得高于该声明中用到的类型的访问修饰符的级别

// 函数声明的修饰符,不能大于其参数和返回值的修饰符
class AccessClass {}
// f1的修饰符public,但是参数和返回值的修饰符是internal的。不可以
// public func f1(c: AccessClass): AccessClass {
//     AccessClass()
// }

// 变量声明的修饰符,不能大于其类型的修饰符
// v1的修饰符public,但是它的类型AccessClass是internal的。不可以
// public let v1: AccessClass = AccessClass()

// 子类声明的修饰符不能大于父类的修饰符
// public AccessSubClass <: AccessClass {}

// 类声明的修饰符不能大于其实现的接口的修饰符
private interface AccessClassInterface {}
// extend AccessClass <: AccessClassInterface {}

// 泛型本身的修饰符,不能大于其实参的修饰符
public class GenericAccessClass<T> {}
// public let v1 = GenericAccessClass<AccessClass>()

// 泛型本身的修饰符,不能大于其where 约束的类型上界
// public class GenericAccessClass2<T> where T <: AccessClass {}

// public 修饰的声明在其 初始化表达式或者函数体里面 可以使用本包可见的任意类型
public func publicAccessInternal() {
    // 在public的函数体里面,可以使用本包中internal的AccessClass
    // 只是不能将internal修饰的AccessClass作为参数或返回值,在函数体内还是可以使用的
    let cls = AccessClass()
}

// public 修饰的顶层声明能使用匿名函数,或者任意顶层函数
// t2可以使用private修饰的privateVariable。
public let t2 = privateVariable

public func packageDemo() {
    println("包基础知识")
}

// 正确的main函数定义
// main() {}

// main(args: Array<String>) {}

// main() {
//     0
// }

main(args: Array<String>): Int {
    0
}

参考资料

  1. 仓颉编程语言开发指南 developer.huawei.com/consumer/cn…
  2. 仓颉编程语言白皮书 developer.huawei.com/consumer/cn…
  3. 仓颉编程语言语言规约developer.huawei.com/consumer/cn…