Skip to content

Commit 4e687ac

Browse files
qwwdfsadansman
authored andcommitted
Pr/2230 (Kotlin#2287)
* Allow nullable types in Flow.firstOrNull * Allow nullable types in Flow.singleOrNull * Align Flow.single and Flow.singleOrNull with Kotlin standard library Fixes Kotlin#2229 Fixes Kotlin#2289 Co-authored-by: Nicklas Ansman Giertz <[email protected]>
1 parent d380be2 commit 4e687ac

File tree

4 files changed

+54
-20
lines changed

4 files changed

+54
-20
lines changed

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

+22-15
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,39 @@ 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
*/
70-
public suspend fun <T: Any> Flow<T>.singleOrNull(): T? {
71-
var result: T? = null
72-
collect { value ->
73-
if (result != null) error("Expected only one element")
74-
result = value
70+
public suspend fun <T> Flow<T>.singleOrNull(): T? {
71+
var result: Any? = NULL
72+
collectWhile {
73+
// No values yet, update result
74+
if (result === NULL) {
75+
result = it
76+
true
77+
} else {
78+
// Second value, reset result and bail out
79+
result = NULL
80+
false
81+
}
7582
}
76-
return result
83+
return if (result === NULL) null else result as T
7784
}
7885

7986
/**
@@ -112,7 +119,7 @@ public suspend fun <T> Flow<T>.first(predicate: suspend (T) -> Boolean): T {
112119
* The terminal operator that returns the first element emitted by the flow and then cancels flow's collection.
113120
* Returns `null` if the flow was empty.
114121
*/
115-
public suspend fun <T : Any> Flow<T>.firstOrNull(): T? {
122+
public suspend fun <T> Flow<T>.firstOrNull(): T? {
116123
var result: T? = null
117124
collectWhile {
118125
result = it
@@ -122,10 +129,10 @@ public suspend fun <T : Any> Flow<T>.firstOrNull(): T? {
122129
}
123130

124131
/**
125-
* The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection.
132+
* The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection.
126133
* Returns `null` if the flow did not contain an element matching the [predicate].
127134
*/
128-
public suspend fun <T : Any> Flow<T>.firstOrNull(predicate: suspend (T) -> Boolean): T? {
135+
public suspend fun <T> Flow<T>.firstOrNull(predicate: suspend (T) -> Boolean): T? {
129136
var result: T? = null
130137
collectWhile {
131138
if (predicate(it)) {

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/FirstTest.kt

+6
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ class FirstTest : TestBase() {
128128
assertNull(emptyFlow<Int>().firstOrNull { true })
129129
}
130130

131+
@Test
132+
fun testFirstOrNullWithNullElement() = runTest {
133+
assertNull(flowOf<String?>(null).firstOrNull())
134+
assertNull(flowOf<String?>(null).firstOrNull { true })
135+
}
136+
131137
@Test
132138
fun testFirstOrNullWhenErrorCancelsUpstream() = runTest {
133139
val latch = Channel<Unit>()

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

+24-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package kotlinx.coroutines.flow
77
import kotlinx.coroutines.*
88
import kotlin.test.*
99

10-
class SingleTest : TestBase() {
10+
class SingleTest : TestBase() {
1111

1212
@Test
1313
fun testSingle() = runTest {
@@ -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
@@ -61,6 +61,10 @@ class SingleTest : TestBase() {
6161
assertEquals(1, flowOf<Int?>(1).single())
6262
assertNull(flowOf<Int?>(null).single())
6363
assertFailsWith<NoSuchElementException> { flowOf<Int?>().single() }
64+
65+
assertEquals(1, flowOf<Int?>(1).singleOrNull())
66+
assertNull(flowOf<Int?>(null).singleOrNull())
67+
assertNull(flowOf<Int?>().singleOrNull())
6468
}
6569

6670
@Test
@@ -69,5 +73,22 @@ class SingleTest : TestBase() {
6973
val flow = flowOf(instance)
7074
assertSame(instance, flow.single())
7175
assertSame(instance, flow.singleOrNull())
76+
77+
val flow2 = flow {
78+
emit(BadClass())
79+
emit(BadClass())
80+
}
81+
assertFailsWith<IllegalArgumentException> { flow2.single() }
82+
}
83+
84+
@Test
85+
fun testSingleNoWait() = runTest {
86+
val flow = flow {
87+
emit(1)
88+
emit(2)
89+
awaitCancellation()
90+
}
91+
92+
assertNull(flow.singleOrNull())
7293
}
7394
}

0 commit comments

Comments
 (0)