Skip to content

Commit 218dc97

Browse files
authored
Make ChannelIterator.next non-suspending
* Mark ReceiveChannel.map with @ObsoleteCoroutinesApi Fixes #1162
1 parent 7af7ede commit 218dc97

File tree

5 files changed

+47
-21
lines changed

5 files changed

+47
-21
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,12 @@ public final class kotlinx/coroutines/channels/Channel$Factory {
561561

562562
public abstract interface class kotlinx/coroutines/channels/ChannelIterator {
563563
public abstract fun hasNext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
564-
public abstract fun next (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
564+
public abstract fun next ()Ljava/lang/Object;
565+
public abstract synthetic fun next (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
566+
}
567+
568+
public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls {
569+
public static synthetic fun next (Lkotlinx/coroutines/channels/ChannelIterator;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
565570
}
566571

567572
public final class kotlinx/coroutines/channels/ChannelKt {

kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -901,15 +901,15 @@ internal abstract class AbstractChannel<E> : AbstractSendChannel<E>(), Channel<E
901901
}
902902

903903
@Suppress("UNCHECKED_CAST")
904-
override suspend fun next(): E {
904+
override fun next(): E {
905905
val result = this.result
906906
if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
907907
if (result !== POLL_FAILED) {
908908
this.result = POLL_FAILED
909909
return result as E
910910
}
911-
// rare case when hasNext was not invoked yet -- just delegate to receive (leave state as is)
912-
return channel.receive()
911+
912+
throw IllegalStateException("'hasNext' should be called prior to 'next' invocation")
913913
}
914914
}
915915

kotlinx-coroutines-core/common/src/channels/Channel.kt

+25-16
Original file line numberDiff line numberDiff line change
@@ -295,25 +295,34 @@ public interface ChannelIterator<out E> {
295295
*/
296296
public suspend operator fun hasNext(): Boolean
297297

298+
@Deprecated(message = "Since 1.3.0, binary compatibility with versions <= 1.2.x", level = DeprecationLevel.HIDDEN)
299+
@Suppress("INAPPLICABLE_JVM_NAME")
300+
@JvmName("next")
301+
public suspend fun next0(): E {
302+
/*
303+
* Before 1.3.0 the "next()" could have been used without invoking "hasNext" first and there were code samples
304+
* demonstrating this behavior, so we preserve this logic for full binary backwards compatibility with previously
305+
* compiled code.
306+
*/
307+
if (!hasNext()) throw ClosedReceiveChannelException(DEFAULT_CLOSE_MESSAGE)
308+
return next()
309+
}
310+
298311
/**
299-
* Retrieves and removes the element from this channel suspending the caller while this channel
300-
* is empty or throws [ClosedReceiveChannelException] if the channel
301-
* [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause.
302-
* It throws the original [close][SendChannel.close] cause exception if the channel has _failed_.
303-
*
304-
* This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this
305-
* function is suspended, this function immediately resumes with [CancellationException].
306-
*
307-
* *Cancellation of suspended receive is atomic* -- when this function
308-
* throws [CancellationException] it means that the element was not retrieved from this channel.
309-
* As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may
310-
* continue to execute even after it was cancelled from the same thread in the case when this receive operation
311-
* was already resumed and the continuation was posted for execution to the thread's queue.
312+
* Retrieves the element from the current iterator previously removed from the channel by preceding call to [hasNext] or
313+
* throws [IllegalStateException] if [hasNext] was not invoked.
314+
* [next] should only be used in pair with [hasNext]:
315+
* ```
316+
* while (iterator.hasNext()) {
317+
* val element = iterator.next()
318+
* // ... handle element ...
319+
* }
320+
* ```
312321
*
313-
* Note that this function does not check for cancellation when it is not suspended.
314-
* Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed.
322+
* This method throws [ClosedReceiveChannelException] if the channel [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause.
323+
* It throws the original [close][SendChannel.close] cause exception if the channel has _failed_.
315324
*/
316-
public suspend operator fun next(): E
325+
public operator fun next(): E
317326
}
318327

319328
/**

kotlinx-coroutines-core/common/src/channels/Channels.common.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ public suspend inline fun <E, K, V, M : MutableMap<in K, MutableList<V>>> Receiv
11961196
* The operation is _intermediate_ and _stateless_.
11971197
* This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel].
11981198
*/
1199-
// todo: mark transform with crossinline modifier when it is supported: https://youtrack.jetbrains.com/issue/KT-19159
1199+
@ObsoleteCoroutinesApi
12001200
public fun <E, R> ReceiveChannel<E>.map(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R): ReceiveChannel<R> =
12011201
GlobalScope.produce(context, onCompletion = consumes()) {
12021202
consumeEach {

kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt

+12
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ class BasicOperationsTest : TestBase() {
7373
finish(4)
7474
}
7575

76+
@Test
77+
fun testIterator() = runTest {
78+
TestChannelKind.values().forEach { kind ->
79+
val channel = kind.create()
80+
val iterator = channel.iterator()
81+
assertFailsWith<IllegalStateException> { iterator.next() }
82+
channel.close()
83+
assertFailsWith<IllegalStateException> { iterator.next() }
84+
assertFalse(iterator.hasNext())
85+
}
86+
}
87+
7688
private suspend fun testReceiveOrNull(kind: TestChannelKind) = coroutineScope {
7789
val channel = kind.create()
7890
val d = async(NonCancellable) {

0 commit comments

Comments
 (0)