最近在学习 Kotlin 这门编程语言,不得不感叹 Kotlin 这语言是真的骚。
重载操作符
在伴生对象重载 invoke 操作符
interface BotUser {
val id: Long
val name: String
val description: String
companion object {
operator fun invoke(
id: Long,
name: String = "",
description: String = ""
): BotUser {
return BotUserImpl(id, name, description)
}
}
}
class BotUserImpl(...) { ... }
如果你这样做的话,可以让接口或者抽象类仿佛看起来能够被实例化一样(看似调用“构造方法”,实际上是调用了 invoke() )。
用途:项目中途将某个类抽象为一个接口,并将原有类作为此接口的默认实现,这样可以做到源代码的兼容(对 Java 代码以及二进制仍不兼容,毕竟本质不同)。
代码来自我的项目 “MoeCraftBotNG”
其他操作符重载
一般可以重载 get()
set()
来使对象能够按数组、字典一样去操作。
为对象编写 contains()
方法时(判断某个元素是否“属于”此对象),可以顺便为此方法加个 operator
关键字可以让 in
关键字支持这个对象
然而操作符重载还是慎用为妙,众所周知这个特性就是被 C++ 玩坏的,当你 java 甚至以不支持操作符重载为荣。好在 Kotlin 在这方面也比较节制。
协程
如果想让你的 suspend 函数被 Java 用户友善地调用,防止你被人砍,你可以:
用 Kotlin 编写一个类:
class Coroutines {
/**
* 获取在 Java 代码中调用 Kotlin Suspend 函数所需的最后一个参数 (Continuation)
* @param onFinished 当suspend函数执行完毕后所调用的回调。若 Throwable 不为 null 则说明执行失败。否则为执行成功
* @param dispatcher 协程执行线程的类型。可以为 Dispatchers.Default(CPU密集型) Dispatchers.Main(主线程) Dispatchers.IO(IO密集型)
*/
@JvmOverloads
fun <R> getContinuation(onFinished: BiConsumer<R?, Throwable?>, dispatcher: CoroutineDispatcher = Default): Continuation<R> {
return object : Continuation<R> {
override val context: CoroutineContext
get() = dispatcher
override fun resumeWith(result: Result<R>) {
//注意 Result 是 inline class,不可直接给出去
onFinished.accept(result.getOrNull(), result.exceptionOrNull())
}
}
}
}
kotlin suspend 函数的调用难点在于最后一个参数,这个参数是 suspend 函数自动生成的,但是在 Java 方面处理起来却十分棘手。你可以提供这样的一个类来生成最后一个参数的值。
然后在 Java 中,就可以这样调用了:
Coroutines coroutines = new Coroutines();
//假设一个 suspend fun login(username: String, password: String): RequestResult,位于 object UserUtils
UserUtils.INSTANCE.login("user", "pass", coroutines.getContinuation(
(result, throwable) -> {
//suspend fun执行结束的回调
System.out.println("Coroutines finished");
System.out.println("Result: " + result);
System.out.println("Exception: " + throwable);
}
)
);
另外,也可以使用 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8
,这个库可以让 suspend fun 返回 CompletableFuture<>
以便在 java 使用
fun doSomethingAsync(): CompletableFuture<List<MyClass>> =
GlobalScope.future { doSomething() } //返回 CompletableFuture 包装的 suspend fun doSomething()
内置函数
Kotlin 有很多实用的内置函数,只提几个。
run()
run() 字面意思,用于执行一个任意代码块,并返回代码块的返回值
run() 有两种,签名如下:
public inline fun <R> run(block: () -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R
第一种 run() 在为构造函数委托传递参数时特别有用,例如有一个类和两个次构造函数
class ManagedJavaProperties(val inputStream: InputStream, val outputStream: OutputStream? = null) {
constructor(file: File): this(file.inputStream(), file.outputStream())
constructor(fileName: String): this(???) //需要进行处理才能委托给其他构造函数
}
第二个次构造函数中需要对 fileName 进行一些处理,此时需要 run() 登场了:
class ... {
constructor(fileName: String): this(
kotlin.run {
val file = File(fileName)
if (!file.exists()) {
file.createNewFile()
}
file
}
)
}
第二种 run() 则是将调用者当作 this 传递给 lambda,除此之外和第一种完全相同
run() 也可以用于防止代码块内变量污染当前作用域。(类似于 Java 的 { }
)
抑制错误
Kotlin 的 @Suppress
注解不仅可以抑制警告,还可以抑制任何错误。具体的错误名字可以到 kotlin 编译器项目按错误提示寻找。
抑制错误可以写出很多魔法代码,比如
interface ErrorSuppressTest {
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION",
"NOTHING_TO_OVERRIDE",
"ABSTRACT_FUNCTION_WITH_BODY",
"INCOMPATIBLE_MODIFIERS",
"DECLARATION_CANT_BE_INLINED",
"OVERRIDING_FINAL_MEMBER")
public protected internal private final abstract override inline suspend fun foo() {
println("11")
}
}
当然这没有什么意义,不过有时这样做却是有意义的,比如下面这样一个 kotlin 的 Trait 实作:
interface TraitTest {
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION", "INCOMPATIBLE_MODIFIERS")
protected final fun foo() {
println("11")
}
}
Kotlin DSL
DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言,比如大家耳熟能详的 SQL 和正则表达式。
无论是通用编程语言,还是领域专用语言,最终都是要通过 API 的形式向开发者呈现。良好的、优雅的、整洁的、一致的 API 风格是每个优秀开发者的追求,而 DSL 往往具备独特的代码结构和一致的代码风格,从 SQL 和正则表达式的语法风格便可感受一二。
下面的代码是一个 QQ 机器人的代码。用户可通过调用 subscribeMessages()
来订阅机器人事件。
// 监听这个 bot 的来自所有群和好友的消息
this.subscribeMessages {
// 当接收到消息 == "你好" 时就回复 "你好!"
"你好" reply "你好!"
// 当消息 == "查看 subject" 时, 执行 lambda
case("查看 subject") {
if (subject is QQ) {
reply("消息主体为 QQ, 你在发私聊消息")
} else {
reply("消息主体为 Group, 你在群里发消息")
}
// 在回复的时候, 一般使用 subject 来作为回复对象.
// 因为当群消息时, subject 为这个群.
// 当好友消息时, subject 为这个好友.
// 所有在 MessagePacket(也就是此时的 this 指代的对象) 中实现的扩展方法, 如刚刚的 "reply", 都是以 subject 作为目标
}
// 当消息里面包含这个类型的消息时
has<Image> {
// this: MessagePacket
// message: MessageChain
// sender: QQ
// it: String (MessageChain.toString)
// message[Image].download() // 还未支持 download
if (this is GroupMessage) {
//如果是群消息
// group: Group
this.group.sendMessage("你在一个群里")
// 等同于 reply("你在一个群里")
}
reply("图片, ID= ${message[Image]}")//获取第一个 Image 类型的消息
reply(message)
}
"hello.*world".toRegex() matchingReply {
"Hello!"
}
"123" containsReply "你的消息里面包含 123"
// 当收到 "我的qq" 就执行 lambda 并回复 lambda 的返回值 String
"我的qq" reply { sender.id }
"at all" reply AtAll // at 全体成员
// 如果是这个 QQ 号发送的消息(可以是好友消息也可以是群消息)
sentBy(123456789) {
}
contains("关闭复读") {
if (repeaterListener?.complete() == null) {
reply("没有开启复读")
} else {
reply("成功关闭复读")
}
}
}
subscribeMessages {
case("你好") {
// this: MessagePacket
// message: MessageChain
// sender: QQ
// it: String (来自 MessageChain.toString)
// group: Group (如果是群消息)
reply("你好")
}
}
launch {
// channel 风格
for (message in this@messageDSL.incoming<FriendMessage>()) {
println(message)
}
// 这个 for 循环不会结束.
}
subscribeGroupMessages {
// this: FriendMessage
// message: MessageChain
// sender: QQ
// it: String (来自 MessageChain.toString)
// group: Group
case("recall") {
reply("😎").recallIn(3000) // 3 秒后自动撤回这条消息
}
case("禁言") {
// 挂起当前协程, 等待下一条满足条件的消息.
// 发送 "禁言" 后需要再发送一条消息 at 一个人.
val value: At = nextMessage { message.any(At) }[At]
value.member().mute(10)
}
startsWith("群名=") {
if (!sender.isOperator()) {
sender.mute(5)
return@startsWith
}
group.name = it
}
}
在这种精心构建的 DSL 内,代码与人的思维方式高度符合,可读性大大提高。
同时,即使用户并不熟悉 Kotlin ,甚至对编程都不熟悉,也可以快速编写出能用的代码,而且代码质量也不会太差。
我们也可以用 DSL 来替换 XXX.Builder() 类似的 Builder 设计。(当然用 Builder.apply {} 好像也行)
有关 DSL 的东西实在太多,这里不再赘述,可以阅读
https://juejin.im/entry/5a9b2320f265da238f12014
上述代码来自 mamoe 团队的项目 mirai
委托
可以使用委托来“精简”代码,使代码更佳易读。
众所周知 Android 的 Preference 是一个很常用的东西,用来存放一些设置。但用来存取却有些麻烦。
先看看使用委托后的效果:
定义一个文件
object UserPreferences : ManagedPreferences("user") {
var id: Int by preferenceOf()
var token: String by preferenceOf()
var name: String by preferenceOf()
var email: String by preferenceOf()
var role: Int by preferenceOf()
var age: Int by preferenceOf()
var sex: Sex by preferenceOf()
}
如果要存取此 Preference:
取 id: val id = UserPreferences.id
存 id: UserPreferences.id = 666
取在类中定义的 ziduan: UserPreferences["ziduan"] = "233"
存在类中定义的 ziduan: val data: String = MainPreferences["ziduan"]
这是 preferenceOf()
的定义:
open class ManagedPreferences(
private val preferenceName: String,
private val preferenceAccessMode: Int = Context.MODE_PRIVATE
) {
...
inline fun <reified T> preferenceOf( //Kotlin通过内联代码实现了真实的泛型
key: String? = null,
defValue: T? = null
): DelegatedPreference<T> {
return object :
DelegatedPreference<T> {
//实现 `val id = UserPreferences.id`
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
get(key ?: property.name, defValue)
//实现`UserPreferences.id = 666`
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
set(key ?: property.name, value)
}
}
inline fun <reified T> get(key: String, defValue: T?): T {
return when (T::class) { //对于不同类型的数据,有不同的存取方式
Float::class -> preferences.getFloat(key, (defValue ?: 0F) as Float) as T
Int::class -> preferences.getInt(key, (defValue ?: 0) as Int) as T
Long::class -> preferences.getLong(key, (defValue ?: 0L) as Long) as T
Boolean::class -> preferences.getBoolean(key, (defValue ?: false) as Boolean) as T
String::class -> preferences.getString(key, (defValue ?: "") as String) as T
Short::class -> preferences.getInt(key, (defValue as Short).toInt()).toShort() as T
Byte::class -> preferences.getInt(key, (defValue as Byte).toInt()).toByte() as T
Char::class -> preferences.getInt(key, (defValue as Char).toInt()).toChar() as T
Serializable::class -> { // Serializable 对象,有一个 WeakHashMap 进行缓存防止无谓的反复反序列化
if (cachedSerializableObjects.containsKey(key)) {
cachedSerializableObjects[key] as T
} else {
val serialized: String? = preferences.getString(key, null)
if (serialized == null) {
defValue as T
} else {
cachedSerializableObjects[key] =
T::class.java.newInstanceFromSerialized(serialized) as Serializable
cachedSerializableObjects[key] as T
}
}
}
Set::class -> preferences.getStringSet(
key,
(defValue ?: emptySet<String>()) as Set<String>
) as T
else -> throw IllegalArgumentException("Type not supported: ${T::class.qualifiedName} on $key")
}
}
//实现 `val data: String = MainPreferences["ziduan"]`
inline operator fun <reified T> set(key: String, value: T) {
when (T::class) { //对于不同类型的数据,有不同的存取方式
Float::class -> preferenceEditor.putFloat(key, value as Float)
Int::class -> preferenceEditor.putInt(key, value as Int)
Long::class -> preferenceEditor.putLong(key, value as Long)
Boolean::class -> preferenceEditor.putBoolean(key, value as Boolean)
String::class -> preferenceEditor.putString(key, value as String)
Short::class -> preferenceEditor.putInt(key, (value as Short).toInt())
Byte::class -> preferenceEditor.putInt(key, (value as Byte).toInt())
Char::class -> preferenceEditor.putInt(key, (value as Char).toInt())
Serializable::class -> {
val serialized = (value as Serializable).serializeToString()
preferenceEditor.putString(key, serialized)
cachedSerializableObjects[key] = value
}
Set::class -> preferenceEditor.putStringSet(key, value as Set<String>)
else -> throw IllegalArgumentException("Type not supported: ${T::class.qualifiedName} on $key")
}
}
//实现 `UserPreferences["ziduan"] = "233"`
inline operator fun <reified T> get(key: String) = get<T>(key, null)
...
}
用途:快速集中管理、编写 Preferences
代码来自我的项目 “android_utils”
扩展函数
可以扩展那些 Java 中众所周知的“无用”接口,使他们变得有用。以 Serializable
为例
fun Serializable.serializeToBytes(): ByteArray {
return ByteArrayOutputStream().use { bytes ->
ObjectOutputStream(bytes).use { obj ->
obj.writeObject(this)
}
bytes.toByteArray()
}
}
这样实现了 Serializable
接口类都可以直接调用 serializeToBytes()
方法来直接获得此类的序列化的结果
正确地使用扩展函数可以极大地提升开发效率和代码可读性。但最好不要滥用扩展函数,否则可能造成 IDE 方法提示过长、令使用者迷惑
代码来自我的项目 “java_utils”
TODO
如果你现在定义了一个方法但又不想实现它,可以写 TODO()
这样方法就无需实现了并且可以过编译。假设此方法被执行,它将抛出 NotImplementedError