Kotlin 元编程之 KotlinPoet
创始人
2024-05-13 14:43:15
0

在 KSP 中默认生成代码的方式是通过CodeGenerator 创建文件流后以字符串拼接的方式来生成代码,对于简单的demo还好,但是对于实际生产项目中要生成的代码可能会十分复杂,如果还是自己手动去拼接,可能非常的繁琐,累死人不说,还非常容易出错,比如说少拼接了一个标点符号,可能需要排查半天。实际生产项目中使用的最多的就是由 JakeWharton 大神所写的著名的开源库 JavaPoet(很有诗意的名字,翻译过来叫Java诗人)使用该库可通过方便的函数进行拼接,减少出错。

KotlinPoet 是对应 JavaPoet 的 Kotlin 版本,同样是由square开发的,它可以用来很方便的生成 Kotlin 代码。

本文介绍 KotlinPoet 的使用,包括但不限于其官网文档中的内容,你也可以直接参考其官方文档:https://square.github.io/kotlinpoet/

配置

在ksp模块的build.gradle中添加KotlinPoet的依赖:

dependencies {implementation 'com.squareup:kotlinpoet:1.12.0' 
}

对应版本可以在Github上的KotlinPoet官网上查找。

简单使用

例如:

val greeterClass = ClassName("com.example.generated", "Greeter")
val fileSpec = FileSpec.builder("com.example.generated", "HelloWorld").addType(TypeSpec.classBuilder(greeterClass).primaryConstructor(FunSpec.constructorBuilder().addParameter("name", String::class).build()).addProperty(PropertySpec.builder("name", String::class).initializer("name").build()).addFunction(FunSpec.builder("greet").addStatement("println(%P)", "Hello, \$name").build()).build()).addFunction(FunSpec.builder("main").addParameter("args", String::class, KModifier.VARARG).addStatement("%T(args[0]).greet()", greeterClass).build()).build()fileSpec.writeTo(System.out)

这会生成一个包含如下代码的HelloWorld.kt文件:

package com.example.generatedimport kotlin.String
import kotlin.Unitpublic class Greeter(public val name: String,
) {public fun greet(): Unit {println("""Hello, $name""")}
}public fun main(vararg args: String): Unit {Greeter(args[0]).greet()
}

是不是很简单,跟直接拼接的方式相比,可读性很好,而且更加安全。

KotlinPoet 根据不同的使用用途提供了不同的开箱即用的类:

生成目标使用对象
Kotlin 文件FileSpec,可以调用其addTypeaddFunctionaddImportaddCodeaddProperty等来生成文件内容
类、接口和对象TypeSpec,可以调用其addModifiersaddFunctionsaddProperty等来生成主体内容
函数和构造函数FunSpec,可以调用其 addModifiersaddParametersaddStatementaddCode等来生成函数内容
参数ParameterSpec
属性PropertySpec
注解AnnotationSpec
类型别名TypeAliasSpec

addCode

但是方法和构造函数的主体在 KotlinPoet 中没有建模,没有表达式类、语句类或语法树节点。KotlinPoet 可通过调用 addCode 方法传入一个字符串模板作为代码块生成方式,可以利用 Kotlin 的多行字符串使它看起来更漂亮:

val main = FunSpec.builder("main").addCode("""|var total = 0|for (i in 0 until 10) {|    total += i|}|""".trimMargin()).build()

这样会生成如下代码:

fun main() {var total = 0for (i in 0 until 10) {total += i}
}

ControlFlow

通过 addStatement 配合 beginControlFlowendControlFlow可以进行更加灵活的流程控制代码生成:

private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {return FunSpec.builder(name).returns(Int::class).addStatement("var result = 1").beginControlFlow("for (i in $from until $to)").addStatement("result = result $op i").endControlFlow().addStatement("return result").build()
}

例如当调用 computeRange("computeRange", 1, 100, "*")时,会生成以下代码:

public fun computeRange(): Int {var result = 1for (i in 1 until 100) {result = result * i}return result
}

%S 代表字符串

当使用字符串模板的方式生成代码时,使用%S代表一个String,它会完成包装引号和转义,例如:

fun main(args: Array) {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()
}

这会生成以下代码:

class HelloWorld {fun slimShady(): String = "slimShady"fun eminem(): String = "eminem"fun marshallMathers(): String = "marshallMathers"
}

使用%S会自动加上双引号。

%P 用于字符串模板

%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"

如果调用printTotal()函数,就会输出Your total is $50,可以看到美元符号被自动转义了,这很好,但是如果需要在拼接字符串模板时, $用于引用变量,而不转义美元符号,请使用 %P

private fun stringTemplate(): FunSpec{val stringWithADollar = "Your total is " + "\${amount}"return FunSpec.builder("printTotal").addParameter("amount", String::class).returns(String::class).addStatement("return %P", stringWithADollar).build()
}

这会产生:

public fun printTotal(amount: String): String = """Your total is ${amount}"""

这样就是动态输出amount变量的值了。

CodeBlock

