前言
上一篇文章里我们基本上讲明白了KotlinPoet
是如何生成.kt
源文件的。KotlinPoet
给Kotlin中的每个实体都创建了Model
,文件-FileSpec
、类、接口和对象-TypeSpec
、类型别名-TypeAliasSpec
、属性-PropertySpec
、方法和构造方法-FunSpec
、参数-ParameterSpec
和注解-AnnotationSpec
。
但是KotlinPoet
并没有给方法体提供Model
。而是直接使用字符串代替代码块,于是字符串中的占位符逻辑和应用就是很重要的一环。
代码和控制流
用%S占位字符串
在我们构造代码块的时候,可以用%S
作为一个字符串的占位符:
fun main(args: Array<String>) {
val helloWorld = TypeSpec.classBuilder("HelloWorld")
.addFunction(whatsMyNameYo("slimShady"))
.addFunction(whatsMyNameYo("eminem"))
.addFunction(whatsMyNameYo("marshallMathers"))
.build()
val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld")
.addType(helloWorld)
.build()
kotlinFile.writeTo(System.out)
}
private fun whatsMyNameYo(name: String): FunSpec {
return FunSpec.builder(name)
.returns(String::class)
.addStatement("return %S", name)
.build()
}
会生成(如果return语句就是方法体的第一行,就会写成kotlin中一行方法的形式):
class HelloWorld {
fun slimShady(): String = "slimShady"
fun eminem(): String = "eminem"
fun marshallMathers(): String = "marshallMathers"
}
用%P占位字符串模板
占位符%S
处理转义字符的时候,会进行变化来让转义字符表达字符本来的意思。例如美元符号$
,在Kotlin的字符串中被用来当作串模版使用,如果在Kotlin字符串中需要使用$
作为字符,就需要写成${'$'}
,占位符%S
会自动进行这种处理:
val stringWithADollar = "Your total is " + "$" + "50"
val funSpec = FunSpec.builder("printTotal")
.returns(String::class)
.addStatement("return %S", stringWithADollar)
.build()
会生成:
fun printTotal(): String = "Your total is ${'$'}50"
如果我们不需要这种处理,需要使用%P
,%P
不会处理符号的转义:
val amount = 50
val stringWithADollar = "Your total is " + "$" + "amount"
val funSpec = FunSpec.builder("printTotal")
.returns(String::class)
.addStatement("return %P", stringWithADollar)
.build()
会生成:
fun printTotal(): String = "Your total is $amount"
同时%P
还可以当作代码块CodeBlock
(此处的CodeBlock
是KotlinPoet
中的类)的占位符:
val file = FileSpec.builder("com.example", "Digits")
.addFunction(
FunSpec.builder("print")
.addParameter("digits", IntArray::class)
.addStatement("println(%P)", buildCodeBlock {
val contentToString = MemberName("kotlin.collections", "contentToString")
add("These are the digits: ${digits.%M()}", contentToString)
})
.build()
)
.build()
println(file)
会生成:
package com.example
import kotlin.IntArray
import kotlin.collections.contentToString
fun print(digits: IntArray) {
println("""These are the digits: ${digits.contentToString()}""")
}
用 %T占位类
可以用%T
来进行类型引用,KotlinPoet
可以为被%T
引用的类自动生成import
语句:
val today = FunSpec.builder("today")
.returns(Date::class)
.addStatement("return %T()", Date::class)
.build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
.addFunction(today)
.build()
val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld")
.addType(helloWorld)
.build()
kotlinFile.writeTo(System.out)
会生成以下.kt
文件,并包含必要的import
:
package com.example.helloworld
import java.util.Date
class HelloWorld {
fun today(): Date = Date()
}
上面我们通过传递Date::class
引用了一个在我们生成代码时就可用的类。同样我们可以通过ClassName
引用一个生成代码时不存在的类:
val hoverboard = ClassName("com.mattel", "Hoverboard")
val tomorrow = FunSpec.builder("tomorrow")
.returns(hoverboard)
.addStatement("return %T()", hoverboard)
.build()
KotlinPoet
会导入那个尚不存在的类并生成:
package com.example.helloworld
import com.mattel.Hoverboard
class HelloWorld {
fun tomorrow(): Hoverboard = Hoverboard()
}
ClassName
是KotlinPoet
中经常会用到的很重要的类,可以识别任何被声明的类。ClassName
还能识别数组类型、参数化类型、通配符类型、lambda 类型和类型变量。对每一种类型,KotlinPoet
中都有用于构建的类:
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
val hoverboard = ClassName("com.mattel", "Hoverboard")
val list = ClassName("kotlin.collections", "List")
val arrayList = ClassName("kotlin.collections", "ArrayList")
val listOfHoverboards = list.parameterizedBy(hoverboard)
val arrayListOfHoverboards = arrayList.parameterizedBy(hoverboard)
val thing = ClassName("com.misc", "Thing")
val array = ClassName("kotlin", "Array")
val producerArrayOfThings = array.parameterizedBy(WildcardTypeName.producerOf(thing))
val beyond = FunSpec.builder("beyond")
.returns(listOfHoverboards)
.addStatement("val result = %T()", arrayListOfHoverboards)
.addStatement("result += %T()", hoverboard)
.addStatement("result += %T()", hoverboard)
.addStatement("result += %T()", hoverboard)
.addStatement("return result")
.build()
val printThings = FunSpec.builder("printThings")
.addParameter("things", producerArrayOfThings)
.addStatement("println(things)")
.build()
KotlinPoet
会分解每种类型并在可能的情况下导入其组件:
package com.example.helloworld
import com.mattel.Hoverboard
import com.misc.Thing
import kotlin.Array
import kotlin.collections.ArrayList
import kotlin.collections.List
class HelloWorld {
fun beyond(): List<Hoverboard> {
val result = ArrayList<Hoverboard>()
result += Hoverboard()
result += Hoverboard()
result += Hoverboard()
return result
}
fun printThings(things: Array<out Thing>) {
println(things)
}
}
可空类型
KotlinPoet
支持可为空的类型。使用copy(``nullable = true``)
方法可把TypeName
转化成可空的类型(注意可空是配置在类型上的,而不是参数/属性上):
val java = PropertySpec.builder("java", String::class.asTypeName().copy(nullable = true))
.mutable()
.addModifiers(KModifier.PRIVATE)
.initializer("null")
.build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
.addProperty(java)
.addProperty("kotlin", String::class, KModifier.PRIVATE)
.build()
会生成:
class HelloWorld {
private var java: String? = null
private val kotlin: String
}
用%M占位参数
与类型一样,KotlinPoet
也为成员(函数和属性)提供了一个特殊的占位符%M
,如果代码中需要访问顶级成员和在对象内部声明的成员时,就需要使用%M
。用%M
引用成员时,需要一个MemberName
作为占位符的参数,KotlinPoet
会自动处理import
:
val createTaco = MemberName("com.squareup.tacos", "createTaco")
val isVegan = MemberName("com.squareup.tacos", "isVegan")
val file = FileSpec.builder("com.squareup.example", "TacoTest")
.addFunction(
FunSpec.builder("main")
.addStatement("val taco = %M()", createTaco)
.addStatement("println(taco.%M)", isVegan)
.build()
)
.build()
println(file)
上面的代码会生成:
package com.squareup.example
import com.squareup.tacos.createTaco
import com.squareup.tacos.isVegan
fun main() {
val taco = createTaco()
println(taco.isVegan)
}
%M
也可以用来引用扩展函数和扩展属性。不过需要确保导入的成员不会发生简单名称(不包括namespace的名称)冲突,否则导入将失败并且代码生成器输出将无法通过编译。不过KotlinPoet
提供了FileSpec.addAliasedImport()
用于为冲突创建别名导入:
val createTaco = MemberName("com.squareup.tacos", "createTaco")
val createCake = MemberName("com.squareup.cakes", "createCake")
val isTacoVegan = MemberName("com.squareup.tacos", "isVegan")
val isCakeVegan = MemberName("com.squareup.cakes", "isVegan")
val file = FileSpec.builder("com.squareup.example", "Test")
.addAliasedImport(isTacoVegan, "isTacoVegan")
.addAliasedImport(isCakeVegan, "isCakeVegan")
.addFunction(
FunSpec.builder("main")
.addStatement("val taco = %M()", createTaco)
.addStatement("val cake = %M()", createCake)
.addStatement("println(taco.%M)", isTacoVegan)
.addStatement("println(cake.%M)", isCakeVegan)
.build()
)
.build()
println(file)
KotlinPoet
会给2个同名的isVegan
成员生成别名导入:
package com.squareup.example
import com.squareup.cakes.createCake
import com.squareup.tacos.createTaco
import com.squareup.cakes.isVegan as isCakeVegan
import com.squareup.tacos.isVegan as isTacoVegan
fun main() {
val taco = createTaco()
val cake = createCake()
println(taco.isTacoVegan)
println(cake.isCakeVegan)
}
参数名称和运算符
MemberName
还支持运算符(或运算符重载),可以使用MemberName(String, KOperator)
或MemberName(ClassName, KOperator)
来导入和引用运算符。
val taco = ClassName("com.squareup.tacos", "Taco")
val meat = ClassName("com.squareup.tacos.ingredient", "Meat")
val iterator = MemberName("com.squareup.tacos.internal", KOperator.ITERATOR)
val minusAssign = MemberName("com.squareup.tacos.internal", KOperator.MINUS_ASSIGN)
val file = FileSpec.builder("com.example", "Test")
.addFunction(
FunSpec.builder("makeTacoHealthy")
.addParameter("taco", taco)
.beginControlFlow("for (ingredient %M taco)", iterator)
.addStatement("if (ingredient is %T) taco %M ingredient", meat, minusAssign)
.endControlFlow()
.addStatement("return taco")
.build()
)
.build()
println(file)
KotlinPoet
将导入并使用重载之后的运算符:
package com.example
import com.squareup.tacos.Taco
import com.squareup.tacos.ingredient.Meat
import com.squareup.tacos.internal.iterator
import com.squareup.tacos.internal.minusAssign
fun makeTacoHealthy(taco: Taco) {
for (ingredient in taco) {
if (ingredient is Meat) taco -= ingredient
}
return taco
}
用%N占位名称
一般来说,我们生成的代码需要相互引用或者自我引用。KotlinPoet
中使用%N
来引用在KotlinPoet
中声明的另一个实体。下面例子中一个方法引用了另一个方法:
fun byteToHex(b: Int): String {
val result = CharArray(2)
result[0] = hexDigit((b ushr 4) and 0xf)
result[1] = hexDigit(b and 0xf)
return String(result)
}
fun hexDigit(i: Int): Char {
return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()
}
在生成上面的代码时,我们需要使用%N
将hexDigit()
方法作为参数传递给byteToHex()
方法:
val hexDigit = FunSpec.builder("hexDigit")
.addParameter("i", Int::class)
.returns(Char::class)
.addStatement("return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()")
.build()
val byteToHex = FunSpec.builder("byteToHex")
.addParameter("b", Int::class)
.returns(String::class)
.addStatement("val result = CharArray(2)")
.addStatement("result[0] = %N((b ushr 4) and 0xf)", hexDigit)
.addStatement("result[1] = %N(b and 0xf)", hexDigit)
.addStatement("return String(result)")
.build()
%N
另一个功能是会使用双勾号``
自动转义包含非法标识符字符的名称。比如下面的例子使用了Kotlin中的关键字package作为方法名:
val taco = ClassName("com.squareup.tacos", "Taco")
val packager = ClassName("com.squareup.tacos", "TacoPackager")
val file = FileSpec.builder("com.example", "Test")
.addFunction(
FunSpec.builder("packageTacos")
.addParameter("tacos", LIST.parameterizedBy(taco))
.addParameter("packager", packager)
.addStatement("packager.%N(tacos)", packager.member("package"))
.build()
)
.build()
%N
会转义名称,确保输出能通过编译:
package com.example
import com.squareup.tacos.Taco
import com.squareup.tacos.TacoPackager
import kotlin.collections.List
fun packageTacos(tacos: List<Taco>, packager: TacoPackager) {
packager.`package`(tacos)
}
用%L占位字面量(Literals)
一般来说KotlinPoet
中的字符串模板效果都很好。但考虑到我们有时候可能需要在生成的代码中带一些字面量的内容, KotlinPoet
提供了一种类似 String.format()
的语法。用%L
作为字面量的占位符,工作起来就像String.format()
中的%s
:
private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {
return FunSpec.builder(name)
.returns(Int::class)
.addStatement("var result = 0")
.beginControlFlow("for (i in %L until %L)", from, to)
.addStatement("result = result %L i", op)
.endControlFlow()
.addStatement("return result")
.build()
}
%L
会不经过任何转义直接将字面量提供给输出文件。%L
的参数可以是字符串、原始类型或者之前提到过的一些 KotlinPoet
类型。
代码块中占位符传递参数
代码块有三种方式给占位符传递参数,每个代码块只能使用一种。
相对位置
按顺序给每个占位符传递参数值。下面每个例子,都会生成代码“I ate 3 tacos”:
CodeBlock.builder().add("I ate %L %L", 3, "tacos")
指定位置
在占位符前放一个从1开始的整数索引,用指定要使用第几个参数。
CodeBlock.builder().add("I ate %2L %1L", "tacos", 3)
命名参数
使用语法%argumentName:X
,其中X
是对应需要使用的占位符。CodeBlock.addNamed()
中需要传入<名称,值>这样的包含所有使用到的参数的映射。参数名称只能使用a-z
,A-Z
,0-9
,和_
并且以小写字母开头。
val map = LinkedHashMap<String, Any>()
map += "food" to "tacos"
map += "count" to 3
CodeBlock.builder().addNamed("I ate %count:L %food:L", map)
别名(Type Aliases)
KotlinPoet
使用TypeAliasSpec.builder
创建别名,支持三种别名类型,简单类型、泛型类型和lambda类型:
val k = TypeVariableName("K")
val t = TypeVariableName("T")
val fileTable = Map::class.asClassName()
.parameterizedBy(k, Set::class.parameterizedBy(File::class))
val predicate = LambdaTypeName.get(
parameters = arrayOf(t),
returnType = Boolean::class.asClassName()
)
val helloWorld = FileSpec.builder("com.example", "HelloWorld")
.addTypeAlias(TypeAliasSpec.builder("Word", String::class).build())
.addTypeAlias(
TypeAliasSpec.builder("FileTable", fileTable)
.addTypeVariable(k)
.build()
)
.addTypeAlias(
TypeAliasSpec.builder("Predicate", predicate)
.addTypeVariable(t)
.build()
)
.build()
会生成:
package com.example
import java.io.File
import kotlin.Boolean
import kotlin.String
import kotlin.collections.Map
import kotlin.collections.Set
typealias Word = String
typealias FileTable<K> = Map<K, Set<File>>
typealias Predicate<T> = (T) -> Boolean
可调用的引用(Callable References)
KotlinPoet
中也支持对构造函数、函数和属性的可调用引用:
- 用
ClassName.constructorReference()
调用构造函数
- 用
MemberName.reference()
调用函数和属性
val helloClass = ClassName("com.example.hello", "Hello")
val worldFunction: MemberName = helloClass.member("world")
val byeProperty: MemberName = helloClass.nestedClass("World").member("bye")
val factoriesFun = FunSpec.builder("factories")
.addStatement("val hello = %L", helloClass.constructorReference())
.addStatement("val world = %L", worldFunction.reference())
.addStatement("val bye = %L", byeProperty.reference())
.build()
FileSpec.builder("com.example", "HelloWorld")
.addFunction(factoriesFun)
.build()
会产生:
package com.example
import com.example.hello.Hello
fun factories() {
val hello = ::Hello
val world = Hello::world
val bye = Hello.World::bye
}
如果顶级类或成员之间有名称上的冲突,可能需要别名导入(见%M一节)。
kotlin-reflect包
为了能够生成K``Type
和它的实现类,以及获得kotlin内置反射API没办法获取到的信息,KotlinPoet依赖了kotlin-reflect
包。 kotlin-reflect
可以读取类信息以获得额外的信息。比如说KotlinPoet 能够从一个带泛型的KType
实现类中读取类型参数(一般用于泛型)和它们的型变(Variance,也就是in,out和*)。
kotlin-reflect
是一个相对比较大的依赖,所以有时候我们为了节省空间或者简化proguard/R8混淆设置会希望能够从最终执行文件中删掉它(例如一个生成Kotlin代码的Gradle插件)。删掉它是完全可行的,而且删掉之后依然可以使用大部分KotlinPoet的API:
dependencies {
implementation("com.squareup:kotlinpoet:<version>") {
exclude(module = "kotlin-reflect")
}
}
KotlinPoet中依赖kotlin-reflect
的主要API是KType.asTypeName()
和typeNameOf<T>()
。如果在没有kotlin-reflect
依赖的情况调用的话,很显然会崩溃。
可以替换为显式传递类型参数的方式,并且注释清楚:
// Replace
// kotlin-reflect needed
val typeName = typeNameOf<List<Int?>>()
// With
// kotlin-reflect not needed
val typeName =
List::class.asClassName().parameterizedBy(Int::class.asClassName().copy(nullable = true))