Avoid boxing in TypeSafeMap and support serialization

This commit is contained in:
Him188 2021-08-16 14:40:57 +08:00
parent 2696f0b95e
commit f5b2dbc65d
3 changed files with 108 additions and 11 deletions

View File

@ -67,6 +67,12 @@ kotlin {
}
}
val commonTest by getting {
dependencies {
api(yamlkt)
}
}
if (isAndroidSDKAvailable) {
val androidMain by getting {
//

View File

@ -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<out T>(public val name: String) {
public value class TypeKey<T>(public val name: String) {
override fun toString(): String = "Key($name)"
public inline infix fun <T> 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 <T> get(key: TypeKey<T>): T
public operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null
public fun toMap(): Map<TypeKey<*>, Any?>
public fun toMapBoxed(): Map<TypeKey<*>, Any?>
public fun toMap(): Map<String, Any?>
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 <T> set(key: TypeKey<T>, value: T)
public fun <T> remove(key: TypeKey<T>): T?
public fun setAll(other: TypeSafeMap)
}
@PublishedApi
internal open class TypeSafeMapImpl(
internal open val map: Map<TypeKey<*>, Any?> = ConcurrentHashMap()
@PublishedApi internal open val map: Map<String, Any?> = ConcurrentHashMap()
) : TypeSafeMap {
override val size: Int get() = map.size
@ -67,16 +74,17 @@ internal open class TypeSafeMapImpl(
}
override operator fun <T> get(key: TypeKey<T>): T =
map[key]?.uncheckedCast() ?: throw NoSuchElementException(key.toString())
map[key.name]?.uncheckedCast() ?: throw NoSuchElementException(key.toString())
override operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null
override fun toMap(): Map<TypeKey<*>, Any?> = map
override fun toMapBoxed(): Map<TypeKey<*>, Any?> = map.mapKeys { TypeKey<Any?>(it.key) }
override fun toMap(): Map<String, Any?> = map
}
@PublishedApi
internal class MutableTypeSafeMapImpl(
override val map: MutableMap<TypeKey<*>, Any?> = ConcurrentHashMap()
@PublishedApi override val map: MutableMap<String, Any?> = 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 <T> set(key: TypeKey<T>, value: T) {
map[key] = value
map[key.name] = value
}
override fun setAll(other: TypeSafeMap) {
@ -98,9 +106,13 @@ internal class MutableTypeSafeMapImpl(
}
}
override fun <T> remove(key: TypeKey<T>): T? = map.remove(key)?.uncheckedCast()
override fun <T> remove(key: TypeKey<T>): T? = map.remove(key.name)?.uncheckedCast()
}
public inline fun MutableTypeSafeMap(): MutableTypeSafeMap = MutableTypeSafeMapImpl()
public inline fun MutableTypeSafeMap(map: Map<String, Any?>): 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)

View File

@ -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<String>("test")
private val myKey2 = TypeKey<CharSequence>("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<String>("test")] = "str"
map[TypeKey<String>("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)
}
}