Deriving

一个简单示例:

import std.deriving.*

@Derive[ToString]
class User {
    User(
        let name: String,
        let id: Int
    ) {}
}

main() {
    println(User("root", 0)) // -> "User(name = root, id = 0)"
}

@Derive[ToString] 应用于类或结构体时, Deriving 会收集和使用类或结构体的可变和不可变字段,包括在主构造函数中指定的字段,并自动实现 ToString 的方法。当 @Derive[ToString] 应用于枚举时, Deriving 将收集枚举的构造函数参数。静态字段和属性将不会被收集和使用,另外, Deriving 收集的字段不允许存在私有字段,否则将抛出编译错误。

收集到的字段用于 Deriving 时,其类型需要实现目标接口,以便将字段结果组合在一起。例如,当处理 ToString 时,生成的代码将在所有收集到的字段上调用 toString ,然后将结果与对应的字段名称连接成一个字符串。如果其中一个字段的类型不支持 ToString ,则会抛出编译错误并且 Deriving 无法完成。

注意

标记为派生的类应该是最终的:它不应该是开放的、抽象的或 sealed 的。

有些字段可能具有特殊含义,它们的值没有多大意义,则可通过在这些字段上应用 @DeriveExclude 来排除这些字段:

@Derive[ToString]
class User {
    User(let name: String) {}

    @DeriveExclude
    let lazyHashCode = 0 // it will not be printed because it's excluded
}

默认情况 Deriving 仅使能字段,对于属性则需要通过 @DeriveInclude 来显式使能:

@Derive[ToString]
class User {
    User(
        let id: Int
    ) {}

    @DeriveInclude
    prop name: String {
        get() { passwdFile.getUserName(id) }
    }
}

main() {
    println(User(0)) // -> "User(id = 0, name = root)"
}

请注意,因为属性 name 是在 id 之后声明的,因此打印的顺序为先 idname

如果需要更改打印的顺序,可以使用 @DeriveOrder

@Derive[ToString]
@DeriveOrder(name, id)
class User {
    User(
        let id: Int
    ) {}

    @DeriveInclude
    prop name: String {
        get() { passwdFile.getUserName(id) }
    }
}

main() {
    println(User(0)) // -> "User(name = root, id = 0)"
}

常见的 Deriving 语法

@Derive 宏支持以逗号分隔的接口名称列表。此外,该宏可以重复多次被调用,但所有 @Derive 宏调用都应位于声明的顶部,而其他宏(如 @DeriveOrder )应始终位于其后。

支持的接口列表的顺序没有影响。

@Derive[ToString, Hashable]
@Derive[Equatable]
@DeriveOrder[currency,price,quantity]
struct Order {}

当 Deriving 多个相交的接口时,例如,Comparable 还包括 Equatable ,则允许两者同时存在,等同于仅有范围最广的一个:

@Derive[Comparable] // does also generate Equatable

等同于:

@Derive[Comparable, Equatable]

包含和排除

默认情况下会处理所有字段,包括定义为主构造函数参数的字段。 当需要排除某个字段时,可以对其应用 @DeriveExclude

@Derive[ToString]
struct S {
    S(let id) { key = "s_${id}" }

    @DeriveExclude
    let key: String
}

默认情况下不处理属性,需要通过 @DeriveInclude 包含属性。

@Derive[ToString]
struct S {
    S(let id) {}

    @DeriveInclude
    prop key: String {
        get() { "s_${id}" }
    }
}

被 Deriving 的字段和属性都不能是 private 的。因此,private 的字段或者属性应被除外或者使其为包内可见属性。

注意

静态的字段和属性始终会被忽略,因此他们都不能被 @DeriveInclude@DeriveInclude 修饰。

支持的接口

当前仅支持如下接口:

  • ToString
  • Hashable
  • Equatable
  • Comparable

暂不支持用户自定义的接口。

变更顺序

在对由多个字段组成的复杂类型的实例进行排序和比较时,测试字段的顺序通常很重要。默认情况下,所有字段都按声明顺序考虑。可以使用 @DeriveOrder 宏修改顺序。

@Derive[Comparable, ToString]
struct Floor {
    Floor(
        let level: Int,
        let building: Int
    ) {}
}

main() {
    let floors = [
        Floor(1, 2),
        Floor(3, 2),
        Floor(2, 1)
    ]
    floors.sort()
    for (f in floors) { println(f) }
}

上述示例将打印以下内容,看起来顺序没有很大影响。

Floor(level = 1, building = 2)
Floor(level = 2, building = 1)
Floor(level = 3, building = 2)

但是当我实现 Comparable 时,不同的顺序将影响结果。

@Derive[Comparable, ToString]
@DeriveOrder[building, level]
struct Floor {
    Floor(
        let level: Int,
        let building: Int
    ) {}
}

此时,结果将首先按 building 排序,然后按 level 排序:

Floor(building = 1, level = 2)
Floor(building = 2, level = 1)
Floor(building = 2, level = 3)

泛型

实现泛型类型的接口通常需要应用约束,以便类型仅在某些条件下实现接口。例如:

class Cell<T> {
    Cell(let value: T) {}
}

此时可能希望仅当单元格的值可打印时才能够打印该单元格。为了实现它,我们将编写一个带有约束的扩展:

extend <T> Cell<T> <: ToString where T <: ToString {
    public func toString(): String {
        "Cell(value = ${value})"
    }
}

当使用 Deriving 时,它会默认尝试对所有泛型参数应用约束,因此以下内容与上面的扩展相同:

@Derive[ToString]
class Cell<T> {
    Cell(let value: T) {}
}

然而在某些情况下,默认行为并不符合期望。此时,可使用 @Derive 内部的 where 来覆盖默认约束:

interface PrintableCellValue <: ToString { ... }

@Derive[ToString where T <: PrintableCellValue]
class Cell<T> {}

请注意,在上面的示例中,自定义约束仅适用于 ToString ,因此如果需要对所有接口进行约束,则应单独为每个接口重复此动作。

@Derive[ToString where T <: PrintableCellValue]
@Derive[Hashable where T <: PrintableCellValue & Hashable]
class Cell<T> {}

性能说明

由于 Deriving 是基于仓颉宏的,不涉及任何反射,因此 Deriving 实现的运行时性能与手写相当。但是,Deriving 涉及编译时的代码转换,因此它会影响编译时间。