仓颉语言学习笔记(5)类class、接口interface、属性、子类型关系、类型转换、Collection类型
class定义
注意:
- class 只能定义在源文件的顶层作用域。
- 抽象类中禁止定义 private 的抽象函数;
- 不能为抽象类创建实例;
- 抽象类的非抽象子类必须实现父类中的所有抽象函数。
abstract class Shape{
public func getArea(): Float64
}
class Rectangle <: Shape {
private var width: Float64
private var height: Float64
init(width: Float64, height: Float64) {
this.width = width
this.height = height
}
public func getArea(): Float64 {
return this.width * this.height
}
}
main() {
let a: Shape = Rectangle(5.0, 1.5)
println("Area: " + a.getArea().toString())
}
// Area: 7.500000
class成员变量
class 成员变量分为实例成员变量和静态成员变量,静态成员变量使用 static 修饰符修饰,没有静态初始化器时必须有初值,只能通过类型名访问。
实例成员变量定义时可以不设置初值(但必须标注类型),也可以设置初值,只能通过对象(即类的实例)访问。
class Calculator {
static var x: Float64
var y: Float64
Calculator(y: Float64) {
this.y = y
}
static init() {
Calculator.x = 10.0
}
func calc(): Float64 {
return Calculator.x * this.y
}
}
main() {
let a: Calculator = Calculator(5.0)
println("Area: " + a.calc().toString())
}
// Area: 50.000000
class静态初始化器
静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。
一个 class 中最多允许定义一个静态初始化器,否则报重定义错误。
class Circle {
let radius: Float64
static let pi: Float64
static init() {
pi = 3.141592653589793
}
init(r: Float64) {
radius = r
}
func area(): Float64 {
return Circle.pi * radius * radius
}
}
main() {
let a: Circle = Circle(5.0)
println("Area: " + a.area().toString())
}
// Area: 78.539816
class构造函数
普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。一个类中可以定义多个普通构造函数,不同 构造函数之间必须构成重载。
class Rectangle {
let width: Float64
let height: Float64
init(width: Float64, height: Float64) {
this.width = width
this.height = height
}
}
main() {
let rectangle = Rectangle(10.0, 10.0)
let area = rectangle.width * rectangle.height
print("Area: " + area.toString())
}
// Area: 78.539750
一个类内还可以定义(最多)一个主构造函数。主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时具有定义成员变量和构造函数参数的功能。
class Rectangle {
Rectangle(let width: Float64, let height: Float64) {}
func area() {
return this.width * this.height
}
}
main() {
print("The area of width ${10.0} and height ${6.666666} is ")
println(Rectangle(10.0, 6.666666).area())
}
// The area of width 10.000000 and height 6.666666 is 66.666660
创建类的实例时调用的构造函数,将根据以下顺序执行类中的表达式:
- 先初始化主构造函数之外定义的有缺省值的变量;
- 如果构造函数体内未显式调用父类构造函数或本类其它构造函数,则调用父类的无参构造函数 super(),如果父类没有无参构造函数,则报错;
- 执行构造函数体内的代码。
class终结器
class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源。
class成员函数
class 成员函数同样分为实例成员函数和静态成员函数(使用 static 修饰符修饰),实例成员函数只能通过对象访问,静态成员函数只能通过 class 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。
根据有没有函数体,实例成员函数又可以分为抽象成员函数和非抽象成员函数。抽象成员函数没有函数体,只能定义在抽象类或接口中。需要注意的是,抽象实例成员函数默认具有 open 的语义,open 修饰符是可选的,且必须使用 public 或 protected 进行修饰。
abstract class Animal {
public func sound(): Unit
}
class Cat <: Animal {
public func sound() {
println("The cat meowed")
}
}
main() {
Cat().sound()
}
class成员的访问修饰符
对于 class 的成员(包括成员变量、成员属性、构造函数、成员函数),可以使用的访问修饰符有 4 种访问修饰符修饰:private、internal、protected 和 public,缺省的含义是 internal。4种访问修饰符的可见范围按从小到大的顺序排列。
- private 表示在 class 定义内可见。
- internal 表示仅当前包及子包内可见。
- protected 表示当前模块及当前类的子类可见。
- public 表示模块内外均可见。
This类型
在类内部,支持 This 类型占位符,代指当前类的类型。它只能被作为实例成员函数的返回类型来使用,当使用子类对象调用在父类中定义的返回 This 类型的函数时,该函数调用的类型会被识别为子类类型,而非定义所在的父类类型。
如果实例成员函数没有声明返回类型,并且只存在返回 This 类型表达式时,当前函数的返回类型会推断为 This。示例如下:
open class C1 {
func f(): This { // its type is `() -> C1`
return this
}
func f2() { // its type is `() -> C1`
return this
}
public open func f3(): C1 {
return this
}
}
class C2 <: C1 {
// member function f is inherited from C1, and its type is `() -> C2` now
public override func f3(): This { // Ok
return this
}
}
var obj1: C2 = C2()
var obj2: C1 = C2()
var x = obj1.f() // During compilation, the type of x is C2
var y = obj2.f() // During compilation, the type of y is C1
class的继承
子类将继承父类中除 private 成员和构造函数以外的所有成员。
抽象类总是可被继承的,故抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。但非抽象的类可被继承是有条件的:非抽象类定义时必须使用修饰符 open 修饰。当带 open 修饰的实例成员被 class 继承时,该 open 的修饰符也会被继承。当非 open 修饰的类中存在 open 修饰的成员时,编译器会给出告警。
class 仅支持单继承。
open class Animal{
let name: String
init(name: String) {
this.name = name
}
public open func sound() {
println("${name} makes a sound")
}
}
class Cat <: Animal {
init(name: String) {
super(name)
}
public override func sound() {
println("${name} meows")
}
}
对于定义时指定了父类的 class,它的直接父类就是定义时指定的类,对于定义时未指定父类的 class,它的直接父类是 Object 类型。Object 是所有类的父类。
父类构造函数调用
子类的 init 构造函数可以使用 super(args) 的形式调用父类构造函数,或使用 this(args) 的形式调用本类其他构造函数,但两者之间只能调用一个。如果调用,必须在构造函数体内的第一个表达式处,在此之前不能有任何表达式或声明。
open class Animal{
let name: String
init(name: String) {
this.name = name
}
}
class Cat <: Animal {
let nickname: String
init(name: String, nickname: String) {
super(name)
this.nickname = nickname
}
}
子类的主构造函数中,可以使用 super(args) 的形式调用父类构造函数(不必是父类的主构造函数),但不能使用 this(args) 的形式调用本类其他构造函数。
覆盖和重定义
子类中可以**覆盖(override)**父类中的同名非抽象实例成员函数(回顾重载:),即在子类中为父类中的某个实例成员函数定义新的实现。覆盖时,要求父类中的成员函数使用 open 修饰,子类中的同名函数使用 override 修饰,其中 override 是可选的。
对于被覆盖的函数,调用时将根据变量的运行时类型(由实际赋给该变量的对象决定)确定调用的版本(即所谓的动态派发)。
open class Cat {
let name: String
init(name: String) {
this.name = name
}
public open func getName() {
return name
}
func toString() {
return this.getName()
}
}
class LittleCat <: Cat {
init(name: String) {
super(name)
}
public override func getName() {
return "Little " + super.getName()
}
}
main() {
let cats = ArrayList<Cat>()
cats.add(LittleCat("CQX"))
cats.add(Cat("HX"))
for(cat in cats) {
println(cat.toString())
}
}
子类中可以重定义父类中的同名非抽象静态函数,即在子类中为父类中的某个静态函数定义新的实现。重定义时,要求子类中的同名静态函数使用 redef 修饰,其中 redef 是可选的。
open class MathTool {
public static func add(a: Int64, b: Int64){
return a + b
}
}
class WeirdMathTool <: MathTool {
public redef static func add(a: Int64, b: Int64) {
return a * b
}
}
main() {
let x = 5
let y = 6
println("Weird ${x} + ${y} = ${WeirdMathTool.add(x, y)}")
}
// Weird 5 + 6 = 30
接口定义
接口使用关键字 interface 声明,其后是接口的标识符 I 和接口的成员。接口成员可被 open 修饰符修饰,并且 open 修饰符是可选的。
当接口 I 声明了一个成员函数 f 之后,要为一个类型实现 I 时,就必须在该类型中实现一个对应的 f 函数。
因为 interface 默认具有 open 语义,所以 interface 定义时的 open 修饰符是可选的。
interface Animal {
func sound() {}
}
class Dog <: Animal {
public func sound() {
print("Woof!")
}
}
main() {
let dog = Dog()
dog.sound()
}
接口实现的要求
仓颉除 Tuple、VArray 和函数外的其他类型都可以实现接口。
一个类型实现接口有三种途径:
- 在定义类型时就声明实现接口。
- 通过扩展实现接口。
- 由语言内置实现。
实现类型声明实现接口时,需要实现接口中要求的所有成员,为此需要满足下面一些规则。
- 对于成员函数和操作符重载函数,要求实现类型提供的函数实现与接口对应的函数名称相同、参数列表相同、返回类型相同。
- 对于成员属性,要求是否被
mut修饰保持一致,并且属性的类型相同。
如果接口中的成员函数或操作符重载函数的返回值类型是 class 类型,那么允许实现函数的返回类型是其子类型。
open class Base {}
class Sub <: Base {}
interface I {
func f(): Base
}
class C <: I {
public func f(): Sub {
Sub()
}
}
接口的成员还可以提供默认实现。
interface Animal {
func sound() {
print("???")
}
}
class Dog <: Animal {
}
main() {
let dog = Dog()
dog.sound()
}
如果一个类型在实现多个接口时,多个接口中包含同一个成员的默认实现,这时会发生多重继承的冲突,语言无法选择最适合的实现,因此这时接口中的默认实现也会失效,需要实现类型提供自己的实现。
interface Run{
func play() {
return "Running"
}
}
interface Fight {
func play() {
return "Fighting"
}
}
class Man <: Run & Fight {
public override func play() {
return "Running ans Fighting"
}
}
main() {
let man = Man()
println(man.play())
}
Any类型
Any 类型是一个内置的接口,仓颉中所有接口都默认继承它,所有非接口类型都默认实现它,因此所有类型都可以作为 Any 类型的子类型使用。
main() {
var any: Any = 1
any = 2.0
any = "hello, world!"
}
属性
**属性(Properties)**提供了一个 getter 和一个可选的 setter 来间接获取和设置值。
使用属性的时候与普通变量无异,只需要对数据操作,对内部的实现无感知,可以更便利地实现访问控制、数据监控、跟踪调试、数据绑定等机制。
class Cat {
private var age = 10
public mut prop propAge: Int64 {
get() {
println("get")
return age
}
set(value) {
println("set")
age = value
}
}
}
main() {
let myCat = Cat()
println(myCat.propAge)
myCat.propAge = 5
println(myCat.propAge)
return 0
}
// get
// 10
// set
// get
// 5
属性可以在 interface、class、struct、enum、extend 中定义。
无 mut 修饰符的属性有且仅有定义 getter(对应取值)实现。使用 mut 修饰的属性必须分别定义 getter(对应取值)和 setter(对应赋值)的实现。
class Cat <: ToString{
private var age: Int64
private let name: String
init(age: Int64, name: String) {
this.age = age
this.name = name
}
public mut prop propAge: Int64 {
get() {
println("get")
return age
}
set(value) {
println("set")
age = value
}
}
public prop propName: String {
get() {
return name
}
}
public override func toString(): String {
return "cat name: ${this.name}, age: ${this.age}"
}
}
main() {
let myCat = Cat(10, "baby")
println(myCat)
myCat.propAge = 123
println(myCat)
return 0
}
抽象属性
在 interface 和抽象类中也可以声明抽象属性,这些抽象属性没有实现。当实现类型实现 interface 或者非抽象子类继承抽象类时,必须要实现这些抽象属性。
与覆盖的规则一样,实现类型或子类在实现这些属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。
interface I {
prop a: Int64
mut prop b: Int64
}
class C <: I {
private var value = 0
public prop a: Int64 {
get() {
value
}
}
public mut prop b: Int64 {
get() {
value
}
set(v) {
value = v
}
}
}
属性使用
class A {
private static var valueY: Int64 = 123
public mut static prop y: Int64 {
get() {
valueY
}
set(v) {
valueY = v
}
}
}
main() {
println(A.y)
A.y = 321
println(A.y)
return 0
}
// 123
// 321
子类型关系
与其他面向对象语言一样,仓颉语言提供子类型关系和子类型多态。
- 假设函数的形参是类型
T,则函数调用时传入的参数的实际类型既可以是T也可以是T的子类型(严格地说,T的子类型已经包括T自身,下同)。 - 假设赋值表达式
=左侧的变量的类型是T,则=右侧的表达式的实际类型既可以是T也可以是T的子类型。 - 假设函数定义中用户标注的返回类型是
T,则函数体的类型既可以是T也可以是T的子类型。
-
继承clas带来的子类型关系
-
实现接口带来的子类型关系
实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。
-
元组类型的子类型关系
如果一个元组
t1的每个元素的类型都是另一个元组t2的对应位置元素类型的子类型,那么元组t1的类型也是元组t2的类型的子类型。open class C1 {} class C2 <: C1{} open class C3 {} class C4 <: C3 {} main() { let t: (C1, C3) = (C2(), C4()) return 0 } -
函数类型的子类型关系
给定两个函数类型
(U1) -> S2和(U2) -> S1,如果存在(U1) -> S2是(U2) -> S1的子类型,当且仅当U2是U1的子类型,且S2是S1的子类型(S2 <: S1且U2 <: U1)。 -
永远成立的子类型关系
- 一个类型
T永远是自身的子类型,即T <: T。 Nothing类型永远是其他任意类型T的子类型,即Nothing <: T。- 任意类型
T都是Any类型的子类型,即T <: Any。 - 任意
class定义的类型都是Object的子类型,即如果有class C {},则C <: Object。
- 一个类型
-
传递性带来的子类型关系
类型转换
仓颉不支持不同类型之间的隐式转换(子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。
open class Base {}
class Sub <: Base {}
main() {
let b: Base = Sub() // 子类型天然是父类型
let x: Float64 = 100 // 错误,不支持隐式转换
return 0
}
-
数值类型之间的转换
对于数值类型(包括:
Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64),仓颉支持使用T(e)的方式得到一个值等于e,类型为T的值。main() { let x: IntNative = 100 let y = Float64(x) println(x) println(y) } -
Rune到UInt32和整数类型到Rune的转换
Rune到UInt32的转换使用UInt32(e)的方式,其中e是一个Rune类型的表达式,UInt32(e)的结果是e的 Unicode scalar value 对应的UInt32类型的整数值。整数类型到
Rune的转换使用Rune(num)的方式,其中num的类型可以是任意的整数类型,且仅当num的值落在[0x0000, 0xD7FF]或[0xE000, 0x10FFFF](即 Unicode scalar value)中时,返回对应的 Unicode scalar value 表示的字符,否则,编译报错(编译时可确定num的值)或运行时抛异常。
is和as操作符
仓颉支持使用 is 操作符来判断某个表达式的类型是否是指定的类型(或其子类型)。具体而言,对于表达式 e is T(e 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e is T 的值为 true,否则 e is T 的值为 false。
open class A {}
class B <: A {}
main() {
let aa: A = A()
let ab: A = B()
let bb: B = B()
// let ba: B = A() // error
// 子类就是父类
println("aa is A = ${aa is A}")
println("aa is B = ${aa is B}")
println("ab is A = ${ab is A}")
println("ab is B = ${ab is B}")
println("bb is A = ${bb is A}")
println("bb is B = ${bb is B}")
}
// aa is A = true
// aa is B = false
// ab is A = true
// ab is B = true
// bb is A = true
// bb is B = true
as 操作符可以用于将某个表达式的类型转换为指定的类型。因为类型转换有可能会失败,所以 as 操作返回的是一个 Option 类型。具体而言,对于表达式 e as T(e 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option<T>.Some(e),否则 e as T 的值为 Option<T>.None。
Collention类型概述
- Array:不需要增加和删除元素,但需要修改元素
- ArrayList:需要频繁对元素增删查改
- HashSet:希望每个元素都是唯一的
- HashMap:希望存储一系列的映射关系
(学这个主要是用于替代C++中的vector、set、map等类)
| 类型名称 | 元素可变 | 增删元素 | 元素唯一性 | 有序序列 |
|---|---|---|---|---|
| Array | Y | N | N | Y |
| ArrayList | Y | Y | N | Y |
| HashSet | N | Y | Y | N |
| HashMap<K, V> | K: N, V: Y | Y | K: Y, V: N | N |
ArrayList
仓颉使用 ArrayList 表示 ArrayList 类型,T 表示 ArrayList 的元素类型,T 可以是任意类型。
let a = ArrayList<String>()
let b = ArrayList<String>(100)
let c = ArrayList<Int64>([1, 2, 3, 4, 5])
let d = ArrayList<Int64>(c)
let e = ArrayList<String>(2, {x: Int64 => x.toString()})
访问ArrayList成员
使用for-in循环遍历。
main(){
let list = ArrayList<String>(5, {x: Int64 => x.toString()})
println("Size is ${list.size})
for(i in list) {
println("The item is: ${i}")
}
}
// The item is: 0
// The item is: 1
// The item is: 2
// The item is: 3
// The item is: 4
修改ArrayList
可以使用下标语法对某个位置的元素进行修改。ArrayList 是引用类型,ArrayList 在作为表达式使用时不会拷贝副本,同一个 ArrayList 实例的所有引用都会共享同样的数据。
main(){
let list = ArrayList<Int64>([1, 2, 3, 4, 5])
for(i in 0..5) {
list[i] += 1
}
println(list)
}
// [2, 3, 4, 5, 6]
使用add函数在末尾添加元素,使用add(all!: Collection)同时添加多个元素。
main(): Int64 {
let list = ArrayList<Int64>([0, 1, 2, 3, 4])
list.add(all: [6, 6, 6])
println(list)
return 0
}
// [0, 1, 2, 3, 4, 6, 6, 6]
可以通过 add(T, at!: Int64) 和 add(all!: Collection<T>, at!: Int64) 函数将指定的单个元素或相同元素类型的 Collection 值插入到指定索引的位置。该索引处的元素和后面的元素会被挪后以腾出空间。
从 ArrayList 中删除元素,可以使用 remove 函数,需要指定删除的索引。该索引处后面的元素会前移以填充空间。
main(): Int64 {
let list = ArrayList<Int64>([0, 1, 2, 3, 4])
list.add(all: [6, 6, 6], at: 2)
println(list)
list.remove(at: 2)
println(list)
return 0
}
// [0, 1, 6, 6, 6, 2, 3, 4]
// [0, 1, 6, 6, 2, 3, 4]
HashSet
main(): Int64 {
let hs = HashSet<Int64>(10, {x: Int64 => (x * x)})
println(hs)
return 0
}
// [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
修改HashSet
HashSet 是一种可变的引用类型,HashSet 类型提供了添加元素、删除元素的功能。
使用 add 函数添加元素,使用remove函数删除指定的元素。
main(): Int64 {
let hs = HashSet<Int64>(10, {x: Int64 => x})
for(i in 1..5) {
hs.add(i + 10)
hs.remove(i)
}
println(hs)
return 0
}
HashMap
HashMap 是一种哈希表,提供对其包含的元素的快速访问。表中的每个元素都使用其键作为标识,可以使用键来访问相应的值。
仓颉使用 HashMap<K, V> 表示 HashMap 类型,K 表示 HashMap 的键类型,K 必须是实现了 Hashable 和 Equatable<K> 接口的类型,例如数值或 String。V 表示 HashMap 的值类型,V 可以是任意类型。
main(): Int64 {
let hm = HashMap<String, Int64>()
hm["one"] = 1
hm["two"] = 2
hm["three"] = 3
println(hm)
return 0
}
// [(one, 1), (two, 2), (three, 3)]
访问HashMap成员
用for-in循环访问HashMap成员
main(): Int64 {
let hm = HashMap<String, Int64>()
hm["one"] = 1
hm["two"] = 2
hm["three"] = 3
for((k, v) in hm) {
println("key is ${k}, value is ${v}")
}
for(kv in hm) {
println("key is ${kv[0]}, value is ${kv[1]}")
}
return 0
}
修改HashMap
使用下标语法对某个键对应的值进行修改。
如果希望同时添加多个键值对,可以使用 add(all!: Collection<(K, V)>) 函数。当键不存在时,add 函数会执行添加的操作,当键存在时,add 函数会将新的值覆盖旧的值。也可以使用赋值的方式直接将新的键值对添加到 HashMap。使用remove函数指定删除的键。
main(): Int64 {
let hm = HashMap<String, Int64>()
hm["one"] = 1
hm["two"] = 2
hm["three"] = 3
hm.add("two", 666)
println(hm)
hm.remove("three")
println(hm)
return 0
}
// [(one, 1), (two, 666), (three, 3)]
// [(one, 1), (two, 666)]

被折叠的 条评论
为什么被折叠?



