3430 字
17 分钟
仓颉文档阅读-开发指南X: Collection 类型(II) - HashSet 与 HashMap
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置 Canjie-SDK

WARNING

博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

此样式内容, 表示文档原文内容

Collection类型#

仓颉的Collection类型, 新增元素和删除元素函数的用法基本相似

HashSet#

使用HashSet类型需要导入collection包:

import std.collection.*

可以使用HashSet类型来构造只拥有不重复元素Collection

仓颉使用HashSet<T>表示HashSet类型, T表示HashSet的元素类型, T必须是实现了HashableEquatable<T>接口的类型, 例如数值或String

var a: HashSet<Int64> = ... // 一个元素类型为 Int64 的 HashSet
var b: HashSet<String> = ... // 一个元素类型为 String 的 HashSet

元素类型不相同的HashSet是不相同的类型, 所以它们之间不可以互相赋值

因此以下例子是不合法的

b = a // 类型不匹配

仓颉中可以使用构造函数的方式构造一个指定的HashSet

let a = HashSet<String>() // 创建一个 空 HashSet, 其元素类型为 String
let b = HashSet<String>(100) // 创建一个容量为 100 的 HashSet
let c = HashSet<Int64>([0, 1, 2]) // 创建一个元素类型为 Int64 的 HashSet, 包含元素 0、1、2
let d = HashSet<Int64>(c) // 使用另一个`Collection`来初始化一个 HashSet
let e = HashSet<Int64>(10, {x: Int64 => (x * x)}) // 创建一个元素类型为 Int64、大小为 10 的 HashSet, 所有元素均通过指定的规则函数初始化

HashSet是在std.collection包中定义的, 使用时要将其导入

HashSetArrayList一样, 可以传入不同的类型实参, 声明存储不同类型元素的HashSet, 不同元素类型的HashSet属于不同类型

传入的类型实参, 必须是实现了HashableEquatable<T>接口的类型

HashSet添加元素一定是根据元素的哈希值来确定存储位置的

创建实例, 可以调用HashSet的构造函数进行构造

同样也可以通过 其他同元素类型的Collection进行构造

访问HashSet成员#

当需要对HashSet的所有元素进行访问时, 可以使用for-in循环遍历HashSet的所有元素

需要注意的是, HashSet并不保证按插入元素的顺序排列, 因此遍历的顺序和插入的顺序可能不同

import std.collection.*
main() {
let mySet = HashSet<Int64>([0, 1, 2])
for (i in mySet) {
println("The element is ${i}")
}
}

编译并执行上面的代码, 有可能会输出:

The element is 0
The element is 1
The element is 2

当需要知道某个HashSet包含的元素个数时, 可以使用size属性获得对应信息

import std.collection.*
main() {
let mySet = HashSet<Int64>([0, 1, 2])
if (mySet.size == 0) {
println("This is an empty hashset")
} else {
println("The size of hashset is ${mySet.size}")
}
}

编译并执行上面的代码, 会输出:

The size of hashset is 3

当想判断某个元素是否被包含在某个HashSet中时, 可以使用contains函数

如果该元素存在会返回true, 否则返回false

let mySet = HashSet<Int64>([0, 1, 2])
let a = mySet.contains(0) // a == true
let b = mySet.contains(-1) // b == false

HashSet不能使用下标随机访问元素

只能在遍历所有元素时 访问元素, 比如使用for-in循环遍历HashSet的元素

但, HashSet可以通过contains()函数, 判断目标元素是否存在, 存在返回true, 不存在返回false

修改HashSet#

HashSet是一种可变的引用类型, HashSet类型提供了添加元素、删除元素的功能

HashSet的可变性是一个非常有用的特征, 可以让同一个HashSet实例的所有引用都共享同样的元素, 并且对它们统一进行修改

如果需要将单个元素添加到HashSet里, 请使用add函数

如果希望同时添加多个元素, 可以使用add(all!: Collection<T>)函数, 这个函数可以接受另一个相同元素类型的Collection类型(例如Array)

当元素不存在时, add函数会执行添加的操作, 当HashSet中存在相同元素时, add函数将不会有效果

