Skip to content

Commit f681902

Browse files
committed
Align Flow.single and Flow.singleOrNull with Kotlin standard library
Fixes #2289
1 parent 7d86342 commit f681902

File tree

3 files changed

+25
-14
lines changed

3 files changed

+25
-14
lines changed

kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt

+21-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package kotlinx.coroutines.flow
1010

1111
import kotlinx.coroutines.flow.internal.*
12+
import kotlinx.coroutines.internal.Symbol
1213
import kotlin.jvm.*
1314

1415
/**
@@ -47,33 +48,43 @@ public suspend inline fun <T, R> Flow<T>.fold(
4748
}
4849

4950
/**
50-
* The terminal operator, that awaits for one and only one value to be published.
51+
* The terminal operator that awaits for one and only one value to be emitted.
5152
* Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow
5253
* that contains more than one element.
5354
*/
5455
public suspend fun <T> Flow<T>.single(): T {
5556
var result: Any? = NULL
5657
collect { value ->
57-
if (result !== NULL) error("Expected only one element")
58+
require(result == NULL) { "Flow has more than one element" }
5859
result = value
5960
}
6061

61-
if (result === NULL) throw NoSuchElementException("Expected at least one element")
62-
@Suppress("UNCHECKED_CAST")
62+
if (result === NULL) throw NoSuchElementException("Flow is empty")
6363
return result as T
6464
}
6565

6666
/**
67-
* The terminal operator, that awaits for one and only one value to be published.
68-
* Throws [IllegalStateException] for flow that contains more than one element.
67+
* The terminal operator that awaits for one and only one value to be emitted.
68+
* Returns the single value or `null`, if the flow was empty or emitted more than one value.
6969
*/
7070
public suspend fun <T> Flow<T>.singleOrNull(): T? {
71-
var result: T? = null
71+
var result: Any? = NULL
7272
collect { value ->
73-
if (result != null) error("Expected only one element")
74-
result = value
73+
/*
74+
* result === NULL -> first value
75+
* result === user value -> we already had first value and second one arrived
76+
* result === DONE -> we've seen more than one value, time to return it
77+
* as well.
78+
*/
79+
result = if (result === NULL) {
80+
value
81+
} else {
82+
// Indicator that more than one value was observed
83+
DONE
84+
}
7585
}
76-
return result
86+
// Symbols are never leaked, so it's one comparison versus two
87+
return if (result is Symbol) null else result as T
7788
}
7889

7990
/**

kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ class OnCompletionTest : TestBase() {
231231

232232
@Test
233233
fun testSingle() = runTest {
234-
assertFailsWith<IllegalStateException> {
234+
assertFailsWith<IllegalArgumentException> {
235235
flowOf(239).onCompletion {
236236
assertNull(it)
237237
expect(1)
@@ -240,7 +240,7 @@ class OnCompletionTest : TestBase() {
240240
expectUnreached()
241241
} catch (e: Throwable) {
242242
// Second emit -- failure
243-
assertTrue { e is IllegalStateException }
243+
assertTrue { e is IllegalArgumentException }
244244
throw e
245245
}
246246
}.single()

kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class SingleTest : TestBase() {
2525
emit(239L)
2626
emit(240L)
2727
}
28-
assertFailsWith<RuntimeException> { flow.single() }
29-
assertFailsWith<RuntimeException> { flow.singleOrNull() }
28+
assertFailsWith<IllegalArgumentException> { flow.single() }
29+
assertNull(flow.singleOrNull())
3030
}
3131

3232
@Test

0 commit comments

Comments
 (0)