还可以将CodeBlocks 用作 %P 的参数,这在需要在字符串模板中引用可导入类型或成员时非常方便:

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.exampleimport kotlin.IntArray
import kotlin.collections.contentToStringfun print(digits: IntArray) {println("""These are the digits: ${digits.contentToString()}""")
}

%T 引用类型自动导入

KotlinPoet 对类型有丰富的内置支持,包括 import 语句的自动生成。仅用于%T引用类型:

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.helloworldimport java.util.Dateclass HelloWorld {fun today(): Date = Date()
}

ClassName 用于构建Class类型

上面我们通过Date::class引用了一个我们在编写生成代码时恰好可用的类。但是我们也可以引用一个在编写生成代码时还不存在的类:

val hoverboard = ClassName("com.mattel", "Hoverboard")val tomorrow = FunSpec.builder("tomorrow").returns(hoverboard).addStatement("return %T()", hoverboard).build()

这会生成以下代码,那个还不存在的类也被导入了:

package com.example.helloworldimport com.mattel.Hoverboardclass HelloWorld {fun tomorrow(): Hoverboard = Hoverboard()
}

由于类型非常重要,在使用 KotlinPoet 时会经常需要到 ClassName。它可以识别任何声明的类。声明类型只是 Kotlin 丰富类型系统的开始:我们还有数组、参数化类型、通配符类型、lambda 类型和类型变量。KotlinPoet 具有用于构建以下各项的类:

import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedByval 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.helloworldimport com.mattel.Hoverboard
import com.misc.Thing
import kotlin.Array
import kotlin.collections.ArrayList
import kotlin.collections.Listclass HelloWorld {fun beyond(): List {val result = ArrayList()result += Hoverboard()result += Hoverboard()result += Hoverboard()return result}fun printThings(things: Array) {println(things)}
}

可空类型

KotlinPoet 支持可空类型。要将一个 TypeName 转换成可为 null 的对应项,请使用copy(nullable = true)方法:

val name = PropertySpec.builder("name", String::class.asTypeName().copy(nullable = true)).mutable().addModifiers(KModifier.PRIVATE).initializer("null").build()val address = PropertySpec.builder("address", String::class).addModifiers(KModifier.PRIVATE).initializer("%S", "china").build()TypeSpec.classBuilder("HelloWorld").addProperty(name).addProperty(address)//.addProperty("address", String::class, KModifier.PRIVATE).build()

这会生成以下代码:

class HelloWorld {private var name: String? = nullprivate val address: String = "china"
}

%M 引用 MemberName 成员

ClassName类似,KotlinPoet 有一个特殊的成员占位符(函数和属性),当代码需要访问顶级成员和在对象内部声明的成员时,它会派上用场。用%M引用成员时,需要传递一个MemberName实例作为占位符的参数,KotlinPoet 将自动处理导入:

package com.squareup.tacosclass Taco {}
fun createTaco() = Taco() 
val Taco.isVegan: Booleanget() = false
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.exampleimport com.squareup.tacos.createTaco
import com.squareup.tacos.isVeganfun main() {val taco = createTaco()println(taco.isVegan)
}

如您所见,%M可以用于扩展函数和属性引用。只需要确保没有名称冲突的情况下导入成员,否则导入失败将导致代码生成器无法通过编译。不过,有一种方法可以解决这种名称冲突情况:使用FileSpec.addAliasedImport() 用于为冲突创建别名 MemberName

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 将为以下内容生成别名导入:

package com.squareup.exampleimport com.squareup.cakes.createCake
import com.squareup.tacos.createTaco
import com.squareup.cakes.isVegan as isCakeVegan
import com.squareup.tacos.isVegan as isTacoVeganfun main() {val taco = createTaco()val cake = createCake()println(taco.isTacoVegan)println(cake.isCakeVegan)
}

请注意 %M 只能用于引用MemberName不能用于引用ClassName,如果你在 %M 占位处传了一个ClassName会build时报类型转换异常。

MemberName和运算符

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)

这会生成以下代码:

package com.exampleimport com.squareup.tacos.Taco
import com.squareup.tacos.ingredient.Meat
import com.squareup.tacos.internal.iterator
import com.squareup.tacos.internal.minusAssignfun makeTacoHealthy(taco: Taco) {for (ingredient in taco) {if (ingredient is Meat) taco -= ingredient}return taco
}

KOperator中目前定义了如下可用的运算符:

public enum class KOperator(internal val operator: String,internal val functionName: String
) {UNARY_PLUS("+", "unaryPlus"),PLUS("+", "plus"),UNARY_MINUS("-", "unaryMinus"),MINUS("-", "minus"),TIMES("*", "times"),DIV("/", "div"),REM("%", "rem"),PLUS_ASSIGN("+=", "plusAssign"),MINUS_ASSIGN("-=", "minusAssign"),TIMES_ASSIGN("*=", "timesAssign"),DIV_ASSIGN("/=", "divAssign"),REM_ASSIGN("%=", "remAssign"),INC("++", "inc"),DEC("--", "dec"),EQUALS("==", "equals"),NOT_EQUALS("!=", "equals"),NOT("!", "not"),RANGE_TO("..", "rangeTo"),CONTAINS("in", "contains"),NOT_CONTAINS("!in", "contains"),GT(">", "compareTo"),LT("<", "compareTo"),GE(">=", "compareTo"),LE("<=", "compareTo"),ITERATOR("in", "iterator"),
}