let mySet = HashSet<Int64>()
mySet.add(0) // mySet 包含元素 0
mySet.add(0) // mySet 包含元素 0
mySet.add(1) // mySet 包含元素 0, 1
let li = [2, 3]
mySet.add(all: li) // mySet 包含元素 0, 1, 2, 3

HashSet是引用类型, HashSet在作为表达式使用时不会拷贝副本, 同一个HashSet实例的所有引用都会共享同样的数据

因此对HashSet元素的修改会影响到该实例的所有引用

let set1 = HashSet<Int64>([0, 1, 2])
let set2 = set1
set2.add(3)
// set1 包含元素 0, 1, 2, 3
// set2 包含元素 0, 1, 2, 3

HashSet中删除元素, 可以使用remove函数, 需要指定删除的元素

let mySet = HashSet<Int64>([0, 1, 2, 3])
mySet.remove(1) // mySet 包含元素 0, 2, 3

HashSet中已存在的元素, 是无法原地修改的, 但可以先删除旧元素 再添加新元素

HashSet提供有 添加元素(add()函数)和删除元素(remove()函数) 的功能

HashSet内不允许存在重复的元素, 所以当使用add()函数 不会向HashSet添加已存在的元素

HashSetadd()也可以传入其他同元素类型的Collection实例

HashSetremove()函数, 可以删除目标元素

HashMap#

使用HashMap类型需要导入collection包:

import std.collection.*

可以使用HashMap类型来构造元素为键值对的Collection

HashMap是一种哈希表, 提供对其包含的元素的快速访问

表中的每个元素都使用其键作为标识, 可以使用键来访问相应的值

仓颉使用HashMap<K, V>表示HashMap类型, K表示HashMap的键类型, K必须是实现了HashableEquatable<K>接口的类型, 例如数值或String

V表示HashMap的值的类型, V可以是任意类型

var a: HashMap<Int64, Int64> = ... // 键类型为 Int64, 值类型为 Int64 的 HashMap
var b: HashMap<String, Int64> = ... // 键类型为 String, 值类型为 Int64 的 HashMap

元素类型不相同的HashMap是不相同的类型, 所以它们之间不可以互相赋值

因此以下例子是不合法的

b = a // 类型不匹配

仓颉中可以使用构造函数的方式构造一个指定的HashMap

let a = HashMap<String, Int64>() // 创建一个空的 HashMap, 其键类型为 String, 值类型为 Int64
let b = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)]) // 其键类型为 String, 值类型为 Int64, 包含元素("a", 0), ("b", 1), ("c", 2)
let c = HashMap<String, Int64>(b) // 使用另一个`Collection`来初始化一个 HashMap
let d = HashMap<String, Int64>(10) // 创建一个键类型为 String、值类型为 Int64、容量为 10 的 HashMap
let e = HashMap<Int64, Int64>(10, {x: Int64 => (x, x * x)}) // 创建一个键值类型为 Int64、大小为 10 的 HashMap, 所有元素都通过指定的规则函数进行初始化

HashMap是在std.collection包中定义的, 使用时要将其导入

HashMap也可以传入不同的类型实参, 声明存储不同类型元素的HashMap, 不同元素类型的HashMap属于不同类型

不过HashMap需要传入两个类型实参, 第一个作为HashMap的键K类型, 第二个作为HashMap的值V类型, 因为HashMap存储的是键值对

传入的键类型实参, 必须是实现了HashableEquatable<T>接口的类型; 值类型实参, 则无所谓

HashMap添加或修改元素, 一定是根据目标键的哈希值确定位置的

创建实例, 可以调用HashMap的构造函数进行构造, 同样也可以通过 其他同元素类型的Collection进行构造

访问HashMap成员#

当需要对HashMap的所有元素进行访问时, 可以使用for-in循环遍历HashMap的所有元素

需要注意的是, HashMap不保证按插入元素的顺序排列, 因此遍历的顺序和插入的顺序可能不同

import std.collection.*
main() {
let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
for ((k, v) in map) {
println("The key is ${k}, the value is ${v}")
}
}

编译并执行上面的代码, 有可能会输出:

The key is a, the value is 0
The key is b, the value is 1
The key is c, the value is 2

