1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlinのサスペンド関数(Suspend Function)

Posted at

サスペンド関数は、Kotlinのコルーチン(非同期プログラミング)の中核となる機能です。長時間実行される操作を非ブロッキングな方法で処理するために設計されています。

1. サスペンド関数の基本概念

定義と特徴

サスペンド関数とは、suspend修飾子が付いた関数で、実行中に一時停止(サスペンド)して後で再開できる特別な関数です。

suspend fun fetchUserData(): UserData {
    // 時間のかかる操作(ネットワークリクエストなど)
    return api.getUserData() // これも通常はsuspend関数
}

重要なポイント

  1. 一時停止と再開

    • サスペンド関数は実行中に一時停止(サスペンド)でき、後で再開できます
    • 一時停止中はスレッドをブロックせず、他の処理に使用できる
  2. サスペンドポイント

    • サスペンド関数内で別のサスペンド関数を呼び出す場所が「サスペンドポイント」
    • この地点で関数は一時停止する可能性があります
  3. 呼び出し制約

    • サスペンド関数は別のサスペンド関数または特定のコルーチンビルダー(launchasyncなど)からのみ呼び出せます
    • 通常の関数から直接呼び出すことはできません

2. サスペンド関数の使い方

基本的な使用例

import kotlinx.coroutines.*

// サスペンド関数の定義
suspend fun performTask(): String {
    delay(1000) // 非ブロッキングな遅延(これもサスペンド関数)
    return "Task completed"
}

// コルーチンの中でサスペンド関数を呼び出す
fun main() = runBlocking {
    println("Starting task...")
    val result = performTask() // サスペンド関数を呼び出し
    println(result)
}

コルーチンビルダーとの関係

サスペンド関数は、次のようなコルーチンビルダー内から呼び出す必要があります:

fun main() = runBlocking {
    // launch:新しいコルーチンを開始し、結果を待たない
    launch {
        val data = fetchUserData()
        println("User data: $data")
    }
    
    // async:新しいコルーチンを開始し、結果を返す
    val deferred = async {
        fetchUserProfile()
    }
    val profile = deferred.await() // 結果を取得
    
    // coroutineScope:子コルーチンが完了するまで現在のコルーチンを一時停止
    coroutineScope {
        val result1 = async { fetchData1() }
        val result2 = async { fetchData2() }
        println("Results: ${result1.await()} and ${result2.await()}")
    }
}

3. サスペンド関数の内部メカニズム

サスペンド関数は、コンパイル時に「コンティニュエーション渡しスタイル(CPS)」という手法で変換されます。

変換の概要

// 元のサスペンド関数
suspend fun fetchUserData(): UserData {
    val token = getToken()
    return api.getUserData(token)
}

// コンパイラによって変換された形(イメージ)
fun fetchUserData(continuation: Continuation<UserData>): Any {
    val state = continuation as? FetchUserDataContinuation ?: FetchUserDataContinuation(continuation)
    
    when(state.label) {
        0 -> {
            state.label = 1
            return getToken(state)
        }
        1 -> {
            val token = state.result as String
            state.label = 2
            return api.getUserData(token, state)
        }
        2 -> {
            return state.result as UserData
        }
        else -> error("Invalid state")
    }
}

この仕組みにより、サスペンド関数は実行中に一時停止し、その状態を保存して後で再開できます。

4. 実践的なパターンと応用例

エラーハンドリング

suspend fun fetchSafeUserData(): Result<UserData> {
    return try {
        val data = fetchUserData()
        Result.success(data)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

// 使用例
launch {
    when (val result = fetchSafeUserData()) {
        is Result.Success -> displayUserData(result.value)
        is Result.Failure -> showError(result.error)
    }
}

並列処理

suspend fun fetchCombinedData(): CombinedData = coroutineScope {
    // 並列に2つのリクエストを実行
    val userData = async { fetchUserData() }
    val profileData = async { fetchUserProfile() }
    
    // 両方の結果を待ってから結合
    CombinedData(userData.await(), profileData.await())
}

タイムアウト設定

suspend fun fetchWithTimeout(): UserData {
    return withTimeout(5000L) { // 5秒でタイムアウト
        fetchUserData()
    }
}

リトライ機能の実装

suspend fun <T> retry(
    times: Int = 3,
    initialDelay: Long = 100,
    maxDelay: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: Exception) {
            // リトライ間隔を指数関数的に増加
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
        }
    }
    // 最後の試行
    return block()
}

// 使用例
val data = retry(times = 3) {
    fetchUserData()
}

Flow(リアクティブストリーム)との組み合わせ