%N 引用已创建的声明

生成的代码通常是自引用的。可用%N通过名称引用另一个已生成的声明。例如下面代码中byteToHex方法调用了hexDigit方法:

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()
}

在生成上面的代码时,我们将创建好的hexDigit()方法的FunSpec作为参数,传递给构建 byteToHex() 方法的 %N 占位符:

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 的关键字作为名字创建了一个MemberName

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.exampleimport com.squareup.tacos.Taco
import com.squareup.tacos.TacoPackager
import kotlin.collections.Listfun packageTacos(tacos: List, packager: TacoPackager) {packager.`package`(tacos)
}

%L 引用字面量

虽然 Kotlin 的字符串模板通常在您想要将字面量包含到生成的代码中时效果很好,但 KotlinPoet 提供了额外的语法,灵感来自但不兼容 String.format()。它接受在输出%L中发出一个字面量值。这就像是Formatter:%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()
}

字面量将被直接发送到输出代码,没有转义。字面量支持的参数类型可以是String基本类型和下面将要提到的几个KotlinPoet 类型

代码块的字符串格式化

代码块可以通过几种方式指定其占位符的值。代码块上的每个操作只能使用一种样式。

相关参数

在格式字符串中将每个占位符的参数值传递给CodeBlock.add()方法

CodeBlock.builder().add("I ate %L %L", 3, "tacos")

位置参数

可在格式字符串中的占位符之前放置一个整数索引(从 1 开始)以指定要使用的参数。

CodeBlock.builder().add("I ate %2L %1L", "tacos", 3)

命名参数占位符

使用语法%argName:X,其中X是格式字符的占位符,通过使用CodeBlock.addNamed()方法为X传递一个map参数,map必须包含格式化字符串中所有argName参数名的key

val map = LinkedHashMap()
map += "food" to "tacos"
map += "count" to 3
CodeBlock.builder().addNamed("I ate %count:L %food:L", map)

其中,参数名argName的命名规则:字符a-z, a-z, 0-9_,并且必须以小写字符开头

创建一个简单的类很容易,使用TypeSpec.classBuilder即可:

TypeSpec.classBuilder(greeterClass).addModifiers(KModifier.OPEN).primaryConstructor(FunSpec.constructorBuilder().addParameter("name", String::class).build()).addProperty(PropertySpec.builder("name", String::class).addModifiers(KModifier.PRIVATE).initializer("name").build()).addFunction(FunSpec.builder("greet").addStatement("println(%P)", "Hello, \$name").build()) .build()

这会生成:

public open class Greeter(private val name: String) {public fun greet(): Unit {println("""Hello, $name""")} 
}

使用TypeSpec.classBuilder之后可以调用addPropertyaddFunction为其添加属性和函数,并且还可以继续在上面调用addType来添加子类和接口。

注意类的构造函数参数和属性如果想达到上面那样跟在类名后面合并的效果,就必须同时设置primaryConstructoraddProperty,否则在kotlin中二者是分开的。

KModifier 修饰符

前面代码中在类和属性上调用addModifiers添加了修饰符,对于可见性,如果不设置,默认是public, 但addModifiers可以添加很多其他Kotlin支持的修饰符。
以下是 KModifier 枚举类中可用的修饰符:

修饰符作用目标
PUBLIC PROTECTED PRIVATE INTERNAL FINAL OPEN ABSTRACT
EXTERNAL EXPECT ACTUAL
Class 类Property 属性Function 函数
SEALED INNER ENUM DATA VALUE ANNOTATION COMPANIONClass 类
CONST LATEINITProperty 属性
OVERRIDEProperty 属性Function 函数
SUSPEND INLINE TAILREC INFIX OPERATORFunction 函数
VARARG NOINLINE CROSSINLINEParameter 参数
REIFIED内联函数上的泛型参数
IN OUT类上的泛型参数:逆变、协变
FUNinterface 接口

函数

前面通过FunSpec.builder创建的所有函数都有函数体代码,如果要生成没有函数体的抽象函数,请使用 FunSpec.builderaddModifiers() 方法设置KModifier.ABSTRACT 这只有在生成抽象类或接口的函数时才是合法的。

val flux = FunSpec.builder("flux").addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(KModifier.ABSTRACT).addFunction(flux).build()

这会生成以下代码:

abstract class HelloWorld {protected abstract fun flux()
}

其他修饰符在允许的情况下起作用。

Kotlin中的函数还拥有参数、可变参数、KDoc、注解、类型变量、返回类型和扩展函数的接收者类型等。而所有这些在FunSpec.Builder中都有相关的配置项可使用。

