Kotlin suspend fun

2019-06-20 04:13发布

问题:

I have Kotlin interface

interface FileSystem {
    suspend fun getName(path: Path): List<String>
}

How I can call it from Java? What is

Continuation <? super List<String>>

回答1:

Kotlin implements coroutines using a mix of the regular stack-based calling convention and the continuation-passing style (CPS). To achieve that, it performs a CPS transformation on all suspend funs by adding an implicit parameter, an object you can use to continue the program from the place where the function was called. That's how Kotlin manages to pull off the trick to suspend the execution within your function's body: it extracts the continuation object, saves it somewhere, and then makes your function return (without yet having produced its value). Later on it can achieve the effect of jumping into the middle of your function body by invoking the continuation object.

The continuation is basically a callback object, just like those familiar from async Java APIs. Instead of returning its result, the suspendable function passes its result to the continuation. To call a suspend fun from java, you'll have to create such a callback. Here's an example:

Continuation<List<String>> myCont = new Continuation<List<String>>() {
    @Override public void resume(List<String> result) {
        System.out.println("Result of getName is " + result);
    }
    @Override public void resumeWithException(Throwable throwable) {
        throwable.printStackTrace();
    }
    @NotNull @Override public CoroutineContext getContext() {
        return Unconfined.INSTANCE;
    }
};

NOTE: The above works only with experimental coroutines. In the actually released API there's just one resumption method: resumeWith(result: Result<T>) where Result is a discriminated union of the result type and the internal class Failure, which makes it unaccessible from Java.

Let's also create a mock implementation of the FileSystem interface:

class MockFs : FileSystem {
    override suspend fun getName(path: Path): List<String> {
        suspendCoroutine<Unit> {
            println("getName suspended")
        }
        println("getName resumed")
        return listOf("usr", "opt")
    }
}

Now we're ready to call it from Java:

Object result = new MockFs().getName(Paths.get(""), myCont);
System.out.println("getName returned " + result);

It prints

getName suspended
getName returned
kotlin.coroutines.experimental.intrinsics.CoroutineSuspendedMarker@6ce253f1

getName() returned a special marker object that signals the function got suspended. The function will pass its actual result to our callback, once it resumes.

Let us now improve MockFs so we can get access to the continuation:

class MockFs : FileSystem {
    var continuation : Continuation<Unit>? = null

    override suspend fun getName(path: Path): List<String> {
        suspendCoroutine<Unit> {
            continuation = it
            println("getName suspended")
        }
        println("getName resumed")
        return listOf("usr", "opt")
    }
}

Now we'll be able to manually resume the continuation. We can use this code:

MockFs mockFs = new MockFs();
mockFs.getName(Paths.get(""), myCont);
mockFs.getContinuation().resume(Unit.INSTANCE);

This will print

getName suspended
getName resumed
Result of getName is [usr, opt]

In real life a suspendable function will use some mechanism to get itself resumed when the result becomes available. For example, if it's a wrapper around some async API call, it will register a callback. When the async API invokes the callback, it will in turn invoke our continuation. You shouldn't need to manually resume it like we did in our mock code.

A suspend fun also has the option to just return its result directly. For example, with this MockFs code

class MockFs : FileSystem {
    override suspend fun getName(path: Path) = listOf("usr", "opt") 
}

in Java we can just say

System.out.println(new MockFs().getName(Paths.get(""), myCont));

and it will print [usr, opt]. We could even have passed in an empty implementation of Continuation.

The most demanding case happens when you don't know in advance whether the function will suspend itself or not. In such a case a good approach is to write the following at the call site:

Object retVal = mockFs.getName(Paths.get(""), myCont);
if (retVal != IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
    myCont.resume((List<String>) retVal);
}

Otherwise you'll have to duplicate the code that handles the function's result.