При разработке API с функцией suspend
иногда я хочу передать, что эта функция должен вызывается, скажем, в потоке ввода-вывода. В других случаях это существенный.
Часто это кажется очевидным; например, вызов базы данных должен вызываться с использованием Dispatchers.IO
, но если это интерфейсная функция, то вызывающий не может этого предполагать.
Какой здесь лучший подход?
Если функция suspend
действительно должна выполняться в определенном контексте, объявите ее непосредственно в теле функции.
suspend fun doInIO() = withContext(Dispatchers.IO) {
}
Если вызывающий должен иметь возможность изменить диспетчера, функция может добавить диспетчер в качестве параметра по умолчанию.
suspend fun doInIO(context: CoroutineContext = Dispatchers.IO) = withContext(context) {
}
Может, что-то вроде suspend fun doSomething(dispatcher: CoroutineDispatcher = Dispatchers.IO) = withContext(dispatcher) {...}
?
Да, если у вызывающего абонента должна быть возможность сменить диспетчера, ваше решение будет в порядке. Отредактирую свой ответ.
Не уверен, но может понятнее принять только диспетчера. В противном случае непонятно, почему функция принимает контекст сопрограммы, когда она уже вызывается в контексте сопрограммы. Принимая только диспетчер, мы говорим, что используется текущий контекст сопрограммы, но с переопределением только диспетчера.
Я бы остался с CoroutineContext
. Таким образом, вызывающий может решить добавить что-то еще, например, обработчик исключений сопрограммы. С моей точки зрения, ограничение типа диспетчером не дает никаких преимуществ.
Для подобных контрактов нет строгого механизма, поэтому вы можете гибко выбирать механизм, который подходит вам и вашей команде.
1) Всегда используйте withContext(Dispatcher.IO)
. Это одновременно и строго, и производительно: если метод вызывается из контекста IO
, он будет быстрый путь.
2) Соглашения на основе именования / аннотаций. Вы можете договориться с командой, что любой метод, заканчивающийся на IO
или имеющий определенную аннотацию, должен вызываться с помощью Dispatchers.IO
. Этот подход работает в основном в небольших командах и только для частного API проекта. Как только вы начнете экспортировать его как библиотеку / модуль для других команд, такие контракты, как правило, разрываются.
3) Вы можете смешать предыдущий подход с проверкой:
suspend fun readFile(file: ...) {
require(coroutineContext[ContinuationInterceptor] == Dispatcher.IO) {
"Expected IO dispatcher, but has ${coroutineContext[ContinuationInterceptor]} instead"
}
// read file
}
Но эта проверка работает только в том случае, если вы не обертываете диспетчер ввода-вывода в какой-то делегат / прокси. В этом случае вы должны уведомить валидацию о таких прокси, например:
fun validateIoDispatcher(dispatcher: ContinuationInterceptor) {
if (dispatcher is Dispatchers.IO) return
if (dispatcher is ProjectSpecificIoDispatcher) return
if (dispatcher is ProjectSpecificWrapperDispatcher) {
validateIoDispatcher(dispatcher.delegate)
} else {
error("Expected IO dispatcher, but has $dispatcher")
}
}
I want to convey that this function should be called on, say, an IO thread. Other times that it is essential to do so.
Не уверен, в чем разница между «следует» и «необходимо», но имея в виду эти подходы, вы можете комбинировать их с параметрами метода по умолчанию, такими как suspend fun probablyIO(dispatcher: CoroutineDispatcher = Dispatchers.IO)
или более гибкими соглашениями об именах / аннотациях.
Хорошо, спасибо, но как насчет того, где это должен? Другая проблема заключается в том, что что, если вызывающий абонент использует какой-либо другой диспетчер потоков ввода-вывода? Этот стиль заставит их использовать Dispatchers.IO.