惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Heimdal Security Blog
S
Security @ Cisco Blogs
N
News | PayPal Newsroom
J
Java Code Geeks
罗磊的独立博客
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
V
V2EX
WordPress大学
WordPress大学
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
AI
AI
小众软件
小众软件
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
A
Arctic Wolf
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
美团技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
F
Full Disclosure
B
Blog RSS Feed
Forbes - Security
Forbes - Security
S
SegmentFault 最新的问题
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
Jina AI
Jina AI
Cisco Talos Blog
Cisco Talos Blog
U
Unit 42
Project Zero
Project Zero
H
Hacker News: Front Page
Y
Y Combinator Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Cloudflare Blog
大猫的无限游戏
大猫的无限游戏
S
Secure Thoughts
The Hacker News
The Hacker News
Microsoft Azure Blog
Microsoft Azure Blog

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog 利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog 状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 扔鸡蛋问题 | Kenvix's Blog 计算机幻觉从入门到入土 | Kenvix's Blog Java 注解预处理 Annotation Processing & 代码生成 Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog Windows 选择指定的网卡来开承载网络型热点 | Kenvix's Blog 修复升级 Windows10 版本后所有内置应用闪退+第三方应用参数错误的问题 | Kenvix's Blog 配置用于 Gradle6.x + MySQL 8 的 jOOQ 3.14 代码自动生成 (已更新) 修复 Windows 环境下的程序访问 WSL 中的 MySQL 提示 Access Denied 的问题 修复 WSL 下 PHP+FastCGI 卡死的问题 使用任意磁盘或路径保存 Windows 文件历史记录 | Kenvix's Blog [1.12.2+Mod] MoeCraft :: 自由开放的科技向公益 Mod 服务器 Kenvix's Blog 禁用使用Intel核显的Windows笔记本自动调节亮度功能 | Kenvix's Blog 真正实现Minecraft高级登录(外置登录)的几种方案 | Kenvix's Blog 谈谈神舟的两艘贼船,Z7M-KP7S1 / Z7M-KP7SC USBCopyer: 插上U盘自动按需复制文件 | Kenvix's Blog USBCopyer 回调功能详细说明 | Kenvix's Blog C# 实现自定义"应用程序设置"的配置文件(user.config)存储路径 | Kenvix's Blog Win10 资源管理器为所有格式激活“编辑”按钮并修改文本文件“编辑”按钮的编辑器 | Kenvix's Blog 留言板 | Kenvix's Blog 又一次 Hello world | Kenvix's Blog Java 学习笔记 (仍在更新) | Kenvix's Blog 在Win10 Pro下挂载NFS(网络文件系统) | Kenvix's Blog Nginx 反向代理 Aria2 JSONRPC | Kenvix's Blog (Android6.0~9.0) 清除锁屏密码 | Kenvix's Blog WordPress 更换站点地址后批量修改文章/评论中的旧地址 | Kenvix's Blog 修复一加3/3T因固件过老导致刷入ROM时提示错误7的问题 | Kenvix's Blog 修复Android DM-Verity 警告 | Kenvix's Blog 贴吧云签到 资源索引(下载|文档|插件) | Kenvix's Blog 继续监控!使用树莓派+Motion实现实时视频监控并通过浏览器查看 | Kenvix's Blog 自动获取Pixiv每日排行榜第一张图片(600x600 | 可用于博客背景图) | Kenvix's Blog 使用树莓派实现定时拍照监控并发送邮件到邮箱 | Kenvix's Blog 好压 V2.7 Beta1 绿色版——功能强大,良心的压缩软件 | Kenvix's Blog 任意语言实现读取压缩包注释 | Kenvix's Blog 自己实现QQ群自定义分享(管理员开启了群交易?) | Kenvix's Blog MoeCDN - 加速Gravatar/GoogleAPIs等无法在国内访问的资源 | Kenvix's Blog [Minecraft] WebLogin-连接到你的服务器来检查玩家是否可以登录 | Kenvix's Blog Android卡刷包提示This package is for device: ... this device is ...的解决方案 Kenvix's Blog 给EMLOG评论框加上复选框[√]防止垃圾评论 | Kenvix's Blog 欢迎使用emlog | Kenvix's Blog
Kotlin 的那些骚操作 | Kenvix's Blog
2019-12-13 · via Kenvix's Blog

最近在学习 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