扩展函数

可以通过FunSpec.builder指定一个receiver来生成扩展函数

val square = FunSpec.builder("square").receiver(Int::class).returns(Int::class).addStatement("var s = this * this").addStatement("return s").build()

这会输出:

fun Int.square(): Int {val s = this * thisreturn s
}

单行表达式函数

KotlinPoet 可以正确的识别单行表达式函数,它将每个函数的主体以 return 开头作为单行表达式函数:

val abs = FunSpec.builder("abs").addParameter("x", Int::class).returns(Int::class).addStatement("return if (x < 0) -x else x").build()

这会输出:

fun abs(x: Int): Int = if (x < 0) -x else x

函数默认参数

考虑下面的例子,函数参数b的默认值为 0 以避免重载此函数。

fun add(a: Int, b: Int = 0) {print("a + b = ${a + b}")
}

要声明一个函数参数的默认值,可以使用ParameterSpec.builderdefaultValue()方法来设置,并将ParameterSpec构建生成添加到FunSpec.builder.addParameter()方法中即可。

FunSpec.builder("add").addParameter("a", Int::class).addParameter(ParameterSpec.builder("b", Int::class).defaultValue("%L", 0).build()).addStatement("print(\"a + b = ${a + b}\")").build()

默认情况下空格换行!

为了提供有意义的格式,KotlinPoet 会在代码行数超过长度限制的情况下用新的行符号替换代码块中的空格。我们以这个函数为例:

val funSpec = FunSpec.builder("foo").addStatement("return (100..10000).map { number -> number * number }.map { number -> number.toString() }.also { string -> println(string) }").build()

它最终可能会像这样打印出来:

fun foo() = (100..10000).map { number -> number * number }.map { number -> number.toString() }.also
{ string -> println(string) }

不幸的是,这段代码会编译报错:由于also后面被换行了,编译器期望 also{ 位于同一行。KotlinPoet 无法理解表达式的上下文并修复格式,但是可以使用一个技巧来声明不间断空格:在原本要使用空格的地方使用 · 符号。将上面示例修改如下:

val funSpec = FunSpec.builder("foo").addStatement("return (100..10000).map·{ number -> number * number }.map·{ number -> number.toString() }.also·{ string -> println(string) }").build()

这将产生以下结果:

fun foo() = (100..10000).map { number -> number * number }.map { number ->number.toString()
}.also { string -> println(string) }

代码现在就可以被正确的编译。虽然它看起来仍然不够完美,你可以尝试用 · 符号替换代码块中的剩余的空格以获得更好看的格式。

构造函数

FunSpec 也可以用于构造函数,通过调用其FunSpec.constructorBuilder()方法:

val flux = FunSpec.constructorBuilder().addParameter("greeting", String::class).addStatement("this.%N = %N", "greeting", "greeting").build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addProperty("greeting", String::class, KModifier.PRIVATE).addFunction(flux).build()

这会输出:

class HelloWorld {private val greeting: Stringconstructor(greeting: String) {this.greeting = greeting}
}

在大多数情况下,构造函数就像方法一样工作。发出代码时,KotlinPoet 会将构造函数放在输出文件中的方法之前

很多时候你需要为一个类生成主构造函数:

val helloWorld = TypeSpec.classBuilder("HelloWorld").primaryConstructor(flux).addProperty("greeting", String::class, KModifier.PRIVATE).build()

但是,此代码会生成以下内容:

class HelloWorld(greeting: String) {private val greeting: Stringinit {this.greeting = greeting}
}

默认情况下,KotlinPoet 不会合并主构造函数参数和属性,即使它们共享相同的名称。要达到这个效果,你必须告诉 KotlinPoet 该属性是通过构造函数的参数初始化的:

val flux = FunSpec.constructorBuilder().addParameter("greeting", String::class).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").primaryConstructor(flux).addProperty(PropertySpec.builder("greeting", String::class) // 声明一个属性.initializer("greeting") // 指定该属性的初始化来源是构造函数中的greeting参数.addModifiers(KModifier.PRIVATE).build()).build()

在Kotlin中成员属性Field和 显示声明的constructor构造函数 或者 类名后面不带val的构造函数 中的参数是两个不同的东西。

现在我们得到以下输出:

class HelloWorld(private val greeting: String)

请注意,KotlinPoet 会省略具有空主体的类的 { }

参数

要在函数和构造函数上声明参数,可以使用 FunSpec.addParameter() 或者通过ParameterSpec.builder() 来创建一个参数:

val android = ParameterSpec.builder("android", String::class).defaultValue("\"pie\"") // 默认参数值.build()val welcomeOverlords = FunSpec.builder("welcomeOverlords").addParameter(android).addParameter("robot", String::class).build()

上面的代码生成:

fun welcomeOverlords(android: String = "pie", robot: String) {
}

当参数上面有注解时(比如@Inject),扩展的Builder是很必要的。

属性

与参数一样,可以使用PropertySpec.builder构造器或使用addProperty方法来创建Field属性字段:

val android = PropertySpec.builder("android", String::class).addModifiers(KModifier.PRIVATE).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addProperty(android).addProperty("robot", String::class, KModifier.PRIVATE).build()

这会产生:

class HelloWorld {private val android: Stringprivate val robot: String
}

Field 字段具有 KDoc、注解或字段初始值设定项时,扩展形式的Builder是必需的。Field 字段的初始值设置方式与前面提到的在CodeBlock中使用的有点类似String.format()的语法一样:

val android = PropertySpec.builder("android", String::class).addModifiers(KModifier.PRIVATE).initializer("%S + %L", "Oreo v.", 8.1).build()

这会产生:

private val android: String = "Oreo v." + 8.1

默认情况下PropertySpec.Builder生成val属性,如果你需要生成 var 属性,请使用 mutable()

val android = PropertySpec.builder("android", String::class).mutable().addModifiers(KModifier.PRIVATE).initializer("%S + %L", "Oreo v.", 8.1).build()

内联属性

KotlinPoet 对内联属性建模的方式有点特殊。以下代码片段:

val android = PropertySpec.builder("android", String::class).mutable().addModifiers(KModifier.INLINE).build()

会产生错误:

java.lang.IllegalArgumentException: KotlinPoet doesn't allow setting the inline modifier on
properties. You should mark either the getter, the setter, or both inline.

实际上,标有 inline 的属性应该至少需要有一个将由编译器内联的 accessor 访问器。让我们为这个属性添加一个 getter

val android = PropertySpec.builder("android", String::class).mutable().getter(FunSpec.getterBuilder().addModifiers(KModifier.INLINE).addStatement("return %S", "foo").build()).build()

现在结果如下:

var android: kotlin.Stringinline get() = "foo"

现在,如果我们想为上面的属性添加一个非内联 setter 怎么办?我们可以在不修改之前编写的任何代码的情况下这样做:

val android = PropertySpec.builder("android", String::class).mutable().getter(FunSpec.getterBuilder().addModifiers(KModifier.INLINE).addStatement("return %S", "foo").build()).setter(FunSpec.setterBuilder().addParameter("value", String::class).build()).build()

我们得到了预期的结果:

var android: kotlin.Stringinline get() = "foo"set(`value`) {}

最后,此时如果我们为 setter添加一个 KModifier.INLINE ,KotlinPoet 可以很好地包装它并产生以下结果:

inline var android: kotlin.Stringget() = "foo"set(`value`) {}

这时如果从 gettersetter 中删除修饰符将使表达式回到前面的样子。

另一方面,如果 KotlinPoet 允许inline直接标记一个属性,程序员将不得不在访问器的状态发生变化时手动添加/删除修饰符,以获得正确且可编译的输出。我们通过使访问器成为inline修饰符的真实来源来解决这个问题。

接口

KotlinPoet 通过ypeSpec.interfaceBuilder来创建一个接口。请注意,接口方法的Modifier修饰符必须始终是ABSTRACT,在定义接口方法时需要总是显示的指定该修饰符

val helloWorld = TypeSpec.interfaceBuilder("HelloWorld").addProperty("buzz", String::class).addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).build()).build()

