From f5b2dbc65d029a3723847ff7028049db1f689ca2 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 16 Aug 2021 14:40:57 +0800 Subject: [PATCH] Avoid boxing in `TypeSafeMap` and support serialization --- mirai-core-utils/build.gradle.kts | 6 ++ .../src/commonMain/kotlin/TypeSafeMap.kt | 34 +++++--- .../net/mamoe/mirai/utils/TypeSafeMapTest.kt | 79 +++++++++++++++++++ 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index 37caa8d6d..e0464ee55 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -67,6 +67,12 @@ kotlin { } } + val commonTest by getting { + dependencies { + api(yamlkt) + } + } + if (isAndroidSDKAvailable) { val androidMain by getting { // diff --git a/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt b/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt index 2eec25ba6..fe1cb0177 100644 --- a/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt +++ b/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt @@ -11,24 +11,30 @@ package net.mamoe.mirai.utils +import kotlinx.serialization.Serializable import java.util.concurrent.ConcurrentHashMap import kotlin.contracts.InvocationKind import kotlin.contracts.contract +@Serializable @JvmInline -public value class TypeKey(public val name: String) { +public value class TypeKey(public val name: String) { override fun toString(): String = "Key($name)" - public inline infix fun to(value: T): TypeSafeMap = buildTypeSafeMap { set(this@TypeKey, value) } + public inline infix fun to(value: T): TypeSafeMap = buildTypeSafeMap { set(this@TypeKey, value) } } -public interface TypeSafeMap { +/** + * @see buildTypeSafeMap + */ +public sealed interface TypeSafeMap { public val size: Int public operator fun get(key: TypeKey): T public operator fun contains(key: TypeKey): Boolean = get(key) != null - public fun toMap(): Map, Any?> + public fun toMapBoxed(): Map, Any?> + public fun toMap(): Map public companion object { public val EMPTY: TypeSafeMap = TypeSafeMapImpl(emptyMap()) @@ -46,15 +52,16 @@ public operator fun TypeSafeMap.plus(other: TypeSafeMap): TypeSafeMap { } } -public interface MutableTypeSafeMap : TypeSafeMap { +public sealed interface MutableTypeSafeMap : TypeSafeMap { public operator fun set(key: TypeKey, value: T) public fun remove(key: TypeKey): T? public fun setAll(other: TypeSafeMap) } +@PublishedApi internal open class TypeSafeMapImpl( - internal open val map: Map, Any?> = ConcurrentHashMap() + @PublishedApi internal open val map: Map = ConcurrentHashMap() ) : TypeSafeMap { override val size: Int get() = map.size @@ -67,16 +74,17 @@ internal open class TypeSafeMapImpl( } override operator fun get(key: TypeKey): T = - map[key]?.uncheckedCast() ?: throw NoSuchElementException(key.toString()) + map[key.name]?.uncheckedCast() ?: throw NoSuchElementException(key.toString()) override operator fun contains(key: TypeKey): Boolean = get(key) != null - override fun toMap(): Map, Any?> = map + override fun toMapBoxed(): Map, Any?> = map.mapKeys { TypeKey(it.key) } + override fun toMap(): Map = map } @PublishedApi internal class MutableTypeSafeMapImpl( - override val map: MutableMap, Any?> = ConcurrentHashMap() + @PublishedApi override val map: MutableMap = ConcurrentHashMap() ) : TypeSafeMap, MutableTypeSafeMap, TypeSafeMapImpl(map) { override fun equals(other: Any?): Boolean { return other is MutableTypeSafeMapImpl && other.map == this.map @@ -87,7 +95,7 @@ internal class MutableTypeSafeMapImpl( } override operator fun set(key: TypeKey, value: T) { - map[key] = value + map[key.name] = value } override fun setAll(other: TypeSafeMap) { @@ -98,9 +106,13 @@ internal class MutableTypeSafeMapImpl( } } - override fun remove(key: TypeKey): T? = map.remove(key)?.uncheckedCast() + override fun remove(key: TypeKey): T? = map.remove(key.name)?.uncheckedCast() } +public inline fun MutableTypeSafeMap(): MutableTypeSafeMap = MutableTypeSafeMapImpl() +public inline fun MutableTypeSafeMap(map: Map): MutableTypeSafeMap = + MutableTypeSafeMapImpl().also { it.map.putAll(map) } + public inline fun buildTypeSafeMap(block: MutableTypeSafeMap.() -> Unit): MutableTypeSafeMap { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return MutableTypeSafeMapImpl().apply(block) diff --git a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt new file mode 100644 index 000000000..e10e31394 --- /dev/null +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +import net.mamoe.yamlkt.Yaml +import net.mamoe.yamlkt.YamlBuilder +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class TypeSafeMapTest { + + private val myKey = TypeKey("test") + private val myKey2 = TypeKey("test2") + + @Test + fun `can set get`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + assertEquals("str", map[myKey]) + assertEquals("str2", map[myKey2]) + } + + @Test + fun `key is inlined`() { + val map = MutableTypeSafeMap() + map[TypeKey("test")] = "str" + map[TypeKey("test")] = "str2" + assertEquals(1, map.size) + assertEquals("str2", map[TypeKey("test")]) + } + + @Test + fun `can toMap`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + + val map1 = map.toMapBoxed() + + assertEquals(2, map1.size) + assertEquals("str", map1[myKey]) + assertEquals("str2", map1[myKey2]) + } + + @Test + fun `test serialization`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + + val map1 = map.toMap() + + // Json does not support reflective serialization, so we use Yaml in JSON format + val yaml = Yaml { + classSerialization = YamlBuilder.MapSerialization.FLOW_MAP + mapSerialization = YamlBuilder.MapSerialization.FLOW_MAP + listSerialization = YamlBuilder.ListSerialization.FLOW_SEQUENCE + stringSerialization = YamlBuilder.StringSerialization.DOUBLE_QUOTATION + encodeDefaultValues = true + } + + val string = yaml.encodeToString(map1) + println(string) // { "test2": "str2" ,"test": "str" } + + val result = MutableTypeSafeMap(Yaml.decodeMapFromString(string).cast()) + assertEquals(map, result) + } +} \ No newline at end of file