本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
引子
当代码的运行效果不符合预期时就得进行调试,排查下整个数据链路上到底是哪个环节出了问题。
断点调试当然是首选,由于它能够单步履行程序,并检查当时履行步骤中一切的数据值。但有些场景下,断点调试就显得笨拙。比方大量异步并发的场景,当程序不是线性履行而是跳来跳去时,就会产生你希望下一步是履行到这里,断点调试却跳到了另一个线程,这样的杂乱度,让正在履行的代码变得难以了解。除此之外,有些型号的手机,一断点调试就卡的不行,甚至 crash。
在这种情况下,打日志便是唯一的选择了。
日志输出一个简单的变量值是一件垂手可得的工作:
Log.v("test", "duration=${duration}")
但在杂乱的事务场景中,会存在各种嵌套的杂乱结构,比方 List,Map。断点调试中,能够轻松地点开这些数据结构,检查任何感兴趣的字段,甚至还能够当场核算。在输出日志时有什么好办法能够轻松地输出这些杂乱的数据结构吗?
打印列表 & Map
刚开始开发 Android 时,我是这样打印列表的:
for (int i = 0; i < list.size(); i++) {
Log.d("test", "list item="+list.get(i));
}
用一个 for 循环来打印列表一切元素。
后来我学会了用更高级的语法来简化日志输出:
for (String str:list) {
Log.v("test", "list item="+str);
}
这样的写法是无法被复用的,由于不同的事务场景,数据类型都不一样。为了调试,这样的 for 循环就会散落在各处。
这样写还有一个坏处,输出的列表信息或许被其他日志穿插。由于每一个列表内容都是一条新得日志,中间极有或许被其他 log 打断。
有没有一个函数能够打印包括恣意数据类型的列表,并将列表内容组织成更具可读性的字符串?
用 Kotlin 的扩展函数
+泛型
+高阶函数
就能优雅地做到:
fun <T> Collection<T>.print(mapper: (T) -> String) =
StringBuilder("\n[").also { sb ->
//遍历调集元素将元素转换成感兴趣的字串,并独占一行
this.forEach { e -> sb.append("\n\t${mapper(e)},") }
sb.append("\n]")
}.toString()
为调集的基类Collection
新增一个扩展函数,它是一个高阶函数,由于它的参数是另一个函数,该函数用 lambda 表明。再把调集元素笼统成泛型。通过StringBuilder
将一切调集内容拼接成一个自动换行的字符串。
写段测验代码看下效果:
data class Person(var name: String, var age: Int)
val persons = listOf(
Person("Peter", 16),
Person("Anna", 28),
Person("Anna", 23),
Person("Sonya", 39)
)
persons.print { "${it.name}_${it.age}" }.let { Log.v("test",it) }
打印成果如下:
V/test: [
Peter_16,
Anna_28,
Anna_23,
Sonya_39,
]
这样整个列表内容会作为一条log输出。
同样地,能够依样画葫芦一个打印 Map 的扩展函数:
fun <K, V> Map<K, V?>.print(mapper: (V?) -> String): String =
StringBuilder("\n{").also { sb ->
this.iterator().forEach { entry ->
sb.append("\n\t[${entry.key}] = ${mapper(entry.value)}")
}
sb.append("\n}")
}.toString()
打印杂乱数据结构
有些数据类字段比较多,调试时,想把它们统统打印出来,在 Java 中,借助于 AndroidStudio 的 toString
功用倒是能够方便地生成可读性很高的字串:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return ”Person{“ +
”name=‘“ + name + ’\” +
”, age=“ + age +
‘}’;
}
}
但是每新建一个数据类都要手动生成一个toString()
办法也挺麻烦。
使用 Kotlin 的 data class
能够省去这一步,但打印效果是一切字段都在同一行中:
data class Person(var name: String, var age: Int)
Log.v(“test”, “person=${Person("Peter", 16)}”)
//输出如下:
V/test: person=Person(name=Peter, age=16)
如果字段很多,把它们都打印在一行中可读性很差。
有没有一种办法,能够读取一个类中一切的字段信息?
这样我们就能够将他们组织成想要的形状。
请看下面这个办法:
fun Any.ofMap() =
// 过滤掉除data class以外的其他类
this::class.takeIf { it.isData }
// 遍历类的一切成员 过滤掉成员办法 只考虑成员特点
?.members?.filterIsInstance<KProperty<Any>>()
// 将成员特点名和值存储在Pair中
?.map { it.name to it.call(this) }
// 将Pair转换成map
?.toMap()
为恣意 Kotlin 中的类增加一个扩展函数
,它的功用是将data class
中一切的字段名及其对应值存在一个 map 中。其间用到的 Kotlin 语法糖如下:
-
isData
是KClass
中的一个特点,用于判别该类是不是一个data class
。KClass
是 Kotlin 中用来描述 类的类型,KClass
能够通过对象::class
语法取得。 -
members
也是KClass
中的一个特点,它以列表的形式返回了类中一切的办法和特点。 -
filterIsInstance()
是Iterable
接口的扩展函数,用于过滤出调集中指定的类型。 -
to
是一个infix
扩展函数,它的定义如下:
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
- 带有
infix
标识的函数只允许带有一个参数,并且在调用时能够省略包裹参数的括号。这种语法叫中缀表达式
,它用于简化办法调用。
写段测验代码,结合上一节的打印 map 函数看下效果:
data class Person(var name: String, var age: Int)
Person("Peter", 16).ofMap()?.print { it.toString() }.let { Log.v("test","$it") }
测验代码先将Person
实例转换成 map,然后打印 map。输出成果如下:
V/test:
{
[age] = 16
[name] = Peter
}
若data class
嵌套会产生什么?
//方位,嵌套在Person类中
data class Location(var x: Int, var y: Int)
data class Person(
var name: String,
var age: Int,
var locaton: Location? = null
)
Person("Peter", 16, Location(20, 30))
.ofMap()
?.print { it.toString() }
.let { Log.v("test", "$it") }
// 打印成果如下
{
[age] = 16
[locaton] = Location(x=20, y=30)
[name] = Peter
}
希望得到相似 Json 的打印效果,但输出成果还差一点。是由于将Person
转化成Map
时并没有将嵌套的Location
也转化成键值对。
需求将ofMap()
办法重构成递归调用:
fun Any.ofMap(): Map<String, Any?>? {
return this::class.takeIf { it.isData }
?.members?.filterIsInstance<KProperty<Any>>()
?.map { member ->
val value = member.call(this)?.let { v->
//'若成员变量是data class,则递归调用ofMap(),将其转化成键值对,不然直接返回值'
if (v::class.isData) v.ofMap()
else v
}
member.name to value
}
?.toMap()
}
为了让打印成果也有嵌套缩进效果,打印 Map 的函数也需求相应地重构:
/**
* 打印 Map,生成结构化键值对子串
* @param space 行缩进量
*/
fun <K, V> Map<K, V?>.print(space: Int = 0): String {
//'生成当时层次的行缩进,用space个空格表明,当时层次每一行内容都需求带上缩进'
val indent = StringBuilder().apply {
repeat(space) { append(" ") }
}.toString()
return StringBuilder("\n${indent}{").also { sb ->
this.iterator().forEach { entry ->
//'如果值是 Map 类型,则递归调用print()生成其结构化键值对子串,不然返回值本身'
val value = entry.value.let { v ->
(v as? Map<*, *>)?.print("${indent}${entry.key} = ".length) ?: v.toString()
}
sb.append("\n\t${indent}[${entry.key}] = $value,")
}
sb.append("\n${indent}}")
}.toString()
}
写段测验代码,看看效果:
//'坐标类,嵌套在Location类中'
data class Coordinate(var x: Int, var y: Int)
//'方位类,嵌套在Person类中'
data class Location(
var country: String,
var city: String,
var coordinate: Coordinate
)
data class Person(
var name: String,
var age: Int,
var locaton: Location? = null
)
Person("Peter", 16, Location("china", "shanghai", Coordinate(10, 20)))
.ofMap()
?.print()
.let { Log.v("test", "$it") }
//'打印效果如下'
{
[age] = 16,
[locaton] =
{
[city] = shanghai,
[coordinate] =
{
[x] = 10,
[y] = 20,
},
[country] = china,
},
[name] = Peter,
}
引荐阅览
事务代码参数透传满天飞?(一)
事务代码参数透传满天飞?(二)
全网最优雅安卓控件可见性检测
全网最优雅安卓列表项可见性检测
页面曝光难点分析及应对计划
你的代码太啰嗦了 | 这么多对象名?
你的代码太啰嗦了 | 这么多办法调用?