fun fetchUserUpdates(): Flow<UserData> = flow {
    while (true) {
        val userData = fetchUserData() // サスペンド関数
        emit(userData) // 値を流す
        delay(5000) // 5秒ごとに更新
    }
}

// 使用例
launch {
    fetchUserUpdates()
        .filter { it.isActive }
        .collect { userData ->
            updateUI(userData)
        }
}

5. サスペンド関数のベストプラクティス

1. サスペンド関数の命名規則

// 良い例:時間がかかる可能性があることを名前で示す
suspend fun fetchUserData(): UserData
suspend fun calculateComplexResult(): Result

// 避けるべき例:すぐに完了する操作
suspend fun getUsername(): String // 単純なプロパティアクセスはsuspendにする必要はない

2. サスペンド関数と通常関数の使い分け

  • サスペンド関数を使う場合

    • I/O操作(ネットワーク、ファイルなど)
    • 長時間実行される計算
    • 他のサスペンド関数を呼び出す必要がある場合
  • 通常関数を使う場合

    • 短時間で完了する処理
    • メモリ内の操作のみを行う場合
    • UI更新など、メインスレッドで行う必要がある処理

3. コンテキスト指定の明示

// CPU集約的な処理の場合
suspend fun calculateResult() = withContext(Dispatchers.Default) {
    // 重い計算処理
}

// I/O操作の場合
suspend fun fetchData() = withContext(Dispatchers.IO) {
    // ネットワークリクエストなど
}

// UI更新の場合
suspend fun updateUI() = withContext(Dispatchers.Main) {
    // UI更新処理
}

4. キャンセレーション対応

suspend fun fetchDataWithCancellation(): Data {
    while (isActive) { // コルーチンがアクティブかチェック
        // 長時間実行される処理...
        yield() // コルーチンのキャンセルを協調的に確認
    }
    throw CancellationException("Coroutine was cancelled")
}

6. 実際のアプリケーションでの使用例

Androidでの例

class UserRepository(private val api: UserApi) {
    suspend fun getUser(userId: String): User {
        return withContext(Dispatchers.IO) {
            api.fetchUser(userId)
        }
    }
}

class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _userData = MutableLiveData<User>()
    val userData: LiveData<User> = _userData
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            try {
                val user = repository.getUser(userId)
                _userData.value = user
            } catch (e: Exception) {
                // エラー処理
            }
        }
    }
}

// Activityやフラグメントでの使用
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        
        viewModel.userData.observe(this) { user ->
            // UIの更新
        }
        
        viewModel.loadUser("user123")
    }
}

バックエンドでの例(Ktor)

fun Application.module() {
    routing {
        get("/users/{id}") {
            val userId = call.parameters["id"]
            
            // サスペンド関数を直接ルートハンドラ内で使用できる
            val user = userRepository.getUser(userId)
            call.respond(HttpStatusCode.OK, user)
        }
    }
}

7. サスペンド関数の制限と注意点

  1. メインセーフ

    • サスペンド関数自体はスレッドセーフではなく、呼び出し元のコンテキストで実行される
    • 必要に応じてwithContextを使用して適切なディスパッチャに切り替える
  2. 拡張関数としての利用

    • サスペンド関数は既存のクラスの拡張関数としても定義可能
suspend fun String.fetchDataForId(): Data {
    return apiService.fetchData(this) // thisは文字列(ID)
}

// 使用例
launch {
    val data = "user123".fetchDataForId()
}
  1. 高階関数との組み合わせ
fun <T> withRetry(times: Int, block: suspend () -> T): suspend () -> T {
    return {
        retry(times) { block() }
    }
}

// 使用例
val fetchWithRetry = withRetry(3) { fetchUserData() }
launch {
    val result = fetchWithRetry()
}
  1. インターフェース内でのサスペンド関数
interface UserRepository {
    suspend fun getUser(id: String): User
    suspend fun updateUser(user: User): Boolean
}

class UserRepositoryImpl(private val api: UserApi) : UserRepository {
    override suspend fun getUser(id: String): User = api.fetchUser(id)
    override suspend fun updateUser(user: User): Boolean = api.sendUser(user)
}

まとめ

サスペンド関数は、Kotlinのコルーチンシステムの中核機能であり、非同期処理を効率的かつ読みやすい方法で実装するための優れたツールです。時間のかかる操作をブロッキングせずに実行でき、スレッドリソースを効率的に使用できます。適切に使用することで、複雑な非同期処理フローを簡潔かつ理解しやすいコードで表現できます。

サスペンド関数は、特にAndroidアプリケーションやバックエンドサービスなど、I/O操作や長時間実行されるタスクを処理する場面で非常に役立ちます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?