但是这些修饰符在生成代码的时候就被省略了。这些是默认设置,因此我们不需要包含它们,这正是kotlinc编译器为我们提供的方便!

interface HelloWorld {val buzz: Stringfun beep()
}

生成带泛型的接口类:

private fun generateInterface() : TypeSpec {val K = TypeVariableName("K")val T = TypeVariableName("T")return TypeSpec.interfaceBuilder("Hello").addTypeVariable(K).addTypeVariable(T).addProperty("buzz", String::class).addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).addParameter("a", K).returns(T).build()).build()
}

这里主要通过addTypeVariable为接口添加一个TypeVariableName即可,使用到泛型的接口方法也可以直接将TypeVariableName作为TypeName来使用。

这会生成:

public interface Hello {public val buzz: Stringpublic fun beep(a: K): T
}

Kotlin 1.4 通过fun interface语法添加了对函数接口的支持。要在 KotlinPoet 中创建它,请使用TypeSpec.funInterfaceBuilder()

val helloWorld = TypeSpec.funInterfaceBuilder("HelloWorld").addFunction(FunSpec.builder("beep").addModifiers(KModifier.ABSTRACT).build()).build()// Generates...
fun interface HelloWorld {fun beep()
}

Obejct & 伴生对象

通过 TypeSpec.objectBuilder 来创建一个 kotlin 的 Object 对象

val helloWorld = TypeSpec.objectBuilder("HelloWorld").addProperty(PropertySpec.builder("buzz", String::class).initializer("%S", "buzz").build()).addFunction(FunSpec.builder("beep").addStatement("println(%S)", "Beep!").build()).build()

这会生成:

public object HelloWorld {public val buzz: String = "buzz"public fun beep(): Unit {println("Beep!")}
}

同样,可以通过TypeSpec.companionObjectBuilder()创建一个伴生对象并随后调用addType()将它们添加到类构造器中:

val companion = TypeSpec.companionObjectBuilder().addProperty(PropertySpec.builder("buzz", String::class).initializer("%S", "buzz").build()).addFunction(FunSpec.builder("beep").addStatement("println(%S)", "Beep!").build()).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addType(companion).build()

注意,伴生对象一般是用于创建到一个类中的,随后可以通过所属的类名直接访问它,因此伴生对象一般不需要名字。但是你仍然可以通过TypeSpec.companionObjectBuilder(“xxx”)为伴随对象提供一个可选名称。

父类

要添加一个父类请使用 TypeSpec.classBuilder().superclass() 方法

TypeSpec.classBuilder(ClassName("com.example.generated", "ChildClass")).superclass(Date::class) .addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()

这会生成:

package com.example.generated
import java.util.Date 
import kotlin.Stringpublic class ChildClass : Date() {public fun foo(): String = "aaa"
}

若添加父类是一个编写时不能直接引用的类,使用ClassName来创建即可:

val className = ClassName("com.example.generated", "ChildClass")
val parentClassName = ClassName("com.fly.compose.ksp.application", "ParentClass")
TypeSpec.classBuilder(className) .superclass(parentClassName) .addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()

父类构造器

superclass() 默认会添加一个空的父类构造函数,如果父类有有参数的构造函数,请使用 addSuperclassConstructorParameter 来添加父类有参构造函数的参数:

val className = ClassName("com.example.generated", "ChildClass")
val parentClassName = ClassName("com.fly.compose.ksp.application", "ParentClass")
return TypeSpec.classBuilder(className) .superclass(parentClassName).addSuperclassConstructorParameter("%S, %L", "aaa", 123).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()

这会生成:

package com.example.generated
import com.fly.compose.ksp.application.ParentClass
import kotlin.Stringpublic class ChildClass : ParentClass("aaa", 123) {public fun foo(): String = "aaa"
}

父接口

要添加一个父接口,请使用 TypeSpec.classBuilder().addSuperinterface()

private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Child")val parentInterface = ClassName("com.fly.compose.ksp.application", "MyInterface")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface).addModifiers(KModifier.ABSTRACT).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}

这会生成:

package com.example.generated
import com.fly.compose.ksp.application.MyInterface
import kotlin.Stringpublic abstract class Child : MyInterface {public fun foo(): String = "aaa"
}

这里创建的类修饰符声明为抽象类ABSTRACT,如果不是抽象类,而是普通类,则需要为该类添加实现方法:

private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Child")val parentInterface = ClassName("com.fly.compose.ksp.application", "MyInterface")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface).addFunction(FunSpec.builder("getDescription").returns(String::class).addModifiers(KModifier.OVERRIDE).addParameter("money", Int::class).addStatement("val result = %P", "Total money is " + "\${money}.").addStatement("return result").build()).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}

注意覆写接口的方法需要指定OVERRIDE修饰符。

这会生成:

package com.example.generated
import com.fly.compose.ksp.application.MyInterface
import kotlin.Int 
import kotlin.Stringpublic class Child : MyInterface {public override fun getDescription(money: Int): String {val result = """Total money is ${money}."""return result}public fun foo(): String = "aaa"
}

带泛型的父接口

如果要添加的父接口带有泛型参数,可以在addSuperinterface时使用plusParameter

private fun addSuperInterface2() : TypeSpec {val className = ClassName("com.example.generated", "Engine")val parentInterface = ClassName("com.fly.compose.ksp.application", "Feature")val T = ClassName("com.fly.compose.ksp.application", "Type")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface.plusParameter(T)).addModifiers(KModifier.ABSTRACT).addFunction(FunSpec.builder("foo").returns(String::class).addStatement("return %S", "aaa").build()).build()
}

这会生成:

package com.example.generated
import com.fly.compose.ksp.application.Feature
import kotlin.Stringpublic abstract class Engine : Feature {public fun foo(): String = "aaa"
}

如果是多个泛型参数怎么办,你可以选择parentInterface.plusParameter(T).plusParameter(R),还有一个更好用的方法 parameterizedBy(T, R, ...), 例如:

private fun addSuperInterface() : TypeSpec {val className = ClassName("com.example.generated", "Engine")val parentInterface = ClassName("com.fly.compose.ksp.application", "Feature")val T = ClassName("com.fly.compose.ksp.application", "Type")val R = ClassName("com.fly.compose.ksp.application", "Result")return TypeSpec.classBuilder(className).addSuperinterface(parentInterface.parameterizedBy(T, R)).addFunction(FunSpec.builder("transform").addModifiers(KModifier.OVERRIDE).returns(R).addParameter("a", T).addStatement("val result = %T(a)", R).addStatement("return result").build()).build()
}

这会生成:

package com.example.generated
import com.fly.compose.ksp.application.Feature
import com.fly.compose.ksp.application.Result
import com.fly.compose.ksp.application.Typepublic class Engine : Feature {public override fun transform(a: Type): Result {val result = Result(a)return result}
}

