diff --git a/build.gradle.kts b/build.gradle.kts index bf4d22d..0c252ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { testImplementation(group = "org.junit.jupiter", name = "junit-jupiter", version = "5.6.0") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } + // https://mvnrepository.com/artifact/io.mockk/mockk + testImplementation("io.mockk:mockk:1.9.3") } tasks { diff --git a/src/main/java/com/mapk/core/BucketGenerator.java b/src/main/java/com/mapk/core/BucketGenerator.java new file mode 100644 index 0000000..b74eda7 --- /dev/null +++ b/src/main/java/com/mapk/core/BucketGenerator.java @@ -0,0 +1,50 @@ +package com.mapk.core; + +import kotlin.Pair; +import kotlin.reflect.KParameter; + +import java.util.ArrayList; +import java.util.List; + +class BucketGenerator { + private final int initializationStatus; + private final List initializeMask; + private final int completionValue; + + private final KParameter[] keyArray; + private final Object[] valueArray; + + BucketGenerator(int capacity, Pair instancePair) { + keyArray = new KParameter[capacity]; + valueArray = new Object[capacity]; + + if (instancePair != null) { + initializationStatus = 1; + + keyArray[0] = instancePair.getFirst(); + valueArray[0] = instancePair.getSecond(); + } else { + initializationStatus = 0; + } + + initializeMask = new ArrayList<>(capacity); + int completionValue = 0; + + for (int i = 0, mask = 1; i < capacity; i++, mask <<= 1) { + initializeMask.add(i, mask); + completionValue |= mask; + } + + this.completionValue = completionValue; + } + + ArgumentBucket generate() { + return new ArgumentBucket( + keyArray.clone(), + valueArray.clone(), + initializationStatus, + initializeMask, + completionValue + ); + } +} diff --git a/src/main/kotlin/com/mapk/core/ArgumentBucket.kt b/src/main/kotlin/com/mapk/core/ArgumentBucket.kt index a08c25a..d148cff 100644 --- a/src/main/kotlin/com/mapk/core/ArgumentBucket.kt +++ b/src/main/kotlin/com/mapk/core/ArgumentBucket.kt @@ -2,47 +2,60 @@ package com.mapk.core import kotlin.reflect.KParameter -internal class BucketGenerator( - private val bucket: Array, - private val instancePair: Pair?, - private val initializationStatus: Int, - private val initializeMask: List -) { - private val completionValue: Int = initializeMask.reduce { l, r -> l or r } - private val bucketSize = bucket.size - - fun generate(): ArgumentBucket { - val tempMap = HashMap(bucketSize, 1.0f) - instancePair?.run { tempMap[this.first] = this.second } - - return ArgumentBucket( - bucket.copyOf(), - tempMap, - initializationStatus, - initializeMask, - completionValue - ) - } -} - class ArgumentBucket internal constructor( - internal val bucket: Array, - internal val bucketMap: MutableMap, + private val keyArray: Array, + internal val valueArray: Array, private var initializationStatus: Int, private val initializeMask: List, private val completionValue: Int -) { +) : Map { + // インスタンス有りなら1、そうでなければ0スタート + private var count: Int = initializationStatus + val isInitialized: Boolean get() = initializationStatus == completionValue - fun setArgument(kParameter: KParameter, argument: Any?) { - val index = kParameter.index + class MutableEntry internal constructor( + override val key: KParameter, + override var value: Any? + ) : Map.Entry + + override val size: Int get() = count + + override fun containsKey(key: KParameter): Boolean { + // NOTE: もしかしたらステータスを見た方が速いかも + return keyArray[key.index] != null + } + + override fun containsValue(value: Any?): Boolean { + throw UnsupportedOperationException() + } + + override fun get(key: KParameter): Any? = valueArray[key.index] + fun getByIndex(key: Int): Any? = + if (initializationStatus and initializeMask[key] != 0) valueArray[key] + else throw IllegalStateException("This argument is not initialized.") + + override fun isEmpty(): Boolean = count == 0 + + override val entries: Set> + get() = keyArray.mapNotNull { it?.let { MutableEntry(it, valueArray[it.index]) } }.toSet() + override val keys: MutableSet + get() = keyArray.filterNotNull().toMutableSet() + override val values: MutableCollection + get() = throw UnsupportedOperationException() + + fun putIfAbsent(key: KParameter, value: Any?) { + val index = key.index val temp = initializationStatus or initializeMask[index] // 先に入ったものを優先するため、初期化済みなら何もしない if (initializationStatus == temp) return - bucketMap[kParameter] = argument - bucket[index] = argument + count += 1 initializationStatus = temp + keyArray[index] = key + valueArray[index] = value + + return } } diff --git a/src/main/kotlin/com/mapk/core/KFunctionForCall.kt b/src/main/kotlin/com/mapk/core/KFunctionForCall.kt index 47cc3e4..33b8a3f 100644 --- a/src/main/kotlin/com/mapk/core/KFunctionForCall.kt +++ b/src/main/kotlin/com/mapk/core/KFunctionForCall.kt @@ -15,23 +15,16 @@ class KFunctionForCall(private val function: KFunction, instance: Any? = n // この関数には確実にアクセスするためアクセシビリティ書き換え function.isAccessible = true - // 初期化処理の共通化のため先に初期化 - val tempArray = Array(parameters.size) { null } - val maskList = generateSequence(1) { it.shl(1) }.take(parameters.size).toList() - generator = if (instance != null) { - tempArray[0] = instance - - // 引数の1番目は初期化済みということでinitializationStatusは1スタート - BucketGenerator(tempArray, parameters.first { it.kind == KParameter.Kind.INSTANCE } to instance, 1, maskList) + BucketGenerator(parameters.size, parameters.first { it.kind == KParameter.Kind.INSTANCE } to instance) } else { - BucketGenerator(tempArray, null, 0, maskList) + BucketGenerator(parameters.size, null) } } fun getArgumentBucket(): ArgumentBucket = generator.generate() fun call(argumentBucket: ArgumentBucket): T = - if (argumentBucket.isInitialized) function.call(*argumentBucket.bucket) - else function.callBy(argumentBucket.bucketMap) + if (argumentBucket.isInitialized) function.call(*argumentBucket.valueArray) + else function.callBy(argumentBucket) } diff --git a/src/test/kotlin/com/mapk/core/ArgumentBucketTest.kt b/src/test/kotlin/com/mapk/core/ArgumentBucketTest.kt index 02561c2..c9f820c 100644 --- a/src/test/kotlin/com/mapk/core/ArgumentBucketTest.kt +++ b/src/test/kotlin/com/mapk/core/ArgumentBucketTest.kt @@ -36,7 +36,7 @@ class ArgumentBucketTest { @DisplayName("初期化後") fun isInitialized() { ::sampleFunction.parameters.forEach { - argumentBucket.setArgument(it, object {}) + argumentBucket.putIfAbsent(it, object {}) } assertTrue(argumentBucket.isInitialized) @@ -50,8 +50,8 @@ class ArgumentBucketTest { @DisplayName("正常に追加した場合") fun setNewArgument() { val parameter = ::sampleFunction.parameters.first { it.index == 0 } - argumentBucket.setArgument(parameter, "argument") - assertEquals("argument", argumentBucket.bucket[0]) + argumentBucket.putIfAbsent(parameter, "argument") + assertEquals("argument", argumentBucket.getByIndex(0)) } @Test @@ -59,9 +59,9 @@ class ArgumentBucketTest { fun setArgumentTwice() { val parameter = ::sampleFunction.parameters.first { it.index == 0 } - argumentBucket.setArgument(parameter, "first") - argumentBucket.setArgument(parameter, "second") - assertEquals("first", argumentBucket.bucket[0]) + argumentBucket.putIfAbsent(parameter, "first") + argumentBucket.putIfAbsent(parameter, "second") + assertEquals("first", argumentBucket.getByIndex(0)) } } } diff --git a/src/test/kotlin/com/mapk/core/KFunctionForCallTest.kt b/src/test/kotlin/com/mapk/core/KFunctionForCallTest.kt index 99d2691..daf33ea 100644 --- a/src/test/kotlin/com/mapk/core/KFunctionForCallTest.kt +++ b/src/test/kotlin/com/mapk/core/KFunctionForCallTest.kt @@ -1,5 +1,7 @@ package com.mapk.core +import io.mockk.spyk +import io.mockk.verify import kotlin.reflect.full.functions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName @@ -42,15 +44,17 @@ class KFunctionForCallTest { @Test @DisplayName("コンパニオンオブジェクトから取得した場合") fun fromCompanionObject() { - val function = - Companion::class.functions.find { it.name == (KFunctionForCallTest)::declaredOnCompanionObject.name }!! + val function = Companion::class.functions + .first { it.name == (KFunctionForCallTest)::declaredOnCompanionObject.name } + .let { spyk(it) } val kFunctionForCall = KFunctionForCall(function, Companion) val bucket = kFunctionForCall.getArgumentBucket() - kFunctionForCall.parameters.forEach { bucket.setArgument(it, it.index) } + kFunctionForCall.parameters.forEach { bucket.putIfAbsent(it, it.index) } val result = kFunctionForCall.call(bucket) assertEquals("12", result) + verify(exactly = 1) { function.call(*anyVararg()) } } private fun func(key: String, value: String = "default"): Pair = key to value @@ -58,13 +62,37 @@ class KFunctionForCallTest { @Test @DisplayName("デフォルト値を用いる場合") fun useDefaultValue() { - val kFunctionForCall = KFunctionForCall(::func) + val func = spyk(::func) + val kFunctionForCall = KFunctionForCall(func) val argumentBucket = kFunctionForCall.getArgumentBucket() - ::func.parameters.forEach { if (!it.isOptional) argumentBucket.setArgument(it, it.name) } + func.parameters.forEach { if (!it.isOptional) argumentBucket.putIfAbsent(it, it.name) } val result = kFunctionForCall.call(argumentBucket) assertEquals("key" to "default", result) + verify(exactly = 1) { func.callBy(any()) } + } + + @Test + @DisplayName("同一関数を違うソースから複数回呼んだ場合") + fun multipleCall() { + val function = Companion::class.functions + .first { it.name == (KFunctionForCallTest)::declaredOnCompanionObject.name } + .let { spyk(it) } + + val kFunctionForCall = KFunctionForCall(function, Companion) + + val bucket1 = kFunctionForCall.getArgumentBucket() + kFunctionForCall.parameters.forEach { bucket1.putIfAbsent(it, it.index) } + val result1 = kFunctionForCall.call(bucket1) + assertEquals("12", result1) + + val bucket2 = kFunctionForCall.getArgumentBucket() + kFunctionForCall.parameters.forEach { bucket2.putIfAbsent(it, it.index + 1) } + val result2 = kFunctionForCall.call(bucket2) + assertEquals("23", result2) + + verify(exactly = 2) { function.call(*anyVararg()) } } }