1 概述
1.1 案例介绍
仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE工具链支持,为开发者打造友好开发体验和卓越程序性能。
本案例将介绍仓颉中的反射和注解,并结合简单的编程代码进行知识体验,仓颉开发环境我们使用了开发者空间提供的云主机环境,环境已经预装了仓颉工具链和CodeArts IDE for Cangjie,即开即用,非常便捷。
案例结合代码体验,让大家更直观的了解仓颉语言中的反射和注解。
1.2 适用对象
- 个人开发者
- 高校学生
1.3 案例时间
本案例总时长预计40分钟。
1.4 案例流程
说明:
- 进入华为开发者空间,登录云主机;
- 使用CodeArts IDE for Cangjie编程和运行仓颉代码。
1.5 资源总览
资源名称 | 规格 | 单价(元) | 时长(分钟) |
---|---|---|---|
开发者空间 - 云主机 | 鲲鹏通用计算增强型 kc2 | 4vCPUs | 8G | Ubuntu | 免费 | 40 |
仓颉之反射和注解的神秘力量 👈👈👈体验完整版案例,点击这里。
2 运行测试环境准备
2.1 开发者空间配置
面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。
领取云主机后可以直接进入华为开发者空间工作台界面,点击打开云主机 > 进入桌面连接云主机。没有领取在开发者空间根据指引领取配置云主机即可,云主机配置参考1.5资源总览。
2.2 创建仓颉程序
点击桌面CodeArts IDE for Cangjie,打开编辑器,点击新建工程,保持默认配置,点击创建。
产物类型说明:
- executable,可执行文件;
- static,静态库,是一组预先编译好的目标文件的集合;
- dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。
2.3 运行仓颉工程
创建完成后,打开src/main.cj,参考下面代码简单修改后,点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
package demo
// 第一个仓颉程序
main(): Int64 {
println("hello world")
println("你好,仓颉!")
return 0
}
(* 注意:后续替换main.cj文件代码时,package demo保留)
(* 仓颉注释语法:// 符号之后写单行注释,也可以在一对 /* 和 */ 符号之间写多行注释)
到这里,我们第一个仓颉程序就运行成功啦!后面案例中的示例代码都可以放到main.cj文件中进行执行,接下来我们继续探索仓颉语言。
3 反射和注解
3.1 动态特性
仓颉的动态特性主要包含反射、动态加载。
3.1.1 仓颉反射基本介绍
反射指程序可以访问、检测和修改它本身状态或行为的一种机制。
反射这一动态特性有以下的优点:
- 提高了程序的灵活性和扩展性。
- 程序能够在运行时获悉各种对象的类型,对其成员进行枚举、调用等操作。
- 允许在运行时创建新类型,无需提前硬编码。
但使用反射调用,其性能通常低于直接调用,因此反射机制主要应用于对灵活性和拓展性要求很高的系统框架上。
3.1.2 如何获得TypeInfo
什么是TypeInfo?
TypeInfo 是一个类型,这个核心类型中记录任意类型的类型信息,并且定义了方法用于获取类型信息、设置值等。
可以使用三种静态的 of 方法来生成 TypeInfo 信息类。
例如可以用反射来获取一个自定义类型的类型信息,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.*
class Foo {}
main() {
let a: Foo = Foo()
let info1: TypeInfo = TypeInfo.of(a)
let info2: TypeInfo = TypeInfo.of<Foo>()
println(info1)
println(info2)
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
此外,为配合动态加载使用,TypeInfo 还提供了静态函数 get,该接口可通过传入的类型名称获取 TypeInfo。
具体代码操作如下:
注意:传入的类型名称和您的工程名称是对应的。
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.*
class Foo {}
main() {
// get方法,通过传入类型名称获取TypeInfo
// 类型名称是上个步骤中使用of方法生成的
let info: TypeInfo = TypeInfo.get("demo.Foo")
println(info)
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
采用get方法无法获取一个未实例化的泛型类型。
注意:传入的类型名称和您的工程名称是对应的。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.collection.*
import std.reflect.*
class A<T> {
A(public let t: T) {}
}
class B<T> {
B(public let t: T) {}
}
main() {
let b: B<Int64> = B<Int64>(1)
// Ok `default.B<Int64>` has been instantiated.
let bInfo: TypeInfo = TypeInfo.get("demo.B<Int64>")
println(bInfo)
// Error,`default.A<Int64>` is not instantiated,will throw InfoNotFoundException
let aInfo: TypeInfo = TypeInfo.get("demo.A<Int64>")
println(aInfo)
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
3.1.3 如何使用反射访问成员
在获取到对应的类型信息类即 TypeInfo 后,便可以通过其相应接口访问对应类的实例成员以及静态成员。
注意:仓颉的反射被设计为只能访问到类型内 public 的成员。
例如在运行时对类的某一实例成员变量进行获取与修改。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.*
public class Foo {
public static var param1 = 20
public var param2 = 10
}
main(){
let obj = Foo()
let info = TypeInfo.of(obj)
// 获取成员变量
let staticVarInfo = info.getStaticVariable("param1")
// 获取静态成员变量
let instanceVarInfo = info.getInstanceVariable("param2")
println("成员变量初始值")
print("Foo 的静态成员变量 ${staticVarInfo} = ")
println((staticVarInfo.getValue() as Int64).getOrThrow())
print("obj 的实例成员变量 ${instanceVarInfo} = ")
println((instanceVarInfo.getValue(obj) as Int64).getOrThrow())
println("--------------------------------------------")
println("更改成员变量")
// 更改静态成员变量
staticVarInfo.setValue(8)
// 更改实例成员变量
instanceVarInfo.setValue(obj, 25)
print("Foo 的静态成员变量 ${staticVarInfo} = ")
println((staticVarInfo.getValue() as Int64).getOrThrow())
print("obj 的实例成员变量 ${instanceVarInfo} = ")
println((instanceVarInfo.getValue(obj) as Int64).getOrThrow())
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
通过反射对属性进行检查以及修改。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.*
public class Foo {
public let _p1: Int64 = 1
// 属性p1没有使用mut修饰符,只能定义getter
public prop p1: Int64 {
get() { _p1 }
}
public var _p2: Int64 = 2
// 属性p2使用mut修饰符,必须分别定义getter和setter实现
public mut prop p2: Int64 {
get() { _p2 }
set(v) { _p2 = v }
}
}
main(): Unit{
let obj = Foo()
let info = TypeInfo.of(obj)
let instanceProps = info.instanceProperties.toArray()
println("obj的实例成员属性包含${instanceProps}")
let PropInfo1 = info.getInstanceProperty("p1")
let PropInfo2 = info.getInstanceProperty("p2")
print("获取原有属性值:p1=")
println((PropInfo1.getValue(obj) as Int64).getOrThrow())
print("获取原有属性值:p2=")
println((PropInfo2.getValue(obj) as Int64).getOrThrow())
println("-------------------")
// 属性p1没有使用mut修饰符,只能定义getter事项,不能定义setter实现,不能修改
if (PropInfo1.isMutable()) {
PropInfo1.setValue(obj, 10)
}
if (PropInfo2.isMutable()) {
PropInfo2.setValue(obj, 20)
}
print("修改后的属性值:p1=")
println((PropInfo1.getValue(obj) as Int64).getOrThrow())
print("修改后的属性值:p2=")
println((PropInfo2.getValue(obj) as Int64).getOrThrow())
return
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
通过反射机制进行函数调用,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.*
public class Foo {
public static func add(a: Int64, b: Int64): Int64 {
return a + b
}
}
main(): Unit {
var num = 0
// 获取参数的TypeInfo
let intInfo = TypeInfo.of<Int64>()
// 获取类Foo的TypeInfo
let classInfo = TypeInfo.of<Foo>()
// 获取静态函数的StaticFunctionInfo
let funcInfo = classInfo.getStaticFunction("add", intInfo, intInfo)
// 调用StaticFunctionInfo的apply方法,传入Array
num = (funcInfo.apply([1, 1]) as Int64).getOrThrow()
println(num)
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
3.2 注解
仓颉中提供了一些属性宏用来支持一些特殊情况的处理。
3.2.1 正确使用整数运算溢出的注解
仓颉中提供三种属性宏来控制整数溢出的处理策略,即 @OverflowThrowing,@OverflowWrapping 和 @OverflowSaturating ,这些属性宏当前只能标记于函数声明之上,作用于函数内的整数运算和整型转换。
- 抛出异常(throwing):当整数运算溢出时,抛出异常。
具体代码如下:
@OverflowThrowing
func add(a: Int8, b: Int8){
return a + b
}
main() {
add(100,29)
/* 100 + 29 在数学上等于 129,
* 在 Int8 的表示范围上发生了上溢出,
* 程序抛出异常
*/
0
}
- 高位截断(wrapping):当整数运算的结果超出用于接收它的内存空间所能表示的数据范围时,则截断超出该内存空间的部分。
具体代码如下:
@OverflowWrapping
main() {
let res: Int8 = Int8(105) * Int8(4)
/* 105 * 4 在数学上等于 420,
* 对应的二进制为 1 1010 0100,
* 超过了用于接收该结果的 8 位内存空间,
* 截断后的结果在二进制上表示为 1010 0100,
* 对应为有符号整数 -92
*/
let temp: Int16 = Int16(-132)
let con: UInt8 = UInt8(temp)
/* -132 对应的二进制为 1111 1111 0111 1100,
* 超过了用于接收该结果的 8 位内存空间,
* 截断后的结果在二进制上表示为 0111 1100
* 对应为有符号整数 124
*/
0
}
- 饱和(saturating):当整数运算溢出时,选择对应固定精度的极值作为结果。
具体代码如下:
@OverflowSaturating
main() {
let res: Int8 = Int8(-100) - Int8(45)
/* -100 - 45 在数学上等于 -145,
* 在 Int8 的表示范围上发生了下溢出,
* 选择 Int8 的最小值 -128 作为结果
*/
let con: Int8 = Int8(1024)
/* 1024 在 Int8 的表示范围上发生了上溢出,
* 选择 Int8 的最大值 127 作为结果
*/
0
}
3.2.2 性能优化注解
为了提升与 C语言互操作的性能,仓颉提供属性宏 @FastNative 控制 cjnative 后端优化对于 C 函数的调用。值得注意的是,属性宏 @FastNative 只能用于 foreign 声明的函数。
@FastNative使用限制:
开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的C 函数满足以下两点要求。
- 首先,函数的整体执行时间不宜太长;
- 其次,函数内部不能调用仓颉方法。
3.2.3 自定义注解
自定义注解机制用来让反射获取标注内容,目的是在类型元数据之外提供更多的有用信息,以支持更复杂的逻辑。
开发者可以通过自定义类型标注 @Annotation 方式创建自己的自定义注解。@Annotation 只能修饰 class,并且不能是 abstract 或 open 或 sealed 修饰的 class。当一个 class 声明它标注了 @Annotation,那么它必须要提供至少一个 const init 函数,否则编译器会报错。
下面的例子定义了一个自定义注解 @Version,并用其修饰 A, B 和 C。在 main 中,通过反射获取到类上的 @Version 注解信息,并将其打印出来。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.TypeInfo
@Annotation
public class Version {
let code: String
const init(code: String) {
this.code = code
}
}
@Version["1.0"]
class A {}
@Version["1.1"]
class B {}
main() {
let objects = [A(), B()]
for (obj in objects) {
let annOpt = TypeInfo.of(obj).findAnnotation<Version>()
if (let Some(ann) <- annOpt) {
if (let Some(version) <- ann as Version) {
println(version.code)
}
}
}
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
自定义注解在使用时必须使用 const init 构建出合法的实例。注解声明语法与声明宏语法一致,后面的[ ]括号中需要按顺序或命名参数规则传入参数,且参数必须是 const 表达式。
下面的例子中定义了一个拥有无参 const init 的自定义注解 @Marked,使用时 @Marked 和 @Marked[] 这两种写法均可。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.TypeInfo
@Annotation
public class Marked {
const init() {}
}
@Marked
class A {}
@Marked[]
class B {}
main() {
if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) {
println("A is Marked")
}
if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) {
println("B is Marked")
}
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
Annotation 不会被继承,因此一个类型的注解元数据只会来自它定义时声明的注解。如果需要父类型的注解元数据信息,需要开发者自己用反射接口查询。
下面的例子中,A 被 @Marked 注解修饰,B 继承 A,但是 B 没有 A 的注解。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
import std.reflect.TypeInfo
@Annotation
public class Marked {
const init() {}
}
@Marked
open class A {}
class B <: A {}
main() {
if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) {
println("A is Marked")
}
if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) {
println("B is Marked")
}
}
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
至此,仓颉之反射和注解的神秘力量案例内容已全部完成。
如果想了解更多仓颉编程语言知识可以访问:https://cangjie-lang.cn/