1. 概述

驼峰命名(camel case)和下划线命名(snake case)是两种广泛使用的命名规范。

本文将介绍在 Kotlin 中如何实现这两种命名方式之间的相互转换。这类需求在实际开发中非常常见,比如处理数据库字段映射、API 接口参数转换时就容易踩坑——后端用 camelCase,数据库或前端却习惯 snake_case,不做好转换很容易出问题。

2. 驼峰命名与下划线命名简介

  • 驼峰命名:单词首字母大写并拼接在一起,例如 FlywayMigrator。Java 和 Kotlin 默认采用这种命名风格。
  • 下划线命名:单词之间用下划线 _ 分隔,例如 flyway_migrator。Python 等语言常用这种方式命名变量或函数。

我们接下来的目标就是在这两种格式之间安全、准确地转换。

3. 驼峰命名转下划线命名

目标是将类似 myNiceCamelCaseStringMyNiceCamelCaseString 转换为 my_nice_camel_case_string

先定义测试数据:

val camelCaseInput1 = "myNiceCamelCaseString"
val camelCaseInput2 = "MyNiceCamelCaseString"
val expectedInSnakeCase = "my_nice_camel_case_string"

✅ 注意:输入可能以大写或小写字母开头,转换逻辑需兼容这两种情况。

3.1 使用正则表达式配合 replace() 函数

核心思路是:把每个“前面有字符的大写字母”替换为“下划线 + 小写该字母”。

使用 Kotlin 的扩展函数实现如下:

fun String.camelToSnakeCase(): String {
    val pattern = "(?<=.)[A-Z]".toRegex()
    return this.replace(pattern, "_$0").lowercase()
}

🔍 关键点解析:

  • (<=?.)[A-Z] 是一个正向后行断言(positive look-behind),表示匹配任意一个前面存在字符的大写字母
  • 这样做可以避免在字符串开头的大写字母前添加下划线(如 MyNice... 不会变成 _my_nice...)。
  • $0 表示整个匹配到的内容(即那个大写字母),替换后变成 _X
  • 最后统一转为小写即可得到标准 snake_case。

✅ 单元测试验证:

assertEquals(expectedInSnakeCase, camelCaseInput1.camelToSnakeCase())
assertEquals(expectedInSnakeCase, camelCaseInput2.camelToSnakeCase())

通过测试,说明该方法能正确处理大小写开头的情况。

3.2 使用 fold() 函数(无正则方案)

如果你不想依赖正则表达式(比如出于性能或可读性考虑),可以用 fold() 逐字符处理:

fun String.camelToSnakeCaseNoRegex(): String {
    return this.fold(StringBuilder()) { acc, c ->
        acc.let {
            val lowerC = c.lowercase()
            acc.append(if (acc.isNotEmpty() && c.isUpperCase()) "_$lowerC" else lowerC)
        }
    }.toString()
}

✅ 核心逻辑:

  • 使用 StringBuilder 提高性能(避免频繁创建字符串对象)。
  • 只有当当前字符是大写 前面已有字符时,才插入下划线。
  • 否则直接追加小写形式。

⚠️ 注意:isUpperCase() 判断的是单个字符是否为大写,适用于 ASCII 字符,对 Unicode 多字节字符需额外处理(本文暂不展开)。

测试验证:

assertEquals(expectedInSnakeCase, camelCaseInput1.camelToSnakeCaseNoRegex())
assertEquals(expectedInSnakeCase, camelCaseInput2.camelToSnakeCaseNoRegex())

两种方式结果一致,可根据团队风格选择使用。

4. 下划线命名转驼峰命名

现在反过来,将 one_good_snake_case_string 转为 oneGoodSnakeCaseString

准备测试数据:

val snakeCaseInput = "one_good_snake_case_string"
val expectedInCamelCase = "oneGoodSnakeCaseString"

目标是找到所有 _x 形式的子串,并将其替换为 X(去掉下划线并将字母大写)。

4.1 正则替换基础版

fun String.snakeToCamelCase(): String {
    val pattern = "_[a-z]".toRegex()
    return replace(pattern) { it.value.last().uppercase() }
}

📌 解析:

  • 匹配 _a_z 的模式。
  • it.value 是匹配的完整字符串(如 _g),取 .last() 得到 'g',再 .uppercase() 变成 'G'

4.2 使用分组优化(推荐)

更清晰的方式是使用捕获组:

fun String.snakeToCamelCase2(): String {
    val pattern = "_([a-z])".toRegex()
    return replace(pattern) { it.groupValues[1].uppercase() }
}

📌 说明:

  • ([a-z]) 将小写字母捕获为第一组。
  • it.groupValues[1] 获取第一个捕获组内容(即字母本身)。
  • 更具可读性,也便于后续扩展(如支持数字等)。

✅ 测试验证:

assertEquals(expectedInCamelCase, snakeCaseInput.snakeToCamelCase())
assertEquals(expectedInCamelCase, snakeCaseInput.snakeToCamelCase2())

两者均能正确转换。

❌ 常见坑点提醒:

  • 如果输入包含连续下划线(如 a__b),上述方法会生成非法驼峰名(如 aB),但丢失中间信息。生产环境建议先规范化输入(如去除多余 _)。
  • 若需支持首字母大写的 PascalCase(如 OneGoodSnakeCaseString),可在最后手动首字母大写。

5. 总结

本文通过实例演示了在 Kotlin 中实现 camelCase ↔ snake_case 的互转方法:

方法 优点 缺点
正则 + replace() 简洁高效,适合大多数场景 ✅ 对复杂边界情况需谨慎设计 pattern
fold() 手动拼接 不依赖正则,逻辑透明 ❌ 代码稍长,性能略低

📌 实际项目中,建议封装成工具类或扩展函数,提升复用性和一致性。

示例代码已托管至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-strings-3


原始标题:Conversion Between Camel Case and Snake Case in Kotlin