此外parameterizedBy在需要获取一些集合类的泛型类型时非常有用,例如:

// For:  List
val stringList = ClassName("kotlin.collections", "List").parameterizedBy(String::class.asTypeName())

枚举

使用TypeSpec.enumBuilder创建枚举类型,并使用addEnumConstant()创建每个枚举值:

val helloWorld = TypeSpec.enumBuilder("Roshambo").addEnumConstant("ROCK").addEnumConstant("SCISSORS").addEnumConstant("PAPER").build()

这会生成:

enum class Roshambo {ROCK,SCISSORS,PAPER
}

支持花式枚举,其中枚举值覆盖方法或调用超类构造函数。这是一个综合示例:

val helloWorld = TypeSpec.enumBuilder("Roshambo").primaryConstructor(FunSpec.constructorBuilder().addParameter("handsign", String::class).build()).addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "fist").addFunction(FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).addStatement("return %S", "avalanche!").returns(String::class).build()).build()).addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "peace").build()).addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", "flat").build()).addProperty(PropertySpec.builder("handsign", String::class, KModifier.PRIVATE).initializer("handsign").build()).build()

这会生成:

enum class Roshambo(private val handsign: String) {ROCK("fist") {override fun toString(): String = "avalanche!"},SCISSORS("peace"),PAPER("flat");
}

匿名内部类

在枚举代码中,我们使用了TypeSpec.anonymousClassBuilder() 匿名内部类也可以用在代码块中。它们是可以通过%L引用的值:

val comparator = TypeSpec.anonymousClassBuilder().addSuperinterface(Comparator::class.parameterizedBy(String::class)).addFunction(FunSpec.builder("compare").addModifiers(KModifier.OVERRIDE).addParameter("a", String::class).addParameter("b", String::class).returns(Int::class).addStatement("return %N.length - %N.length", "a", "b").build()).build()val helloWorld = TypeSpec.classBuilder("HelloWorld").addFunction(FunSpec.builder("sortByLength").addParameter("strings", List::class.parameterizedBy(String::class)).addStatement("%N.sortedWith(%L)", "strings", comparator).build()).build()

这会生成一个带有匿名内部类的方法:

class HelloWorld {fun sortByLength(strings: List) {strings.sortedWith(object : Comparator {override fun compare(a: String, b: String): Int = a.length - b.length})}
}

定义匿名内部类的一个特别棘手的部分是超类构造函数的参数。要传递它们,请使用TypeSpec.BuilderaddSuperclassConstructorParameter()方法。

注解

生成简单的注解很容易,例如可以通过 FunSpec.builderaddAnnotation() 来为函数生成添加一个注解:

val test = FunSpec.builder("test string equality").addAnnotation(Test::class).addStatement("assertThat(%1S).isEqualTo(%1S)", "foo").build()

这会生成一个带有@Test注解的函数:

@Test
fun `test string equality`() {assertThat("foo").isEqualTo("foo")
}

可以用 AnnotationSpec.builder()addMember() 设置注解的属性:

val logRecord = FunSpec.builder("recordEvent").addModifiers(KModifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(Headers::class).addMember("accept = %S", "application/json; charset=utf-8").addMember("userAgent = %S", "Square Cash").build()).addParameter("logRecord", LogRecord::class).returns(LogReceipt::class).build()

这会生成一个带有accept and userAgent 属性的@Headers注解:

