Kotlin 生成代码指南

精确描述了协议缓冲区编译器为任何给定的协议定义生成哪些 Kotlin 代码,以及为 Java 生成的代码。

本文档着重介绍 proto2 和 proto3 生成代码之间的任何差异——请注意,这些差异在于本文档中描述的生成代码,而不是在这两个版本中都相同的基本消息类/接口。在阅读本文档之前,您应该阅读proto2 语言指南和/或proto3 语言指南

编译器调用

协议缓冲区编译器生成的 Kotlin 代码基于 Java 代码构建。因此,它必须使用两个命令行标志调用:--java_out=--kotlin_out=--java_out= 选项的参数是您希望编译器写入 Java 输出的目录,--kotlin_out= 也是如此。对于每个 .proto 输入文件,编译器都会创建一个包装器 .java 文件,其中包含一个代表 .proto 文件本身的 Java 类。

无论您的 .proto 文件是否包含以下类似的行

option java_multiple_files = true;

编译器将为它为 .proto 文件中声明的每个顶层消息生成的每个类和工厂方法创建单独的 .kt 文件。

每个文件的 Java 包名与生成的 Java 代码使用的包名相同,具体描述请参阅Java 生成代码参考

输出文件是通过连接 --kotlin_out= 的参数、包名(将句点 [.] 替换为斜杠 [/])和后缀 Kt.kt 文件名来确定的。

例如,假设您按如下方式调用编译器

protoc --proto_path=src --java_out=build/gen/java --kotlin_out=build/gen/kotlin src/foo.proto

如果 foo.proto 的 Java 包是 com.example 并且包含一个名为 Bar 的消息,那么协议缓冲区编译器将生成文件 build/gen/kotlin/com/example/BarKt.kt。如果需要,协议缓冲区编译器将自动创建 build/gen/kotlin/combuild/gen/kotlin/com/example 目录。但是,它不会创建 build/gen/kotlinbuild/genbuild;这些目录必须已经存在。您可以在一次调用中指定多个 .proto 文件;所有输出文件将同时生成。

消息

给定一个简单的消息声明

message FooBar {}

除了生成的 Java 代码外,协议缓冲区编译器还会生成一个名为 FooBarKt 的对象,以及两个顶层函数,其结构如下

object FooBarKt {
  class Dsl private constructor { ... }
}
inline fun fooBar(block: FooBarKt.Dsl.() -> Unit): FooBar
inline fun FooBar.copy(block: FooBarKt.Dsl.() -> Unit): FooBar

嵌套类型

消息可以声明在另一个消息内部。例如

message Foo {
  message Bar { }
}

在这种情况下,编译器将 BarKt 对象和 bar 工厂方法嵌套在 FooKt 中,尽管 copy 方法仍然是顶层的

object FooKt {
  class Dsl { ... }
  object BarKt {
    class Dsl private constructor { ... }
  }
  inline fun bar(block: FooKt.BarKt.Dsl.() -> Unit): Foo.Bar
}
inline fun foo(block: FooKt.Dsl.() -> Unit): Foo
inline fun Foo.copy(block: FooKt.Dsl.() -> Unit): Foo
inline fun Foo.Bar.copy(block: FooKt.BarKt.Dsl.() -> Unit): Foo.Bar

字段

除了前一节中描述的方法外,协议缓冲区编译器还会在 DSL 中为 .proto 文件中消息内定义的每个字段生成可变属性。(Kotlin 已经从 Java 生成的 getter 中推断出消息对象上的只读属性。)

请注意,属性始终使用驼峰命名法,即使 .proto 文件中的字段名使用下划线分隔的小写字母(这是推荐的)。大小写转换规则如下:

  1. 名称中的每个下划线都会被移除,并且其后的字母会大写。
  2. 如果名称附加了前缀(例如,“clear”),则第一个字母会大写。否则,会小写。

因此,字段 foo_bar_baz 变为 fooBarBaz

在少数特殊情况下,当字段名与 Kotlin 中的保留字或 protobuf 库中已定义的方法冲突时,会附加一个额外的下划线。例如,字段名 in 的 clearer 方法是 clearIn_()

单个字段 (proto2)

对于任何这些字段定义

optional int32 foo = 1;
required int32 foo = 1;

编译器将在 DSL 中生成以下访问器

  • fun hasFoo(): Boolean: 如果字段已设置,则返回 true
  • var foo: Int: 字段的当前值。如果字段未设置,则返回默认值。
  • fun clearFoo(): 清除字段的值。调用此方法后,hasFoo() 将返回 falsegetFoo() 将返回默认值。

对于其他简单的字段类型,会根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型会被替换为消息或枚举类。由于消息类型仍在 Java 中定义,消息中的无符号类型在 DSL 中使用标准的对应有符号类型表示,以兼容 Java 和旧版本的 Kotlin。

嵌入式消息字段

请注意,对子消息没有特殊处理。例如,如果您有一个字段

optional Foo my_foo = 1;

您必须编写

myFoo = foo {
  ...
}

通常,这是因为编译器根本不知道 Foo 是否具有 Kotlin DSL,或者例如仅生成了 Java API。这意味着您无需等待依赖的消息添加 Kotlin 代码生成。

单个字段 (proto3)

对于此字段定义

int32 foo = 1;

编译器将在 DSL 中生成以下属性

  • var foo: Int: 返回字段的当前值。如果字段未设置,则返回该字段类型的默认值。
  • fun clearFoo(): 清除字段的值。调用此方法后,getFoo() 将返回该字段类型的默认值。

对于其他简单的字段类型,会根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型会被替换为消息或枚举类。由于消息类型仍在 Java 中定义,消息中的无符号类型在 DSL 中使用标准的对应有符号类型表示,以兼容 Java 和旧版本的 Kotlin。

