Kotlin Suppress 的非常规用法

Author Avatar
Dexlind 12月 09, 2018
  • 在其它设备中阅读本文章

在 Java 中,有个叫 SuppressWarnings 的注解,用于忽略特定的编译器警告。Kotlin 中也有一个类似功能的注解,叫做 Suppress,本文主要讲讲这个注解有什么用。

常规用法

有时候会写出这样的一些代码,在Kotlin编译器的眼里可能会出现问题,但是实际上却非常正确,跑起来不会出任何问题。

然后你就得到了一个编译器警告(warning),甚至是编译错误(error)。

比较常见的,比如说 unchecked cast:

val some: List<*> = emptyList<Nothing>()
some as List<String> // Unchecked cast: List<*> to List<String>

然后就可以使用 Suppress 取消这个 warning:

val some: List<*> = emptyList<Nothing>()
@Suppress("UNCHECKED_CAST")
some as List<String>

这是很简单的破事水,没有任何讨论的价值。

然后下面是正文。

好孩子不要模仿!很危险的!

非常规用法

这破注释竟然能消除 error!

比如说这样的代码:

println((null as String?).length)

编译器会告诉你这样是不对的,然后向你丢出了一个 error:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

然后就可以使用 Suppress 让编译器闭嘴!

@Suppress("UNSAFE_CALL")
println((null as String?).length)

然后就可以顺利的通过编译,编译后跑一跑,就可以轻松地拿到一个空指针异常:

Exception in thread "main" java.lang.NullPointerException
    at .......

很简单,有了 Suppress,你就可以让这些可以本来通过编译器生成java字节码,但是因为各种各样的原因(例如类型安全)被ban掉的代码,顺利通过编译。

下面给几个可能有用的例子。

例1

fun some(any: Any) {
   when(any) {
      // Cannot check for instance of erased type: Array<String>
      is Array<String> -> println(any.size)
   }
}

这是我吐槽过的「Java能做但是Kotlin做不了的事」系列之一。在Java你可以检查一个对象是不是某个特定的数组类型 if (any instanceof String[]),但是Kotlin把这个问题一刀切了,导致Kotlin不能直接做这样的type check。

不考虑Kotlin的其他target(如js),在Jvm平台上有如下的迂回:

any is Array<*> && any.isArrayOf<String>()

就是很丑罢了。

在Kotlin的早期代码,这样的 type check 是允许的,但是后来为了保证类型安全,ban 掉了这样的写法。这个时候可以用 Suppress 把这个后门打开。

fun some(any: Any) {
   @Suppress("CANNOT_CHECK_FOR_ERASED")
   when(any) {
      is Array<String> -> println(any.size)
      // Smart cast to Array<String>
   }
}

读者可以想想为什么要ban掉这样的代码,毕竟这种代码有可能会出现类型安全问题,要格外小心!

PS:这个其实也是可以对非数组的泛型使用的,例如这样:

@Suppress("CANNOT_CHECK_FOR_ERASED")
when(any) {
   is List<String> -> println(any.size)
   // Smart cast to List<String>
}

这里实际上等价于以下代码:

when(any) {
   is List<*> -> println((any as List<String>).size)
}

并不是大家都想要的真泛型(不信可以传个 List<Int> 什么的进去试试),只是上面的代码可以享受到 Smart Cast 罢了。

可想而知,这样的代码也是有类型安全问题的。

例2

可以在非顶层空间定义类型别名。

@Suppress("TOPLEVEL_TYPEALIASES_ONLY")
class Some {
   typealias Str = String

   val a: Str = ""

   fun some() {
      typealias int = Int

      val some: int = 1
   }
}

主要是为了限制类型别名的作用域。另外把类型别名定义在顶层一定会生成一个 文件名Kt 的 class,可以用这个方法避免这种情况的发生。

例3

Kotlin 在远古版本 M13 新增了 lateinit 修饰符,当时是可以对只读属性(val)和可变属性(var)使用。

然后在下一个版本 M14 就残忍地 ban 掉了 lateinit val

现在你可以用 Suppress 绕开编译器对 lateinit val 的限制。

@Suppress("INAPPLICABLE_LATEINIT_MODIFIER")
lateinit val lateInitVal: String

lateinit val 的用处可以看上面 M13 的链接里面的说明。它的 backing field 并不是定义为 final 的,它和 lateinit var 的区别只是没有 setter,所以一般是配合各种注入框架来使用的。

但是没有 setter 并不能阻止我们直接对它进行赋值:

@Suppress("INAPPLICABLE_LATEINIT_MODIFIER")
lateinit val some: String

@Suppress("VAL_REASSIGNMENT")
fun main() {
   some = "1"
   println(some) // 输出 1
   some = "2"
   println(some) // 输出 2
}

嘻嘻~

例4

Kotlin Collection Literals 半成品。这个特性是JetBrains他们在17年4月份的时候做的关于Kotlin未来特性的一份调查问卷的第6个特性。简单地说是用一些特殊的语法来创建各种集合,而不是(丑陋的)listOfmapOf 等标准库函数。

@Suppress("UNSUPPORTED")
val bs: BooleanArray = [true, false, true]

但是这是个未完成的功能,目前只支持数组,在未来计划支持 List、Map 和 Set 的创建。所以为了避免不必要的兼容性麻烦,在正常情况下只允许在注解里这样用。要想在其他地方用,就像上面代码那样加上 Suppress 注解。

例5

可以用来解决某些与 Java 互操作时 package private 可见性带来的问题。

// 这是 Java 代码
package test;

abstract class Test {} // 仅包内可见
// 这是 Kotlin 代码
package test

@Suppress("EXPOSED_SUPER_CLASS")
class TestImpl : Test()

当去掉那行 Suppress 注解时编译无法通过。

结尾

想知道更多的 Suppress 选项的话,可以去翻 Kotlin 的源码。

这种奇技淫巧是无法得到官方支持的,也许下次更新就挂掉了,所以如果用了那就后果自负吧。

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2018/12/09/kotlin-suppress/