MiraiForum

    • 注册
    • 登录
    • 搜索
    • 热门
    • 最新
    • 未解决
    • 标签
    • 群组
    • 友情链接
    1. 主页
    2. PigeonYuze
    • 资料
    • 关注 0
    • 粉丝 2
    • 主题 4
    • 帖子 29
    • 最佳 4
    • 有争议的 0
    • 群组 1

    PigeonYuze

    @PigeonYuze

    7
    声望
    49
    资料浏览
    29
    帖子
    2
    粉丝
    0
    关注
    注册时间 最后登录

    PigeonYuze 取消关注 关注
    CaptchaFree

    PigeonYuze 发布的最佳帖子

    • 一些你可能不知道的神秘 Kotlin 语法糖

      一些奇怪的语法糖

      这些语法糖在 Kotlin 1.7.10 的环境中都可以运行

      具体这些语法糖是在什么时候更新的,还请各位读者自己私下去探索

      本文仅是分析一些笔者在实际项目开发中遇到的神奇的糖,在此进行分享。

      你可以看作本文为抛砖引玉中的砖,可以在本文下进行更多奇怪糖分的讨论。

      不稳定更新中x

      重载运算符

      对应 Kotlin 文档 Kotlin Docs Operator overloading

      在一些常见的语言中,你可能会看到一些支持重载运算符的的语言,如:Rust,C#,C++等众多语言中都支持这一操作

      在编译期间,这些操作符一般都会编译为调用对应的函数

      例如,在kotlin中

      val list = listOf(0,1,2,3,4)
      val element = list[0]
      

      这段代码实则等同于

      val list = listOf(0,1,2,3,4)
      val element = list.get(0)
      

      其中的 [0] 被编译为了调用函数 get(0)

      因此,调用运算符其实本质上属于调用函数

      那我们可否实现像实现函数一样实现重载运算符呢?

      答案是可以的,让我们以mirai中MessageChain的部分代码为例:

      operator fun get(key: MessageKey<SingleMessage>): SingleMessage? {
          return firstOrNull { /* ... */ true  } as SingleMessage?
      }
      

      为便于理解,删除了注解, 部分可省略的声明以及泛型声明

      这段代码的作用是从 MessageChain 中获取支持的元素源

      举个例子,从 MessageChain 中获取所包含的图片信息源

      val message: MessageChain = ...;
      val imageSource = message[Image.Key]
      

      在这段代码中,我们可以使用 [] 运算符

      operator fun 是一种标识符,它代表着这个函数会重载运算符
      (当为 覆盖override 时,可省略operator关键词)

      在操作符中,存在有

      • 一元运算符

      重载函数不需要参数

      例如我们常见的 递增 递减 等操作

      一般来说,重载运算符不应改变调用的对象

      • 二元运算符

      重载函数需要提供参数

      参数的数量不限, 甚至支持 vararg 选项

      • 一些特殊的运算符

      例如对于属性的委托(by) 也可以使用运算符实现

      以及如 迭代器 等 操作(并不进行阐述,因为没什么特殊的)都可以运算符重载

      重载操作符操作并不限制代码所在模块

      你可以使用 扩展函数 以扩展指定对象的运算符

      Kotlin 要求重载运算符为成员函数*(在一个对象中,包含接口)*或扩展函数

      运用重载运算符可以实现许多神奇的功能

      可惜的是,在Java中并不存在此类操作,所有在 kotlin 中的重载运算符在 Java 中并没有效果

      Java 会将 Kotlin重载运算符 视为一个方法

      当运算符位于一个类伴生对象中时,你可以直接从类调用运算符
      例如:

      class Example {
          companion object{
              operator fun get(int: Int) {
                  TODO()
                  //...
              }
          }
      }
      
      fun testExample() {
          Example[114514] // 可通过编译
      }
      

      一元前缀运算符

      此类运算符并不要求提供参数

      运算符 对应函数名称
      + unaryPlus()
      - unaryMinus()
      ! not()

      这些函数并不要求返回类型,你可以不返回任何内容.

      递增 & 递减

      此类运算符要求不提供参数

      此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
      它们不应改变调用的对象。

      注: 前缀与后缀都属于此类操作 a++和++a都调用的是同一个函数

      运算符 对应函数名称
      ++ inc()
      -- dec()

      二元运算符

      此类运算符要求提供参数

      以下运算符中,要求提供一个参数

      运算符 对应函数名称
      + plus
      - minus
      * times
      / div
      % rem
      .. rangeTo

      Kotlin 1.9.0 新增运算符

      ..< rangeUntil

      以下运算符在实际运算符中会为变量重新赋值,需要一个参数

      此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
      它们不应改变调用的对象。

      运算符 对应函数名称
      += plusAssign
      -= minusAssign
      *= timesAssign
      /= divAssign
      &= remAssign

      以下运算符需要传递一个参数,并且必须返回布尔值Boolean

      运算符 对应函数名称
      in contains
      == equals

      !in 实则等同于 !contains

      在 == 中
      针对null 此操作永远不会调用equals, 始终为true

      一些特殊的运算符

      常有的 [] 索引访问运算符, ()调用运算符, 甚至是 by 都可以通过此重载


      • 索引访问运算符 []

      对应函数名: get或set

      get 用于访问元素, set 用于修改元素

      不限参数长度, 但至少有一个参数。 不限返回类型.

      对照表:

      运算符 对应函数
      a[i] a.get(i)
      a[i,j] a.get(i,j)
      a[i] = b a.set(i,b)
      a[i, j] = b a.set(i, j, b)

      在 set 函数中

      修改后的元素始终在参数的最后一位


      • 调用运算符 ()

      对应函数名 invoke

      实现后,它看起来就像是在实例化一个对象一样.

      对于 lambda , 这通常被用来调用 lambda

      例如:

      fun example(lambda: String.(Int) -> Unit) {
          // 我们可以通过 () 调用代替 invoke
          lambda("String", 114514) // 看起来和调用函数一样
      }
      

      但当 invoke 处于一个类的伴生对象时,可能会导致 构造函数 与 invoke 函数重合的状况

      在这种情况下 kotlin 会优先考虑是否为构造函数,随后被编译为实例化对象

      class TestOperator{ // 默认空构造参数
          init {
              println("--init--")
          }
          companion object {
              operator fun invoke() { 
                  println("--operator invoke--")
              }
          }
      }
      fun main() {
          TestOperator()
      }
      

      在这段代码中,就会得到输出 --init--

      而当运算符未一致时,如:

      class TestOperator{ // 默认空构造参数
          init {
              println("--init--")
          }
          companion object {
              operator fun invoke(int: Int) {
                  println("--operator invoke $int--")
              }
          }
      }
      fun main() {
          TestOperator(114514)
      }
      

      这时候的输出就是 --operator invoke 114514-- 了,因为它被编译为了调用重载运算符(不符合构造函数)


      • 比较运算符

      此处逻辑与 Java 一致

      函数名为 compareTo 传入一个参数,始终返回一个Int类型的数据

      一般来说,

      • 当此对象小于另一个对象时, compareTo 返回小于0的数据(一般为-1)
      • 当此对象值等于另一个对象时, compareTo 返回 0
      • 当此对象大于另一个对象时, compareTo 返回大于0的数据(一般为1)
      运算符 对应函数名称
      a > b a.compareTo(b) > 0
      a < b a.compareTo(b) < 0
      a >= b a.compareTo(b) >= 0
      a <= b a.compareTo(b) <= 0

      • 委托属性运算符

      在 Kotlin 中存在有委托字段的操作
      例如:

      val finalDelegatedProperty by lazy {  } // `lazy {}` 为委托器实现
      var mutableDelegateProperty by lazy { }
      

      通过 重载运算符 你可以实现委托字段的操作

      • 对于 val 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型

      • 对于 var 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型, 以及 setValue(thisRef: Any?, property: KProperty<*>, value: 字段类型)

      • thisRef 为属性需要的对象, 当为成员属性时为所属对象 反之则为null

      • property 是为当前属性的相关信息,如属性名称等


      *在 Kotlin 中实现切片

      这是一段在rust中的字符串切片实现

      fn slice() {
          let mut string = String::from("Hello World!");
          let slice = &string[0..3]; // 值为: Hell
      }
      

      Rust 中的切片还支持有如..y, .., x.. 的实现 但这些都难以在 kotlin 中实现,故不参与讨论

      在kotlin中,我们有着针对 x..y 的操作符实现 (range-to 操作符)

      所以, 0..3这段代码实则等于 IntRange(0,3)

      我们只需要一个接受 IntRange 的函数即可

      由于在 String 中并没有定义 get(IntRange) 这一函数, 所以在这里我们需要通过扩展函数以实现功能

      operator fun String.get(range: IntRange): String {
          return this.substring(range) // 截取字符串指定范围
      }
      

      这样,我们就实现了字符串切片的内容。同样地,我们还可以实现集合切片:

      operator fun <T> List<T>.get(range: IntRange): List<T> {
          return this.subList(range.first,range.last)
      }
      

      这样,我们就可以在 kotlin 中运行以下代码了

      fun slice() {
          val str = "Hello World!"
          val strSlice = str[0..3] // 值为 Hell
          
          val list = listOf(0,1,2,3,4)
          val listSlice = list[0..3] // 值为 0,1,2,3
      }
      operator fun String.get(range: IntRange): String {
          return this.substring(range) 
      }
      operator fun <T> List<T>.get(range: IntRange): List<T> {
          return this.subList(range.first,range.last)
      }
      

      *可 null 值 的扩展函数

      或许你会有这样的需求

      一个Map中有多个嵌套Map,

      一般来说,Map通过get函数获取对应的值时,会返回一个可null值 V?

      此时我们尝试获取最后一个嵌套 Map 内的值,就会显得十分不优雅

      val example = 
          mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>() 
          // 真的会有人这样写吗?
      fun getFinalValue() {
          val finalValue: Long = 
              example["Key 0"]!!["Key 1"]!!["Key 2"]!!["Key 3"]
          // Or ...    
          val finalValue1: Long = 
              example["Key 0"]?.get("Key 1")?.get("Key 2")?.get("Key 3")
      }
      

      由于 Kotlin 中的运算符仅支持对于 非null值的调用

      所以,

      • 要么,我们需要使用一连串的!!以将值转换为不可空值(无法为null)
      • 要么,我们使用?.调用方法,而一连串的?.get(..)则显得更加不美观

      此时,我们可能使用造轮传统来防止这样的清空发生

      同样地,我们依旧需要使用扩展函数以支持从外界重写操作符

      operator fun <K,V> Map<K,V>?.get(key: K): V {
          return this!![key] // Or more...
      }
      

      在这里,我们通过 Map<K,V>? 使它支持了对于可空值的[]语法支持

      让我们尝试用这段代码代替原本的代码吧!

      operator fun <K,V> Map<K,V>?.get(key: K): V {
          return this!![key]
      }
      
      val example =
          mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>() 
      
      fun getFinalValue() {
          val finalValue = example["Key 0"]["Key 1"]["Key 2"]["Key 3"]
      }
      

      这样,我们就成功使代码看着更加美观。

      不过,这样写具有一定的坏处———你无法从代码层次直接知晓这个值是否可能为null

      这可能会降低你的代码可读性,请酌情使用.

      以此类推,我们还有

      operator fun <T> List<T>?.get(index: Int) = this!![index]
      

      功能同上,可直接调用 索引运算符

      本地函数 Local Function

      此处的本地函数不指native函数,而是指函数嵌套内的函数

      不建议在实际生产中这么做,它会大大降低你的代码可读性

      总所周知,在Kotlin中,我们可以通过fun关键词声明一个函数

      它看起来会是这样

      fun helloWorld() {
          println("Hello World!")
      }
      

      然而你知道吗,我们还可以往一个函数中套更多的函数

      像这样

      fun helloWorld() {
        fun getHelloWorldMessage() = "Hello World"
        println(getHelloWorldMessage())
      }
      

      我们可以发现,这段函数竟然被成功编译,并且成功运行输出Hello World

      让我们用idea自带的显示字节码,查看相关代码

      // class version 55.0 (55)
      // access flags 0x31
      // 普通函数 helloWorld: Unit
        public final static helloWorld()V
         L0 // 调用内嵌函数 getHelloWorldMessage()
          LINENUMBER 5 L0
          INVOKESTATIC com/pigeonyuze/DeliciousSugarKt.helloWorld$getHelloWorldMessage ()Ljava/lang/String;
          POP
        // More code...
      
      // helloWorld 嵌套本地函数 getHelloWorldMessage: String
        private final static helloWorld$getHelloWorldMessage()Ljava/lang/String;
         L0
          LINENUMBER 4 L0
          LDC "HelloWorld"
          ARETURN
         L1
          MAXSTACK = 1
          MAXLOCALS = 0
          MAXLOCALS = 0
      

      由上可见,我们的代码实则被编译成了两个方法
      helloWorld() 和 helloWorld$getHelloWorldMessage() 方法

      而其中本地函数的标识符无论如何为private
      Note: 你始终无法为嵌套函数设置标识符,你始终只能从本地函数所在函数中调用它
      如:

      fun superFunction() {
          fun childFunction() {}
          childFunction() // 可以通过编译
          private fun childFunction1() {}
          // 无法通过编译: Modifier 'private' is not applicable to 'local function'
          // NOTE: 无法向本地函数添加标识符
      }
      
      fun elseFunction() {
          childFunction()
          // 无法通过编译: Unresolved reference : getHelloWorldMessage
          // NOTE: 无法从外处调用本地函数
      }
      

      或许你可以猜到,你可以无限地嵌套函数

      fun helloWorld() {
       fun helloWorld0() {
         fun helloWorld1() {
           //More shit
         }
       }
      }
      

      这些函数经过编译后,都会被编译为一个单独的 private final函数。

      值得注意的是,kotlin中的inline函数并不支持本地函数的使用。

      kotlin的inline函数会将原函数体内联到调用方处,因此如果内联了本地函数可能会导致意想不到的错误。

      逻辑或|| Logical OR

      我们大家都知道, 我们可以通过逻辑或门||获取一串布尔值中是否包含真值(true)

      false || true // true
      true || true // true
      false || false //false

      它是按照从左到右的顺序来计算的。

      如果你有着乱翻kotlin代码的习惯的话

      你会在kotlin/native关于Throwable获取栈信息的源码里看到以下代码

      // kotlin.Throwable.kt line 77
      private fun Throwable.dumpFullTrace(indent: String, qualifier: String) {
          this.dumpSelfTrace(indent, qualifier) || return
          var cause = this.cause
          while (cause != null) {
              cause.dumpSelfTrace(indent, "Caused by: ")
              cause = cause.cause
          }
      }
      

      一句神奇的代码躲藏在其中

      this.dumpSelfTrace(indent, qualifier) || return
      

      是的没错,这是一个逻辑或门,可是它有着这么一个与众不同的点
      运算符的左侧是布尔值,而右侧却是函数跳出点
      通过反编译为java代码

      if(!this.dumpSelfTrace(indent, qualifier)) {
          return;
      }else {
          Throwable cause = this.cause;
          while (cause != null) {
              cause.dumpSelfTrace(indent, "Caused by: ");
              cause = cause.cause;
          }
      }
      

      Kotlin 编译器将这段||的神奇代码,转变成了if-else。

      当逻辑或运算符左侧不满足时,理应向右继续计算,而此时的右值为return,函数的跳出点。
      那么此时,kotlin该做什么呢?没错,它执行了右侧的语句return代码,这使得程序直接跳出。

      此时,我们甚至可以执行run函数(需要为inline函数,因为这样可以跳出调用处函数),只要它跳出了函数就行

      fun getBoolean(): Boolean { /* More code */ }
      fun call() {
         getBoolean() || run {
             println("Wow, I'm running!")
             throw Throwable()
             // Also, you can just 'return'
         }
         println("Go go go!")
        /* More code... */
      }
      

      调用call()后,如果getBoolean不满足则会运行run内联函数,因为内联函数中最后会跳出函数(抛出错误或return函数),run内联函数中的内容都会运行。
      这相当于

      fun getBoolean(): Boolean { /* More code */ }
      fun call() {
          if(!getBoolean()) {
              println("Wow, I'm running!")
              throw Throwable() //Also, you can just 'return' 
          }
           println("Go go go!")
           /* More code... */
      }  
      

      理论上,你可以不断||下去,只要最右侧跳出即可

      Random.nextBoolean() || (Random.nextBoolean() && Random.nextBoolean()) || /* More Boolean*/ || return
      

      这依托代码你是可以成功编译并运行的,他将会像上文一样编译为一段if-else,就像:

      if (!Random.nextBoolean()) {
        if(!(Random.nextBoolean() && Random.nextBoolean()))  {
          if(!/*More Boolean*/) return
        }
      }
      

      在循环体中,你还可以使用break或continue

      while(true) {
       booleanValue() || continue
       booleanValue() || break
       booleanValue() || return
       booleanValue() || throw Throwable()
      }
      

      这些代码同样也可以编译或运行
      是否在实际项目中使用可根据实际项目复杂度来看
      毕竟过度的||,何尝不是依托呢(笑

      需要 Lambda 的函数

      在讨论如何编写关于 lambda 的函数前,或许我们可以先来了解一下什么是lambda

      在kotlin中,几乎所有的表达式中的{}都会被编译为一个lambda对象

      如果你学习或使用过java,会发现这一点与java亦或是其他语言都有所不同。

      你可以猜想运行以下代码,你会得到哪些输出.

      fun main() {
          { println("Hello world") }
          if (true) { println("I am in an if expression") }
          when {
              true -> { println("I am in an when expression") }
          }
          { println("Invoked lambda by call invoke function") }.invoke();
          {
              println("Invoked lambda by this.()")
          }()
          val lambda = {
              println("Um-mm. why i am in a local variable.")
          }
          lambda() // = lambda.invoke()
          val newLambda = { "Please invoke me." } // UNUSED
          run {
              println("Run!")
          }
      }
      

      运行以上代码,我们得到了以下输出

      I am in an if expression
      I am in an when expression
      Invoked lambda by call invoke function
      Invoked lambda by this.()
      Um-mm. why i am in a local variable
      Run!
      

      等等,是不是少了一句"应有的"输出。

      或许我该告诉你,kotlin的{}就是这么反直觉。

       { println("Hello world") }
      

      这段代码,被解析为了一个 lambda 表达式,而从未调用的 lambda 被编译器所忽略

      或许并没有忽略,在字节码中,这段代码看起来是这样的

      LINENUMBER 2 L0
      GETSTATIC ...
      POP
      

      在字节码层面,它执行了压栈和出栈操作,但仅此而已。

      if (true) { println("I am in an if expression") }
      when {
         true -> { println("I am in an when expression") }
      }
      

      而这两段代码中的{}并没编译为 lambda 表达式,故可以直接运行。

      lambda 表达式并不会解析为一个单独的 函数/方法

      要想调用 lambda 表达式,你应该通过 invoke 函数进行调用

      或者你可以使用 kotlin.run 这一个内联函数

      public inline fun <R> run(block: () -> R): R {
          /* more codes */
          return block()
      }
      

      这是一个需要传入 lambda 的函数

      我们来看看它的参数声明部分

      block: () -> R
      
      • block 为参数名
      • () -> R 为参数类型

      让我们从 () -> R 中谈谈.

      • -> 表明了这是一个 lambda 体
      • -> 后的内容(R)表明了这一个lambda体需要返回的类型
      • -> 前的内容(())表明了这一个lambda体需要提供的类型

      以此类推,我们现在由以下表达式

      String.(Int) -> Any
      

      如果你知道什么是扩展函数,这里会更好理解一些

      fun String.lambda(it: Int): Any
      
      • String 是接收器, 在 lambda 体中使用 this 调用
      • Int 是形参, 在 lambda 体中通常使用 it 调用,也可以使用形参声明规定调用名称, 如`{ int -> /**/ }
      • Any 是返回类型

      你也可以使用泛型,像是这样

      K.(V) -> R
      

      写在函数参数里面,它看起来像是这样:

      fun <K,V,R> invoke(name: K.(V) -> R) {} // 使用 `name` 调用 lambda
      

      或者也可以作为扩展函数,像这样:

      fun <K,V,R> (K.(V) -> R).invoke() {} // 使用 `this` 调用 lambda
      

      你甚至可以使用 suspend 标识这个 lambda 可以为挂起的。例如:

      suspend fun (suspend () -> Any).run() {
          this.invoke()
      }
      suspend fun run(arg: suspend () -> Any) {
          this.invoke()
      }
      

      总结一下,如果你需要将 lambda 作为函数的 形参/扩展函数接收器
      你可以这么写:

      suspend fun <K,V,R> (suspend K.(V) -> R).run0() {}
      suspend fun <K,V,R> (suspend K.(V) -> R).run1(run: suspend K.(V) -> R) {}
      fun <K,V,R> (K.(V) -> R).run2() {}
      // ...
      fun <K,R> (K.() -> R).run3() {}
      fun <R> (() -> R).run4() {}
      fun <R> run5(arg: () -> R) {}
      

      实际生产可参考案例

      suspend fun handleData() {}
      
      fun main() =
          suspend {
              handleData()
          }.runBlock()
      
      fun (suspend () -> Any).runBlock() {
          runBlocking { invoke() }
      }
      
      发布在 技术交流板块
      PigeonYuze
      PigeonYuze
    • GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      GroupBotSuffix

      基于mirai的自动增加qqbot群名后缀的插件

      通过本插件,你可以实现在bot的每一个群聊都自动修改群名片为含后缀 (如现在是北京时间xx:xx此类的表达)

      github链接

      releases链接

      请使用版本号大于 1.3.0 的包,在小于 1.3.0 的版本中,可能会出现被 mirai 服务器断开连接的问题

      如何使用?

      将 releases 下的最新包下载,随后丢到plugins里面

      随后打开mcl在加载完毕后停止,打开config页面,修改本插件的Setting.yml

      默认会在成功加载后每隔一分钟修改一次

      配置说明

      • waitTimeMS
        每次间隔的时间 单位为毫秒
        不建议将此项设得过快 因为mirai并不主动推送群bot名片修改的事件(用户查询bot群名片/发送信息时才可能修改) 过快并不一定有效
      • open
        添加后缀类型的值
        可以为以下内容
        • NOW_TIME 现在的时间 可提供参数自行设置 默认为HH:mm:ss
        • HOW_LONG_TO_DISTANCE 距离什么时候还有多久 需要用参数提供指定日期
          年月日用-分割,且位于开头(可不提供年) 时分秒用:分割(可不提供)
          返回的格式与提供的格式一致
        • CPU_LOAD 系统cpu占用率
        • JVM_CPU_LOAD jvm可使用的cpu占用率
        • MEMORY_LOAD 系统内存占用率
        • JVM_MEMORY_LOAD jvm可使用内存占用率
      • content
        后缀的内容
        使用%s为默认配置的调用
        使用%加参数内容为有参数的配置调用
      • separator
        bot昵称到后缀的分割(默认会重命名为 bot 名称 + 后缀分隔符 + 后缀内容)
      • waitGroupMS
        经过多少毫秒后修改下一个群聊的群名片 单位为毫秒
      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      @南栀沁寒 已修复bug,下载最新版就可以了,测试的时候只跑了一遍就不跑了,十分抱歉(

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • YamlBot - 基于yaml配置的指令编写插件

      YamlBot

      https://github.com/PigeonYuze/YamlBot

      一个基于mirai自带的config中的yaml配置以开发指令的插件

      由于yaml对空格的敏感 在出现相关保存时,请检查您的配置文件是否正确

      同时插件中的部分功能需要ffmpeg的支持,您可以选择性地选择是否加入该功能(调用上传语音相关功能时会自动将.mp3格式转换为silk)

      详细内容可查看readme中的介绍

      以一言为例
      你可以通过以下的yaml代码

      # 指令处理
      COMMAND: 
        - name: 
            - '/hikokoto'
            - 一言
          answeringMethod: QUOTE
          answerContent: '『 %call-hitokoto% 』 —— %call-from%'
          run: 
            - use: HTTP
              call: content
              args: 
                - 'https://v1.hitokoto.cn'
              name: content
            - use: BASE
              call: parseJson
              args: 
                - '%call-content%'
                - hitokoto
              name: hitokoto
            - use: BASE
              call: parseJson
              args: 
                - '%call-content%'
                - from
              name: from
      

      实现以下的效果
      d810e081-a701-4a6e-a1cf-3e4b5b9868ea-image.png

      以上功能在插件成功加载后会自动生成

      另,您所编写的指令可以上传至your-config 以功能-原名的格式上传您的代码

      年轻人的第一个插件何止是瑟图插件

      发布在 插件发布
      PigeonYuze
      PigeonYuze

    PigeonYuze 发布的最新帖子

    • RE: GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      @PigeonYuze 已更新
      v1.3.0-release

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      旧的代码存在问题,请考虑暂时停止对原本此插件的使用!!!
      问题描述: GroupBotSuffix/11

      直到发布了下一个版本后,再使用插件的最新版本。

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: 一些你可能不知道的神秘 Kotlin 语法糖

      更新了关于重载操作符操作的说明,总字节 7583

      发布在 技术交流板块
      PigeonYuze
      PigeonYuze
    • RE: 一些你可能不知道的神秘 Kotlin 语法糖

      更新了一下,写得有点水,下午接着写。
      晚上再写几个语法糖, 挖个坑先。

      import net.mamoe.mirai.event.events.BotMuteEvent
      
      fun test(run: (BotMuteEvent) -> Unit)  {
      }
      
      fun main() {
          test { (durationSecond,operator) -> // For data class
          }
      }
      

      和

      class WrongYamlTypeError(vararg val shouldBe: String) {}
      
      发布在 技术交流板块
      PigeonYuze
      PigeonYuze
    • RE: 一些你可能不知道的神秘 Kotlin 语法糖

      @diyigemt 还有一些糖等有时间了再补上x

      发布在 技术交流板块
      PigeonYuze
      PigeonYuze
    • 一些你可能不知道的神秘 Kotlin 语法糖

      一些奇怪的语法糖

      这些语法糖在 Kotlin 1.7.10 的环境中都可以运行

      具体这些语法糖是在什么时候更新的,还请各位读者自己私下去探索

      本文仅是分析一些笔者在实际项目开发中遇到的神奇的糖,在此进行分享。

      你可以看作本文为抛砖引玉中的砖,可以在本文下进行更多奇怪糖分的讨论。

      不稳定更新中x

      重载运算符

      对应 Kotlin 文档 Kotlin Docs Operator overloading

      在一些常见的语言中,你可能会看到一些支持重载运算符的的语言,如:Rust,C#,C++等众多语言中都支持这一操作

      在编译期间,这些操作符一般都会编译为调用对应的函数

      例如,在kotlin中

      val list = listOf(0,1,2,3,4)
      val element = list[0]
      

      这段代码实则等同于

      val list = listOf(0,1,2,3,4)
      val element = list.get(0)
      

      其中的 [0] 被编译为了调用函数 get(0)

      因此,调用运算符其实本质上属于调用函数

      那我们可否实现像实现函数一样实现重载运算符呢?

      答案是可以的,让我们以mirai中MessageChain的部分代码为例:

      operator fun get(key: MessageKey<SingleMessage>): SingleMessage? {
          return firstOrNull { /* ... */ true  } as SingleMessage?
      }
      

      为便于理解,删除了注解, 部分可省略的声明以及泛型声明

      这段代码的作用是从 MessageChain 中获取支持的元素源

      举个例子,从 MessageChain 中获取所包含的图片信息源

      val message: MessageChain = ...;
      val imageSource = message[Image.Key]
      

      在这段代码中,我们可以使用 [] 运算符

      operator fun 是一种标识符,它代表着这个函数会重载运算符
      (当为 覆盖override 时,可省略operator关键词)

      在操作符中,存在有

      • 一元运算符

      重载函数不需要参数

      例如我们常见的 递增 递减 等操作

      一般来说,重载运算符不应改变调用的对象

      • 二元运算符

      重载函数需要提供参数

      参数的数量不限, 甚至支持 vararg 选项

      • 一些特殊的运算符

      例如对于属性的委托(by) 也可以使用运算符实现

      以及如 迭代器 等 操作(并不进行阐述,因为没什么特殊的)都可以运算符重载

      重载操作符操作并不限制代码所在模块

      你可以使用 扩展函数 以扩展指定对象的运算符

      Kotlin 要求重载运算符为成员函数*(在一个对象中,包含接口)*或扩展函数

      运用重载运算符可以实现许多神奇的功能

      可惜的是,在Java中并不存在此类操作,所有在 kotlin 中的重载运算符在 Java 中并没有效果

      Java 会将 Kotlin重载运算符 视为一个方法

      当运算符位于一个类伴生对象中时,你可以直接从类调用运算符
      例如:

      class Example {
          companion object{
              operator fun get(int: Int) {
                  TODO()
                  //...
              }
          }
      }
      
      fun testExample() {
          Example[114514] // 可通过编译
      }
      

      一元前缀运算符

      此类运算符并不要求提供参数

      运算符 对应函数名称
      + unaryPlus()
      - unaryMinus()
      ! not()

      这些函数并不要求返回类型,你可以不返回任何内容.

      递增 & 递减

      此类运算符要求不提供参数

      此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
      它们不应改变调用的对象。

      注: 前缀与后缀都属于此类操作 a++和++a都调用的是同一个函数

      运算符 对应函数名称
      ++ inc()
      -- dec()

      二元运算符

      此类运算符要求提供参数

      以下运算符中,要求提供一个参数

      运算符 对应函数名称
      + plus
      - minus
      * times
      / div
      % rem
      .. rangeTo

      Kotlin 1.9.0 新增运算符

      ..< rangeUntil

      以下运算符在实际运算符中会为变量重新赋值,需要一个参数

      此类重载函数必须返回一个值, 且应属于原类型 ,该值将分配给使用操作的变量。
      它们不应改变调用的对象。

      运算符 对应函数名称
      += plusAssign
      -= minusAssign
      *= timesAssign
      /= divAssign
      &= remAssign

      以下运算符需要传递一个参数,并且必须返回布尔值Boolean

      运算符 对应函数名称
      in contains
      == equals

      !in 实则等同于 !contains

      在 == 中
      针对null 此操作永远不会调用equals, 始终为true

      一些特殊的运算符

      常有的 [] 索引访问运算符, ()调用运算符, 甚至是 by 都可以通过此重载


      • 索引访问运算符 []

      对应函数名: get或set

      get 用于访问元素, set 用于修改元素

      不限参数长度, 但至少有一个参数。 不限返回类型.

      对照表:

      运算符 对应函数
      a[i] a.get(i)
      a[i,j] a.get(i,j)
      a[i] = b a.set(i,b)
      a[i, j] = b a.set(i, j, b)

      在 set 函数中

      修改后的元素始终在参数的最后一位


      • 调用运算符 ()

      对应函数名 invoke

      实现后,它看起来就像是在实例化一个对象一样.

      对于 lambda , 这通常被用来调用 lambda

      例如:

      fun example(lambda: String.(Int) -> Unit) {
          // 我们可以通过 () 调用代替 invoke
          lambda("String", 114514) // 看起来和调用函数一样
      }
      

      但当 invoke 处于一个类的伴生对象时,可能会导致 构造函数 与 invoke 函数重合的状况

      在这种情况下 kotlin 会优先考虑是否为构造函数,随后被编译为实例化对象

      class TestOperator{ // 默认空构造参数
          init {
              println("--init--")
          }
          companion object {
              operator fun invoke() { 
                  println("--operator invoke--")
              }
          }
      }
      fun main() {
          TestOperator()
      }
      

      在这段代码中,就会得到输出 --init--

      而当运算符未一致时,如:

      class TestOperator{ // 默认空构造参数
          init {
              println("--init--")
          }
          companion object {
              operator fun invoke(int: Int) {
                  println("--operator invoke $int--")
              }
          }
      }
      fun main() {
          TestOperator(114514)
      }
      

      这时候的输出就是 --operator invoke 114514-- 了,因为它被编译为了调用重载运算符(不符合构造函数)


      • 比较运算符

      此处逻辑与 Java 一致

      函数名为 compareTo 传入一个参数,始终返回一个Int类型的数据

      一般来说,

      • 当此对象小于另一个对象时, compareTo 返回小于0的数据(一般为-1)
      • 当此对象值等于另一个对象时, compareTo 返回 0
      • 当此对象大于另一个对象时, compareTo 返回大于0的数据(一般为1)
      运算符 对应函数名称
      a > b a.compareTo(b) > 0
      a < b a.compareTo(b) < 0
      a >= b a.compareTo(b) >= 0
      a <= b a.compareTo(b) <= 0

      • 委托属性运算符

      在 Kotlin 中存在有委托字段的操作
      例如:

      val finalDelegatedProperty by lazy {  } // `lazy {}` 为委托器实现
      var mutableDelegateProperty by lazy { }
      

      通过 重载运算符 你可以实现委托字段的操作

      • 对于 val 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型

      • 对于 var 委托属性, 有 getValue(thisRef: Any?, property: KProperty<*>):字段类型, 以及 setValue(thisRef: Any?, property: KProperty<*>, value: 字段类型)

      • thisRef 为属性需要的对象, 当为成员属性时为所属对象 反之则为null

      • property 是为当前属性的相关信息,如属性名称等


      *在 Kotlin 中实现切片

      这是一段在rust中的字符串切片实现

      fn slice() {
          let mut string = String::from("Hello World!");
          let slice = &string[0..3]; // 值为: Hell
      }
      

      Rust 中的切片还支持有如..y, .., x.. 的实现 但这些都难以在 kotlin 中实现,故不参与讨论

      在kotlin中,我们有着针对 x..y 的操作符实现 (range-to 操作符)

      所以, 0..3这段代码实则等于 IntRange(0,3)

      我们只需要一个接受 IntRange 的函数即可

      由于在 String 中并没有定义 get(IntRange) 这一函数, 所以在这里我们需要通过扩展函数以实现功能

      operator fun String.get(range: IntRange): String {
          return this.substring(range) // 截取字符串指定范围
      }
      

      这样,我们就实现了字符串切片的内容。同样地,我们还可以实现集合切片:

      operator fun <T> List<T>.get(range: IntRange): List<T> {
          return this.subList(range.first,range.last)
      }
      

      这样,我们就可以在 kotlin 中运行以下代码了

      fun slice() {
          val str = "Hello World!"
          val strSlice = str[0..3] // 值为 Hell
          
          val list = listOf(0,1,2,3,4)
          val listSlice = list[0..3] // 值为 0,1,2,3
      }
      operator fun String.get(range: IntRange): String {
          return this.substring(range) 
      }
      operator fun <T> List<T>.get(range: IntRange): List<T> {
          return this.subList(range.first,range.last)
      }
      

      *可 null 值 的扩展函数

      或许你会有这样的需求

      一个Map中有多个嵌套Map,

      一般来说,Map通过get函数获取对应的值时,会返回一个可null值 V?

      此时我们尝试获取最后一个嵌套 Map 内的值,就会显得十分不优雅

      val example = 
          mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>() 
          // 真的会有人这样写吗?
      fun getFinalValue() {
          val finalValue: Long = 
              example["Key 0"]!!["Key 1"]!!["Key 2"]!!["Key 3"]
          // Or ...    
          val finalValue1: Long = 
              example["Key 0"]?.get("Key 1")?.get("Key 2")?.get("Key 3")
      }
      

      由于 Kotlin 中的运算符仅支持对于 非null值的调用

      所以,

      • 要么,我们需要使用一连串的!!以将值转换为不可空值(无法为null)
      • 要么,我们使用?.调用方法,而一连串的?.get(..)则显得更加不美观

      此时,我们可能使用造轮传统来防止这样的清空发生

      同样地,我们依旧需要使用扩展函数以支持从外界重写操作符

      operator fun <K,V> Map<K,V>?.get(key: K): V {
          return this!![key] // Or more...
      }
      

      在这里,我们通过 Map<K,V>? 使它支持了对于可空值的[]语法支持

      让我们尝试用这段代码代替原本的代码吧!

      operator fun <K,V> Map<K,V>?.get(key: K): V {
          return this!![key]
      }
      
      val example =
          mutableMapOf<String, Map<String, Map<String, Map<String,Long>>>>() 
      
      fun getFinalValue() {
          val finalValue = example["Key 0"]["Key 1"]["Key 2"]["Key 3"]
      }
      

      这样,我们就成功使代码看着更加美观。

      不过,这样写具有一定的坏处———你无法从代码层次直接知晓这个值是否可能为null

      这可能会降低你的代码可读性,请酌情使用.

      以此类推,我们还有

      operator fun <T> List<T>?.get(index: Int) = this!![index]
      

      功能同上,可直接调用 索引运算符

      本地函数 Local Function

      此处的本地函数不指native函数,而是指函数嵌套内的函数

      不建议在实际生产中这么做,它会大大降低你的代码可读性

      总所周知,在Kotlin中,我们可以通过fun关键词声明一个函数

      它看起来会是这样

      fun helloWorld() {
          println("Hello World!")
      }
      

      然而你知道吗,我们还可以往一个函数中套更多的函数

      像这样

      fun helloWorld() {
        fun getHelloWorldMessage() = "Hello World"
        println(getHelloWorldMessage())
      }
      

      我们可以发现,这段函数竟然被成功编译,并且成功运行输出Hello World

      让我们用idea自带的显示字节码,查看相关代码

      // class version 55.0 (55)
      // access flags 0x31
      // 普通函数 helloWorld: Unit
        public final static helloWorld()V
         L0 // 调用内嵌函数 getHelloWorldMessage()
          LINENUMBER 5 L0
          INVOKESTATIC com/pigeonyuze/DeliciousSugarKt.helloWorld$getHelloWorldMessage ()Ljava/lang/String;
          POP
        // More code...
      
      // helloWorld 嵌套本地函数 getHelloWorldMessage: String
        private final static helloWorld$getHelloWorldMessage()Ljava/lang/String;
         L0
          LINENUMBER 4 L0
          LDC "HelloWorld"
          ARETURN
         L1
          MAXSTACK = 1
          MAXLOCALS = 0
          MAXLOCALS = 0
      

      由上可见,我们的代码实则被编译成了两个方法
      helloWorld() 和 helloWorld$getHelloWorldMessage() 方法

      而其中本地函数的标识符无论如何为private
      Note: 你始终无法为嵌套函数设置标识符,你始终只能从本地函数所在函数中调用它
      如:

      fun superFunction() {
          fun childFunction() {}
          childFunction() // 可以通过编译
          private fun childFunction1() {}
          // 无法通过编译: Modifier 'private' is not applicable to 'local function'
          // NOTE: 无法向本地函数添加标识符
      }
      
      fun elseFunction() {
          childFunction()
          // 无法通过编译: Unresolved reference : getHelloWorldMessage
          // NOTE: 无法从外处调用本地函数
      }
      

      或许你可以猜到,你可以无限地嵌套函数

      fun helloWorld() {
       fun helloWorld0() {
         fun helloWorld1() {
           //More shit
         }
       }
      }
      

      这些函数经过编译后,都会被编译为一个单独的 private final函数。

      值得注意的是,kotlin中的inline函数并不支持本地函数的使用。

      kotlin的inline函数会将原函数体内联到调用方处,因此如果内联了本地函数可能会导致意想不到的错误。

      逻辑或|| Logical OR

      我们大家都知道, 我们可以通过逻辑或门||获取一串布尔值中是否包含真值(true)

      false || true // true
      true || true // true
      false || false //false

      它是按照从左到右的顺序来计算的。

      如果你有着乱翻kotlin代码的习惯的话

      你会在kotlin/native关于Throwable获取栈信息的源码里看到以下代码

      // kotlin.Throwable.kt line 77
      private fun Throwable.dumpFullTrace(indent: String, qualifier: String) {
          this.dumpSelfTrace(indent, qualifier) || return
          var cause = this.cause
          while (cause != null) {
              cause.dumpSelfTrace(indent, "Caused by: ")
              cause = cause.cause
          }
      }
      

      一句神奇的代码躲藏在其中

      this.dumpSelfTrace(indent, qualifier) || return
      

      是的没错,这是一个逻辑或门,可是它有着这么一个与众不同的点
      运算符的左侧是布尔值,而右侧却是函数跳出点
      通过反编译为java代码

      if(!this.dumpSelfTrace(indent, qualifier)) {
          return;
      }else {
          Throwable cause = this.cause;
          while (cause != null) {
              cause.dumpSelfTrace(indent, "Caused by: ");
              cause = cause.cause;
          }
      }
      

      Kotlin 编译器将这段||的神奇代码,转变成了if-else。

      当逻辑或运算符左侧不满足时,理应向右继续计算,而此时的右值为return,函数的跳出点。
      那么此时,kotlin该做什么呢?没错,它执行了右侧的语句return代码,这使得程序直接跳出。

      此时,我们甚至可以执行run函数(需要为inline函数,因为这样可以跳出调用处函数),只要它跳出了函数就行

      fun getBoolean(): Boolean { /* More code */ }
      fun call() {
         getBoolean() || run {
             println("Wow, I'm running!")
             throw Throwable()
             // Also, you can just 'return'
         }
         println("Go go go!")
        /* More code... */
      }
      

      调用call()后,如果getBoolean不满足则会运行run内联函数,因为内联函数中最后会跳出函数(抛出错误或return函数),run内联函数中的内容都会运行。
      这相当于

      fun getBoolean(): Boolean { /* More code */ }
      fun call() {
          if(!getBoolean()) {
              println("Wow, I'm running!")
              throw Throwable() //Also, you can just 'return' 
          }
           println("Go go go!")
           /* More code... */
      }  
      

      理论上,你可以不断||下去,只要最右侧跳出即可

      Random.nextBoolean() || (Random.nextBoolean() && Random.nextBoolean()) || /* More Boolean*/ || return
      

      这依托代码你是可以成功编译并运行的,他将会像上文一样编译为一段if-else,就像:

      if (!Random.nextBoolean()) {
        if(!(Random.nextBoolean() && Random.nextBoolean()))  {
          if(!/*More Boolean*/) return
        }
      }
      

      在循环体中,你还可以使用break或continue

      while(true) {
       booleanValue() || continue
       booleanValue() || break
       booleanValue() || return
       booleanValue() || throw Throwable()
      }
      

      这些代码同样也可以编译或运行
      是否在实际项目中使用可根据实际项目复杂度来看
      毕竟过度的||,何尝不是依托呢(笑

      需要 Lambda 的函数

      在讨论如何编写关于 lambda 的函数前,或许我们可以先来了解一下什么是lambda

      在kotlin中,几乎所有的表达式中的{}都会被编译为一个lambda对象

      如果你学习或使用过java,会发现这一点与java亦或是其他语言都有所不同。

      你可以猜想运行以下代码,你会得到哪些输出.

      fun main() {
          { println("Hello world") }
          if (true) { println("I am in an if expression") }
          when {
              true -> { println("I am in an when expression") }
          }
          { println("Invoked lambda by call invoke function") }.invoke();
          {
              println("Invoked lambda by this.()")
          }()
          val lambda = {
              println("Um-mm. why i am in a local variable.")
          }
          lambda() // = lambda.invoke()
          val newLambda = { "Please invoke me." } // UNUSED
          run {
              println("Run!")
          }
      }
      

      运行以上代码,我们得到了以下输出

      I am in an if expression
      I am in an when expression
      Invoked lambda by call invoke function
      Invoked lambda by this.()
      Um-mm. why i am in a local variable
      Run!
      

      等等,是不是少了一句"应有的"输出。

      或许我该告诉你,kotlin的{}就是这么反直觉。

       { println("Hello world") }
      

      这段代码,被解析为了一个 lambda 表达式,而从未调用的 lambda 被编译器所忽略

      或许并没有忽略,在字节码中,这段代码看起来是这样的

      LINENUMBER 2 L0
      GETSTATIC ...
      POP
      

      在字节码层面,它执行了压栈和出栈操作,但仅此而已。

      if (true) { println("I am in an if expression") }
      when {
         true -> { println("I am in an when expression") }
      }
      

      而这两段代码中的{}并没编译为 lambda 表达式,故可以直接运行。

      lambda 表达式并不会解析为一个单独的 函数/方法

      要想调用 lambda 表达式,你应该通过 invoke 函数进行调用

      或者你可以使用 kotlin.run 这一个内联函数

      public inline fun <R> run(block: () -> R): R {
          /* more codes */
          return block()
      }
      

      这是一个需要传入 lambda 的函数

      我们来看看它的参数声明部分

      block: () -> R
      
      • block 为参数名
      • () -> R 为参数类型

      让我们从 () -> R 中谈谈.

      • -> 表明了这是一个 lambda 体
      • -> 后的内容(R)表明了这一个lambda体需要返回的类型
      • -> 前的内容(())表明了这一个lambda体需要提供的类型

      以此类推,我们现在由以下表达式

      String.(Int) -> Any
      

      如果你知道什么是扩展函数,这里会更好理解一些

      fun String.lambda(it: Int): Any
      
      • String 是接收器, 在 lambda 体中使用 this 调用
      • Int 是形参, 在 lambda 体中通常使用 it 调用,也可以使用形参声明规定调用名称, 如`{ int -> /**/ }
      • Any 是返回类型

      你也可以使用泛型,像是这样

      K.(V) -> R
      

      写在函数参数里面,它看起来像是这样:

      fun <K,V,R> invoke(name: K.(V) -> R) {} // 使用 `name` 调用 lambda
      

      或者也可以作为扩展函数,像这样:

      fun <K,V,R> (K.(V) -> R).invoke() {} // 使用 `this` 调用 lambda
      

      你甚至可以使用 suspend 标识这个 lambda 可以为挂起的。例如:

      suspend fun (suspend () -> Any).run() {
          this.invoke()
      }
      suspend fun run(arg: suspend () -> Any) {
          this.invoke()
      }
      

      总结一下,如果你需要将 lambda 作为函数的 形参/扩展函数接收器
      你可以这么写:

      suspend fun <K,V,R> (suspend K.(V) -> R).run0() {}
      suspend fun <K,V,R> (suspend K.(V) -> R).run1(run: suspend K.(V) -> R) {}
      fun <K,V,R> (K.(V) -> R).run2() {}
      // ...
      fun <K,R> (K.() -> R).run3() {}
      fun <R> (() -> R).run4() {}
      fun <R> run5(arg: () -> R) {}
      

      实际生产可参考案例

      suspend fun handleData() {}
      
      fun main() =
          suspend {
              handleData()
          }.runBlock()
      
      fun (suspend () -> Any).runBlock() {
          runBlocking { invoke() }
      }
      
      发布在 技术交流板块
      PigeonYuze
      PigeonYuze
    • RE: YamlBot - 基于yaml配置的指令编写插件

      @彼岸星辰beta 这个是新加的特性,不是bug
      触发条件是argSize为1 argSpilt为""
      你可以看一下这个pr

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      @Niko_Sakura
      这个功能刚刚加了 ()
      Pull request #7

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: YamlBot - 基于yaml配置的指令编写插件

      @彼岸星辰beta mirai.jar是为旧版本的mirai构建的包,里面一同打包了所需要的所有依赖
      而mirai2.jar是新版本mirai的功能,它在被mirai-console加载后会自行下载依赖至对应的lib文件夹内,防止包使用的重复带来了不必要的存储空间占用
      一般来说下载mirai2.jar就可以了
      真的会有人现在还在用mirai 1.x 吗?

      发布在 插件发布
      PigeonYuze
      PigeonYuze
    • RE: GroupBotSuffix - 为你的bot群名片增加有趣的后缀

      @子君9297 @幻空zzz
      已增加了单独设置特定群聊的功能 (

      发布在 插件发布
      PigeonYuze
      PigeonYuze