Kotlin实现HTML生成器时,如何调用接口声明的TagBlock接收者函数?
嘿,我一眼就看出问题出在哪了——你对Kotlin函数式接口的定义和调用逻辑搞混了!你的目标是让html { ... }里的lambda以Tag对象为接收者(这样就能直接在lambda里调用head()、body()这类扩展函数),但你的TagBlock定义完全不符合这个需求。
问题根源
你写的TagBlock定义:
fun interface TagBlock { operator fun Tag.invoke() }
这其实是在给Tag类定义一个成员扩展操作符函数,而不是定义一个能以Tag为接收者执行的函数类型。这就导致你无论怎么调用block都找不到正确的语法。
正确解决方案
我们需要把TagBlock定义为带接收者的函数类型,这样lambda就能自动以Tag为上下文(也就是你要的this指向Tag对象的效果)。最简洁的方式是用typealias:
步骤1:修正TagBlock定义
// 定义带接收者的函数类型别名,等价于"以Tag为接收者、无返回值的函数" typealias TagBlock = Tag.() -> Unit class Tag( val name: String, val text: String? = null, val children: MutableList<Tag> = mutableListOf() )
这里我给Tag加了text和children属性,方便后续存储标签内容和子元素——这是HTML生成器必备的。
步骤2:正确实现html函数
现在你可以在html函数里直接把Tag对象作为接收者调用block:
fun html(block: TagBlock): Tag { val htmlTag = Tag("html") // 两种调用方式等价:把htmlTag作为block的接收者执行 htmlTag.block() // 或者 block(htmlTag) return htmlTag }
步骤3:添加其他标签的扩展函数
为了支持你想要的head { ... }、title("...")这类调用,需要给Tag添加扩展函数:
// 处理head标签 fun Tag.head(block: TagBlock) { val headTag = Tag("head") headTag.block() this.children.add(headTag) } // 处理title标签(带文本内容) fun Tag.title(text: String) { this.children.add(Tag("title", text = text)) } // 同理实现body、div等标签 fun Tag.body(block: TagBlock) { val bodyTag = Tag("body") bodyTag.block() this.children.add(bodyTag) } fun Tag.div(block: TagBlock) { val divTag = Tag("div") divTag.block() this.children.add(divTag) }
现在你可以完美实现目标调用方式了!
val myHtml = html { head { title("Site title") } body { div { // 这里可以继续添加子元素,比如p标签等 } } }
如果你一定要用函数式接口(而不是typealias)
如果因为某些原因必须用函数式接口代替typealias,可以这样定义:
fun interface TagBlock { // 抽象方法明确接收Tag作为参数,不过这样lambda需要显式用参数 fun apply(tag: Tag) } // 然后html函数里调用: fun html(block: TagBlock): Tag { val htmlTag = Tag("html") block.apply(htmlTag) return htmlTag }
但这样调用时需要显式参数,不符合你的预期写法,所以显然typealias的方式更贴合你的需求。
内容的提问来源于stack exchange,提问作者Lino
相关产品推荐
相关产品推荐