当需要知道某个HashMap包含的元素个数时, 可以使用size属性获得对应信息

import std.collection.*
main() {
let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
if (map.size == 0) {
println("This is an empty hashmap")
} else {
println("The size of hashmap is ${map.size}")
}
}

编译并执行上面的代码, 会输出:

The size of hashmap is 3

当想判断HashMap中是否包含某个键时, 可以使用contains函数

如果该键存在会返回true, 否则返回false

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let a = map.contains("a") // a == true
let b = map.contains("d") // b == false

当想访问指定键对应的元素时, 可以使用下标语法访问(下标的类型必须是键类型)

使用不存在的键作为索引会触发运行时异常

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let a = map["a"] // a == 0
let b = map["b"] // b == 1
let c = map["d"] // 运行时异常

HashMap可以通过下标语法, 使用确认存在的键, 访问目标键的值

也可以遍历访问元素

同样提供有contains()函数, 用来判断目标键是否存在

修改HashMap#

HashMap是一种可变的引用类型, HashMap类型提供了修改元素、添加元素、删除元素的功能

HashMap的可变性是一个非常有用的特征, 可以让同一个HashMap实例的所有引用都共享同样的元素, 并且对它们统一进行修改

可以使用下标语法对某个键对应的值进行修改

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
map["a"] = 3

HashMap是引用类型, HashMap在作为表达式使用时不会拷贝副本, 同一个HashMap实例的所有引用都会共享同样的数据

因此对HashMap元素的修改会影响到该实例的所有引用

let map1 = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let map2 = map1
map2["a"] = 3
// map1 包含元素 ("a", 3), ("b", 1), ("c", 2)
// map2 包含元素 ("a", 3), ("b", 1), ("c", 2)

如果需要将单个键值对添加到HashMap里, 请使用add函数

如果希望同时添加多个键值对, 可以使用add(all!: Collection<(K, V)>)函数

当键不存在时, add函数会执行添加的操作, 当键存在时, add函数会将新的值覆盖旧的值

let map = HashMap<String, Int64>()
map.add("a", 0) // map 包含元素 ("a", 0)
map.add("b", 1) // map 包含元素 ("a", 0), ("b", 1)
let map2 = HashMap<String, Int64>([("c", 2), ("d", 3)])
map.add(all: map2) // map 包含元素 ("a", 0), ("b", 1), ("c", 2), ("d", 3)

除了使用add函数以外, 也可以使用赋值的方式直接将新的键值对添加到HashMap

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
map["d"] = 3 // map 包含元素 ("a", 0), ("b", 1), ("c", 2), ("d", 3)

HashMap中删除元素, 可以使用remove函数, 需要指定删除的键

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2), ("d", 3)])
map.remove("d") // map 包含元素 ("a", 0), ("b", 1), ("c", 2)

HashMap可以通过下标语法访问 指定键的值, 当然也可以通过下标语法 修改指定键的值

hashMap["key"] = value

使用下标语法修改指定键的值时, 与只是访问指定键的值不同:

使用下标语法访问指定键的值, 如果指定键不存在, 则会 触发运行时异常

但, 使用下标语法修改指定键的值时, 如果指定键不存在, 则会 新增目标键值对

除此之外HashMap可以使用add()函数, 添加一个键值对, 如果目标键已存在, 则会修改键对应的值

add()函数, 也可以 传入同元素类型的HashMap添加一系列的键值对

add()函数, 更可以 传入其他的Collection类型, 但 要保证元素类型为Tuple<K, V>

HashMap可以使用remove()函数, 传入单个键, 尝试删除单个目标键值对

也可以传入其他 同键类型的Collection类型数据, 尝试删除一系列键值对

TIP

仓颉HashMap, 使用remove()尝试删除键值对时, 如果键不存在, 不会触发异常

而 使用下标语法访问目标键的值时, 如果目标键不存在, 会触发异常


HashSetHashMap都是通过元素(或Key)的哈希值 来确定元素的存储位置的

所以, 元素(或Key)类型要满足 实现HashableEquatable<K>接口的要求

本篇文章只是介绍HashSetHashMap最基本的使用, 更详细的使用可以阅读标准库API的使用或相关源码