可调试性
最终更新: 2024/03/21
本章介绍关于可调试性需要注意的问题.
永远要提供 toString() 方法
为了便于调试, 要为你引入的每个类添加 toString()
方法的实现, 即使是对内部类也是如此.
如果 toString()
是契约(Contract) 的一部分, 那么要提供明确的文档说明.
下面的代码是图形建模代码简化后的例子:
class Vector2D(val x: Int, val y: Int)
fun main() {
val result = (1..20).map { Vector2D(it, it) }
println(result)
}
这段代码的输出没什么用处:
[Vector2D@27bc2616, Vector2D@3941a79c, Vector2D@506e1b77,...]
Debug Tool 窗口中提供的信息也没什么用处:
为了让日志和调试信息更加易于阅读, 请添加一个简单的 toString()
实现, 如下:
override fun toString(): String =
"Vector2D(x=$x, y=$y)"
改善后的输出如下:
[Vector2D(x=1, y=1), Vector2D(x=2, y=2), Vector2D(x=3, y=3), ...
使用 数据类 看起来好像很不错, 因为它们自动带有
toString()
方法. 在本向导的 向后兼容性(Backward Compatibility) 章节中, 你会学习 为什么不应该这样做.
即使你认为这个类不会在任何地方打印输出, 也应该考虑实现 toString()
,
因为它可能会以意想不到的方式提供帮助.
例如, 在 构建器 之内, 能够看到构建器目前的状态可能会非常重要.
class Person(
val name: String?,
val age: Int?,
val children: List<Person>
) {
override fun toString(): String =
"Person(name=$name, age=$age, children=$children)"
}
class PersonBuilder {
var name: String? = null
var age: Int? = null
val children = arrayListOf<Person>()
fun child(personBuilder: PersonBuilder.() -> Unit = {}) {
children.add(person(personBuilder))
}
}
fun person(personBuilder: PersonBuilder.() -> Unit = {}): Person {
val builder = PersonBuilder()
builder.personBuilder()
return Person(builder.name, builder.age, builder.children)
}
上面的代码预期的使用方式是:
如果你在第一个 child
的右大括号之后的行设置断点 (如上图所示),
你会在 Debug Output 中看到一个无意义的字符串:
如果你添加一个简单的 toString()
实现, 如下:
override fun toString(): String =
"PersonBuilder(name=$name, age=$age, children=$children)"
调试信息会变得更加清晰:
你还能立即看到哪些域变量已被设置, 哪些还没有设置.
在
toString()
中暴露域变量时要小心, 因为很容易导致StackOverflowException
. 例如, 如果children
引用到了 parent, 可能会造成循环引用. 而且, 暴露 List 和 Map 时也要小心, 因为toString()
可能会展开一个非常深层的嵌套结构.
下一步做什么?
学习 API 的 向后兼容性(Backward Compatibility).