仓颉语言学习笔记(5)类class、接口interface、属性、子类型关系、类型转换、Collection类型
class定义
注意:
- class 仅能在源文件的顶层作用域中定义。
- 抽象类内不允许定义私有的抽象方法;
- 无法为抽象类创建对象;
- 抽象类的非抽象子类需实现父类的所有抽象方法。
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 类型名访问;静态成员方法中不能访问实例成员变量,也不能调用实例成员方法,但在实例成员方法中可以访问静态成员变量以及静态成员方法。
根据是否拥有方法体,实例成员方法还可以分为
抽象成员方法
和
非抽象成员方法
。抽象成员方法没有方法体,只能定义在抽象类或接口中。需要注意的是,抽象实例成员方法默认具备
可覆盖
的特性,可覆盖 修饰符是可选的,且
必须使用 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。这四种访问修饰符的可见范围按从小到大排序。
private 表示在
class 定义内
可见。
internal 表示仅在
当前包及其子包内
可见。
protected 表示在
当前模块及当前类的子类
可见。
public 表示在
模块内外
均可见。
This类型
在类内部,支持
This 类型占位符
,代表
当前类的类型
。它
仅能作为实例成员方法的返回类型使用
,当使用子类对象调用在父类中定义的返回 This 类型的方法时,该方法调用的类型将被视为子类类型,而不是定义所在父类的类型。
如果实例成员方法没有声明返回类型,并且只包含返回 This 类型的表达式时,当前方法的返回类型会被推断为 This。示例如下:
open class C1 {
func f(): This { // 其类型为 `() -> C1`
return this
}
func f2() { // 其类型为 `() -> C1`
return this
}
public open func f3(): C1 {
return this
}
}
class C2 <: C1 {
// 成员方法 f 从 C1 继承而来,其类型现在为 `() -> C2`
public override func f3(): This { // 正确
return this
}
}
var obj1: C2 = C2()
var obj2: C1 = C2()
var x = obj1.f() // 编译期间,x 的类型为 C2
var y = obj2.f() // 编译期间,y 的类型为 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} 发出声音")
}
}
class Cat <: Animal {
init(name: String) {
super(name)
}
public override func sound() {
println("${name} 喵喵叫")
}
}
对于定义时指定了父类的
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("汪!")
}
}
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 {
return 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 and 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
的子类型
。
继承类带来的子类型关联 实现接口带来的子类型关联 实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。
元组类型的子类型关联 如果一个元组
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
。
Collection 类型概述
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)
}利用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 是一个可更改的引用类别,提供了添加和移除元素的能力。
通过 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>
接口的类型,比如数字或字符串。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)]


雷达卡


京公网安备 11010802022788号







