首页 > 其他分享 >每次调试打印日志都很头痛

每次调试打印日志都很头痛

时间:2023-06-22 10:31:55浏览次数:27  
标签:name 打印 Person var 日志 data class 调试 String


引子

当代码的运行效果不符合预期时就得进行调试,排查下整个数据链路上到底是哪个环节出了问题。

断点调试当然是首选,因为它可以单步执行程序,并查看当前执行步骤中所有的数据值。但有些场景下,断点调试就显得笨拙。比如大量异步并发的场景,当程序不是线性执行而是跳来跳去时,就会发生你期望下一步是执行到这里,断点调试却跳到了另一个线程,这样的复杂度,让正在执行的代码变得难以理解。除此之外,有些型号的手机,一断点调试就卡的不行,甚至 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 语法糖如下:

  • isDataKClass中的一个属性,用于判断该类是不是一个data classKClass是 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,
    }


作者:唐子玄

标签:name,打印,Person,var,日志,data,class,调试,String
From: https://blog.51cto.com/u_16163453/6534332

相关文章

  • ethercat调试
    1.日鼎伺服无法上电状态字始终是0x2306041状态字(官方手册P50)bit15bit14bit13bit12bit11bit10bit9bit8bit7bit6bit5bit4bit3bit2bit1bit0厂家自定义特殊的运行模式内部限制行为目标到达远程参数厂家自定义报警开机去使能急停上电使能故障运行使能开机准备开机......
  • Window 2008 R2 软件限制策略的默认调整,导致记录事件日志的权限不足
    我电脑升级成Window2008R2后,一个企业服务的项目出现如下错误:未找到源,但未能搜索某些或全部事件日志。不可访问的日志:Security。在这个企业服务中,当有错误发生时候,会把错误记录到Windows的事件日志中,这部分的代码如下:usingSystem;usingSystem.Collections.Generic;using......
  • 使用XDebug进行PHP调试
    步骤1、获取Xdebug,下载地址,根据你的操作系统情况,选择合适的下载:http://xdebug.org/download.php 假设下载后的文件为:php_xdebug.dll 2、加载PHP的这个插件以WAMPSERVER为例,我是把它装在D:\wamp\目录下,我就需要把php_xdebug.dll文件拷贝到D:\wamp\bin\php\php5.3.0\ext......
  • linux-centos-打印串口工具
    问题描述操作系统由Windows改用Centos后,没有了串口工具的支持,只能够通过终端打印串口数据。问题解决方法一使用cat配合stty链接【https://blog.51cto.com/zoomdy/5871650】1.stty-F/dev/ttyUSB0ispeed115200ospeed115200cs82.cat/dev/ttyUSB0问题解决方法二终......
  • Android强大的原生调试工具adb的常用命令
    ADB简介ADB(AndroidDebugBridge)是用于与Android设备进行通信和调试的命令行工具。以下是一些常用的ADB调试命令:常用命令列出链接的设备adbdevices:列出连接到计算机的Android设备列表。可以看到这里我连接了两个设备。进入设备的shell环境adbshell:进入设备的命令行shell......
  • 基于消息队列的实时日志处理与监控
    目录1.引言2.技术原理及概念3.实现步骤与流程4.示例与应用"基于消息队列的实时日志处理与监控"随着软件开发和监控的深入发展,日志处理和监控已经成为软件开发中不可或缺的一部分。实时日志处理和监控技术在保障系统稳定性和可靠性方面发挥着越来越重要的作用。在本文中,我们将介......
  • Django 日志配置
    Django项目日志配置记录业务运行过程中的一些关键信息,方便查看程序运行情况以及排查报错等详细日志配置settings.py配置文件中新增日志配置#设置时区,日志输出时间为utc-8时区#TIME_ZONE='UTC'TIME_ZONE='Asia/Shanghai'#日志配置LOGGING={'versio......
  • 如何实现带有颜色文本的日志框_使用HTMLEditor模拟
    如何实现带有颜色文本的日志框_使用HTMLEditor模拟HTMLEditor是一个强大的html编辑器,可以方便的编辑各种html元素并得到html文本。比之TextArea要强大很多,因为TextArea中所有的文本只能有一种样式。如果想要实现一个日志框,其中普通信息、警告信息、错误信息使用不同......
  • vue前端预览pdf并加水印、ofd文件,控制打印、下载、另存,vue-pdf的使用方法以及在开发中
    根据公司的实际项目需求,要求实现对pdf和ofd文件的预览,并且需要限制用户是否可以下载、打印、另存pdf、ofd文件,如果该用户可以打印、下载需要控制每个用户的下载次数以及可打印的次数。正常的预览pdf很简单,直接调用浏览器的预览就可以而且功能也比较全,但是一涉及到禁止用户打印、......
  • mysql的二进制日志和中继日志文件的分析、恢复、清理
    1.mysql的二进制日志目录1.mysql的二进制日志1.1.概述1.2.MySQL中二进制日志(binlog)3种不同的格式(Mixed,Statement,Row)1.2.1.Row1.2.2.Statement1.2.3.Mixed1.3.binglog格式设置1.4.二进制日志文件的清理1.4.1.自动清理binglog1.4.1.修改过期时间1.4.2.手动清除......