嵌入式消息字段

对于消息字段类型,DSL 中会生成一个额外的访问器方法

  • boolean hasFoo(): 如果字段已设置,则返回 true

请注意,没有基于 DSL 设置子消息的快捷方式。例如,如果您有一个字段

Foo my_foo = 1;

您必须编写

myFoo = foo {
  ...
}

通常,这是因为编译器根本不知道 Foo 是否具有 Kotlin DSL,或者例如仅生成了 Java API。这意味着您无需等待依赖的消息添加 Kotlin 代码生成。

重复字段

对于此字段定义

repeated string foo = 1;

编译器将在 DSL 类中生成以下成员

  • class FooProxy: DslProxy,一个仅用于泛型的不可构造类型
  • val fooList: DslList<String, FooProxy>,重复字段中当前元素的只读视图列表
  • fun DslList<String, FooProxy>.add(value: String),一个扩展函数,允许向重复字段添加元素
  • operator fun DslList<String, FooProxy>.plusAssign(value: String),是 add 的别名,使用运算符语法
  • fun DslList<String, FooProxy>.addAll(values: Iterable<String>),一个扩展函数,允许将元素的 Iterable 添加到重复字段
  • operator fun DslList<String, FooProxy>.plusAssign(values: Iterable<String>),是 addAll 的别名,使用运算符语法
  • operator fun DslList<String, FooProxy>.set(index: Int, value: String),一个扩展函数,用于设置给定从零开始的索引处的元素值
  • fun DslList<String, FooProxy>.clear(),一个扩展函数,用于清除重复字段的内容

这种不寻常的构造允许 fooList 在 DSL 范围内“表现得像”一个可变列表,仅支持底层 builder 支持的方法,同时防止可变性“逃逸”出 DSL,从而可能导致令人困惑的副作用。

对于其他简单的字段类型,会根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,其类型就是消息或枚举类。

Oneof 字段

对于此 oneof 字段定义

oneof oneof_name {
    int32 foo = 1;
    ...
}

编译器将在 DSL 中生成以下访问器方法

  • val oneofNameCase: OneofNameCase: 获取 oneof_name 字段中是否有任何字段已设置;返回类型请参阅Java 代码参考
  • fun hasFoo(): Boolean (仅限 proto2): 如果 oneof 情况是 FOO,则返回 true
  • val foo: Int: 如果 oneof 情况是 FOO,则返回 oneof_name 的当前值。否则,返回此字段的默认值。

对于其他简单的字段类型,会根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型会被替换为消息或枚举类。

Map 字段

对于此 map 字段定义

map<int32, int32> weight = 1;

编译器将在 DSL 类中生成以下成员

  • class WeightProxy private constructor(): DslProxy(),一个仅用于泛型的不可构造类型
  • val weight: DslMap<Int, Int, WeightProxy>,map 字段中当前条目的只读视图
  • fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int): 将条目添加到此 map 字段
  • operator fun DslMap<Int, Int, WeightProxy>.put(key: Int, value: Int): 使用运算符语法的 put 别名
  • fun DslMap<Int, Int, WeightProxy>.remove(key: Int): 如果存在,则删除与 key 关联的条目
  • fun DslMap<Int, Int, WeightProxy>.putAll(map: Map<Int, Int>): 将指定 map 中的所有条目添加到此 map 字段,覆盖已存在键的先前值
  • fun DslMap<Int, Int, WeightProxy>.clear(): 清除此 map 字段中的所有条目

扩展 (仅限 proto2)

给定一个带有扩展范围的消息

message Foo {
  extensions 100 to 199;
}

协议缓冲区编译器将向 FooKt.Dsl 添加以下方法

  • operator fun <T> get(extension: ExtensionLite<Foo, T>): T: 获取 DSL 中扩展字段的当前值
  • operator fun <T> get(extension: ExtensionLite<Foo, List<T>>): ExtensionList<T, Foo>: 以只读 List 的形式获取 DSL 中重复扩展字段的当前值
  • operator fun <T : Comparable<T>> set(extension: ExtensionLite<Foo, T>): 设置 DSL 中扩展字段的当前值(对于 Comparable 字段类型)
  • operator fun <T : MessageLite> set(extension: ExtensionLite<Foo, T>): 设置 DSL 中扩展字段的当前值(对于消息字段类型)
  • operator fun set(extension: ExtensionLite<Foo, ByteString>): 设置 DSL 中扩展字段的当前值(对于 bytes 字段)
  • operator fun contains(extension: ExtensionLite<Foo, *>): Boolean: 如果扩展字段有值,则返回 true
  • fun clear(extension: ExtensionLite<Foo, *>): 清除扩展字段
  • fun <E> ExtensionList<Foo, E>.add(value: E): 向重复扩展字段添加一个值
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(value: E): 使用运算符语法的 add 别名
  • operator fun <E> ExtensionList<Foo, E>.addAll(values: Iterable<E>): 向重复扩展字段添加多个值
  • operator fun <E> ExtensionList<Foo, E>.plusAssign(values: Iterable<E>): 使用运算符语法的 addAll 别名
  • operator fun <E> ExtensionList<Foo, E>.set(index: Int, value: E): 设置指定索引处重复扩展字段的元素
  • inline fun ExtensionList<Foo, *>.clear(): 清除重复扩展字段的元素

此处的泛型很复杂,但其效果是 this[extension] = value 适用于除重复扩展之外的所有扩展类型,并且重复扩展具有与非扩展重复字段类似工作的“自然”列表语法。

给定一个扩展定义

extend Foo {
  optional int32 bar = 123;
}

Java 生成“扩展标识符” bar,它用于“键控”上述扩展操作。