@Headers(accept = "application/json; charset=utf-8",userAgent = "Square Cash"
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt

如果你喜欢, AnnotationSpec.builderaddMember()还可以生成嵌套的注解:

val headerList = ClassName("", "HeaderList")
val header = ClassName("", "Header")
val logRecord = FunSpec.builder("recordEvent").addModifiers(KModifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(headerList).addMember("[\n⇥%L,\n%L⇤\n]",AnnotationSpec.builder(header).addMember("name = %S", "Accept").addMember("value = %S", "application/json; charset=utf-8").build(),AnnotationSpec.builder(header).addMember("name = %S", "User-Agent").addMember("value = %S", "Square Cash").build()).build()).addParameter("logRecord", logRecordName).returns(logReceipt).build()

这会生成:

@HeaderList([Header(name = "Accept", value = "application/json; charset=utf-8"),Header(name = "User-Agent", value = "Square Cash")]
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt

KotlinPoet 支持设置注解的 use-site target

val utils = FileSpec.builder("com.example", "Utils").addAnnotation(AnnotationSpec.builder(JvmName::class).useSiteTarget(UseSiteTarget.FILE).build()).addFunction(FunSpec.builder("abs").receiver(Int::class).returns(Int::class).addStatement("return if (this < 0) -this else this").build()).build()

会生成如下代码:

@file:JvmNamepackage com.exampleimport kotlin.Int
import kotlin.jvm.JvmNamefun Int.abs(): Int = if (this < 0) -this else this

AnnotationSpec.UseSiteTarget 目前有如下枚举:

  public enum class UseSiteTarget(internal val keyword: String) {FILE("file"),PROPERTY("property"),FIELD("field"),GET("get"),SET("set"),RECEIVER("receiver"),PARAM("param"),SETPARAM("setparam"),DELEGATE("delegate"),}

它拥有跟 kotlin 的AnnotationTarget 相对应的大部分枚举值:

public enum class AnnotationTarget {/** Class, interface or object, annotation class is also included */CLASS,/** Annotation class only */ANNOTATION_CLASS,/** Generic type parameter */TYPE_PARAMETER,/** Property */PROPERTY,/** Field, including property's backing field */FIELD,/** Local variable */LOCAL_VARIABLE,/** Value parameter of a function or a constructor */VALUE_PARAMETER,/** Constructor only (primary or secondary) */CONSTRUCTOR,/** Function (constructors are not included) */FUNCTION,/** Property getter only */PROPERTY_GETTER,/** Property setter only */PROPERTY_SETTER,/** Type usage */TYPE,/** Any expression */EXPRESSION,/** File */FILE,/** Type alias */@SinceKotlin("1.1")TYPEALIAS
}

类型别名

KotlinPoet 提供了用于创建类型别名的 API ,这些API主要包括TypeVariableNameparameterizedByLambdaTypeName,通过它们可以支持简单的类名、泛型和 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.exampleimport java.io.File
import kotlin.Boolean
import kotlin.String
import kotlin.collections.Map
import kotlin.collections.Settypealias Word = Stringtypealias FileTable = Map>typealias Predicate = (T) -> Boolean

通过LambdaTypeName可以很方便的创建函数类型,以生成高阶函数代码,例如:

private fun functionType() : TypeSpec {val Type = ClassName("com.fly.compose.ksp.application", "Type")val E  = TypeVariableName("E")val R  = TypeVariableName("R")val T = TypeVariableName("T")val E_Type = E.copy(bounds = listOf(Type)) // like: val blockTypeName = LambdaTypeName.get(receiver = T,returnType = R)val blockTypeName2 = LambdaTypeName.get(parameters = arrayOf(T),returnType = R)val onSuccessTypeName = LambdaTypeName.get(parameters = arrayOf(E),returnType = Boolean::class.asClassName())val onFailedTypeName = LambdaTypeName.get(parameters = arrayOf(E),returnType = Unit::class.asClassName())val className = ClassName("com.example.generated", "Foo")return TypeSpec.classBuilder(className).addTypeVariable(E_Type).addFunction(FunSpec.builder("with").addTypeVariables(listOf(T, R)).addParameter("receiver", T).addParameter(ParameterSpec.builder("block", blockTypeName).build()).returns(R).addStatement("return receiver.block()").build()).addFunction(FunSpec.builder("let").addTypeVariables(listOf(T, R)).receiver(T).addParameter(ParameterSpec.builder("block", blockTypeName2).build()).returns(R).addStatement("return block(this)").build()).addFunction(FunSpec.builder("request").addParameter(ParameterSpec.builder("onSuccess", onSuccessTypeName).build()).addParameter(ParameterSpec.builder("onFailed", onFailedTypeName).build()).build()).build()
}

这会生成:

public class Foo {public fun  with(`receiver`: T, block: T.() -> R): R = receiver.block()public fun  T.let(block: (T) -> R): R = block(this)public fun request(onSuccess: (E) -> Boolean, onFailed: (E) -> Unit): Unit {}
}

Callable 引用

可以通过以下方式创建对构造函数函数属性的 Callable 引用:

  • 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.exampleimport com.example.hello.Hellofun factories() {val hello = ::Helloval world = Hello::worldval bye = Hello.World::bye
}

具有冲突名称的顶级类和成员可能需要别名导入,就像前面使用 MemberName 一样。

kotlin-反射

要从任意的 KType 生成源代码,包括内置反射 API 无法访问的信息,KotlinPoet 依赖于kotlin-reflectkotlin-reflect 可以读取类的元数据并访问这些额外信息。例如,KotlinPoet 可以从泛型中读取类型参数及其变体KType,并生成适当的源代码。

kotlin-reflect虽然是一个相对较大的依赖库,但在某些情况下,希望将其从最终可执行文件中删除以节省一些空间和/或简化 proguard/R8 设置(例如,对于生成 Kotlin 代码的 Gradle 插件)。可以这样做并且仍然使用大部分 KotlinPoet API:

dependencies {implementation("com.squareup:kotlinpoet:") {exclude(module = "kotlin-reflect")}
}

KotlinPoet 主要需要的 kotlin-reflect 相关的API是 KType.asTypeName()typeNameOf()。如果你在没有依赖kotlin-reflect库的情况下调用其中了一个,并且类型是泛型或具有注解,将会产生崩溃。

你可以将其替换为显式传递类型参数或注解且不需要kotlin-reflect的代码。例如:

// Replace
// kotlin-reflect needed
val typeName = typeNameOf>()// With
// kotlin-reflect not needed
val typeName =List::class.asClassName().parameterizedBy(Int::class.asClassName().copy(nullable = true))

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...