我喜欢使用Sequences 来生成无休止的值流。
在这种情况下,我们必须编写自定义代码,因为检查重复值是一个有状态的操作,虽然 Sequences 有一个 distinct() 状态过滤器,但它适用于所有生成的值 - 我们只希望它适用于一个有限的窗口。
TL;DR:
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
测试
让我们先编写一个测试,以确保我们的解决方案有效。 Kotest 有一个特定的property based testing 子项目,这将使我们能够非常快速地涵盖广泛的测试用例。
于是,我跑遍了the setup,开始设置测试用例。
播种RandomImageLogic
首先我修改了RandomImageLogic 类,以便可以使用提供的Random 为随机选择播种。
import kotlin.random.Random
class RandomImageLogic(private val random: Random) {
fun retrieve(): Int {
return (1..9).random(random = random)
}
}
这将帮助我们为RandomImageLogic 创建一个Generator。
测试所有值
现在我们可以使用 Kotest 编写一个基于属性的测试,该测试将断言“对于所有顺序值,它们都是不同的”
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.arbitrary.arbitrary
import io.kotest.property.forAll
class RandomImageLogicTest: FunSpec({
// This generator will create a new instance of `RandomImageLogic`
// and generate two sequential values.
val sequentialValuesArb = arbitrary { rs ->
val randomImageLogic = RandomImageLogic(rs.random)
val firstValue = randomImageLogic.retrieve()
val secondValue = randomImageLogic.retrieve()
firstValue to secondValue
}
test("expect sequential values are different") {
forAll(sequentialValuesArb) { (firstValue, secondValue) ->
firstValue != secondValue
}
}
})
当然,测试失败了。
Property failed after 4 attempts
Arg 0: (1, 1)
Repeat this test by using seed 1210584330919845105
Caused by org.opentest4j.AssertionFailedError: expected:<true> but was:<false>
所以让我们修复它吧!
生成序列
正如我之前所说,我真的很喜欢 Sequences。它们非常适合这个用例,因为我们有无限的价值来源。
为了演示如何制作序列,让我们转换现有代码,并使用Iterator 来获取值。
class RandomImageLogic(private val random: Random) {
private val randomInts =
// generate a sequence using values from this lambda
generateSequence { (1..9).random(random = random) }
// use an iterator to fetch values
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
这还没有解决问题 - 序列一次只生成并提供一个值,因此无法进行任何过滤。幸运的是generateSequence() 有一个带有nextFunction: (T) -> T? 的变体,我们可以根据之前的值确定下一个值。
如果我们使用此构造函数,并进行一些重构以共享源值,并使用 util 方法生成下一个值同时过滤掉以前的值...
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({ nextValue() }) { previousValue ->
nextValue(previousValue)
}
.iterator()
现在如果我们运行测试,它就通过了!
Test Duration Result
expect sequential values are different 0.077s passed
改进:两个以上不同的顺序值
如果您不只是希望两个连续的值是不同的,而是 3,会发生什么?甚至更多?让我们将“无重复值”限制配置为可配置,我认为这将说明为什么序列是一个很好的解决方案。
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
// ...
}
测试
再一次,让我们编写一个测试以确保一切按预期工作。
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.collections.shouldNotContainDuplicates
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
import kotlin.random.Random
class RandomImageLogicTest : FunSpec({
test("expect arbitrary sequential values are different") {
checkAll(Arb.int(), Arb.int(1..10)) { seed, noRepeatsLimit->
val randomImageLogic = RandomImageLogic(Random(seed), noRepeatsLimit)
val result = List(noRepeatsLimit) { randomImageLogic.retrieve() }
withClue("Result: $result") {
result shouldHaveSize noRepeatsLimit
result.shouldNotContainDuplicates()
}
}
}
})
当然测试失败了。
Property test failed for inputs
0) -459964888
1) 5
Caused by java.lang.AssertionError: Result: [3, 8, 0, 2, 8]
Collection should not contain duplicates
有很多选项可以使序列有状态 - 让我们再看一个。
值序列
我们可以有一个序列,而不是单个值的序列,其中每个元素不仅是当前值的列表,而且是以前看到的值的列表。
让我们使用ArrayDeque 来存储这些值,因为从开头和结尾添加和删除值很容易。
同样,我们使用相同的 generateSequence 构造函数和 seedFunction 和 nextFunction - 除了这次每个元素都是存储所有值的双端队列,并且在 nextFunction 中,我们将新值添加到双端队列的开头, 如果大于窗口大小则修剪它noRepeatsLimit
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
是的,测试通过了!
Test Duration Result
expect arbitrary sequential values are different 0.210s passed
存储状态
考虑如何存储状态很重要。 RandomImageLogic 需要“状态”来了解生成的值是否不同。
在序列实现中,它存储在内部,因此专门与RandomImageLogic 的实例相关联。也许您的应用程序在任何时候都只有一个 RandomImageLogic 实例,在这种情况下,状态将始终是最新的,并且将在所有调用之间共享。
但是如果RandomImageLogic 的实例不止一个,会发生什么?或者如果有多线程?或者如果RandomImageLogic 实例被重新创建?
这些问题的答案取决于实施和情况。根据您的问题,我怀疑图像从不重复并不重要,但我提出这一点是因为考虑这一点很重要。