糟糕的 Kotlin 语法糖

10,734 阅读3分钟

这几天在 review 同事的代码的时候,发现一块有意思的代码,我将其写成对应的伪代码如下:

class UserViewModel(val userUsecase: UserUsecase) {

    // 根据 userId 获取 userName
    fun getUser(userId:Int) {
        val name = userUsecase(userId).name
    }
    
}

class User(val name: String, val age: Int) {}

起初在看到这段代码的时候,觉得十分反人类,在 Kotlin 中,对象的初始化可以省略 new 操作符,也即类后面再配个 () 即可,为啥一个初始化的对象还能继续用 (),在直观的感受下,我以为是初始化了一个对象,唯一让我觉得不像是初始化的就是 userUsecase 开头并不是大写,这才打消我认为他是初始化对象的疑虑。

在我想点进去看下根据 userId 获取 User 的过程,我无论追踪代码,都无法跳转到真正的逻辑代码调用处,点击 userUsecase 会直接跳转到 UserViewModel 的构造方法,点击 name 会跳转到 User 对象,这让我很苦恼。

我不得不点击 UserUsecase 类去看下里面的代码,这对于 review 人来说简直是灾难,但为了解决问题,先妥协,再一探究竟。

进入 UserUsecase 类,伪代码如下:

class UserUsecase {
    operator fun invoke(userId: Int): User {
        // 从数据库中根据 id 获取 User 数据
        // 返回 User 数据
        return User("lisi", 30)
    }
}

看到了奇怪的 invoke 函数,并且是用了 operator 操作重载符,为了了解这种语法,我在 Kotlin 中文网查了下该语法的使用,在调用操作符章节中有所说明: image.png 对象() 等价于 对象.invoke()()内为函数的参数,也即我们上面的那段代码,可以翻译一下:

class UserViewModel(val userUsecase: UserUsecase) {
    fun getUser() {
        val name = userUsecase(1001).name
        // 等价于
        val name2 = userUsecase.invoke(1001).name
    }
}

也可以用 Kotlin Decompile 看下结果:

image.png

需要说明的是,对象() 这种写法是有条件的:

  • 必须用 operator 修饰方法
  • 方法名称必须是 invoke
  • invoke 参数可以多个,不做限制

由于 invoke 函数参数不加限制,这又带来了一个问题,如果重载了多个 invoke 函数,就更不知道业务方在调用的时候是做了什么事情,依然不得不进入代码才能知道逻辑。

上面的示例给的已足够简单,但实际在我们的业务中,比这还复杂,invoke 函数被封装到了父类,当我点进去的时候根本找不到 invoke 函数,只能往上查看父类有没有,在找到 invoke 函数时才发现,他最终调用了个抽象方法,该抽象方法由子类实现,我又不得不返回到子类查看这个方法,最终才敲定这个方法做了什么逻辑。

总结:

虽然 operator invoke 可以省略调用方写函数名这个过程,但需要注意的是,代码无论是类名还是方法名还是变量名,一定要做到见名识意,显然,他已经破坏了这个规则,让 review 人很抓狂。

我也很理解大家对 Jetpack 的热爱,这种写法在官方也有出现,可以参考 Domain Layer 这章。但我想说的是,省略方法名这个过程真的有必要吗?写代码到底是为了炫技还是为了让别人能看懂自己的代码呢?