Kotlin Coroutine について勉強したことのまとめ第二弾。
第一弾はこちら
Kotlin Coroutine の基本とキャンセルの伝播の話 -launchで伝播する流れを実際に読んでみた-
環境/前提
- Coroutinesのバージョンはv1.5.2
- Kotlinのバージョンはv1.5.31
挙動を試すときは、公式のonline Kotlin playground を使用すると便利です。(import文を書けば)coroutineも使用できます。
asyncとwithContextのキャンセルの伝播の仕様
第一弾でlaunchのコードを追ったときのように、asyncとwithContextも軽く内部実装やコメントを見つつメソッドの仕様やキャンセルの伝播の仕様を紹介します。
aync
![]() |
|---|
launchを追ったときの教訓よりデフォルトはstart.isLazyはfalseです。
したがって、DeferredCoroutineの実装を見てみます。
![]() |
|---|
DeferredCoroutineはlaunchを追ったときにも出てきたAbstractCoroutineの子クラスのようです。
また、その他asyncの内部実装に使われているnewCoroutineContextメソッドやAbstaractCoroutine#startを呼ぶ構造もlaunchと一緒です。
よって、launchを追ったときの結果から、
asyncに引数として渡されたラムダ関数の中で例外が発生した場合は,
async呼び出し時に渡されたCoroutineContextのJob要素またはasyncを起動したCoroutineScopeに紐づいたCoroutineContextのJob
を親にしたCoroutineScopeかつJobであるDeferredCoroutine
が例外を受け取ります。
※ちなみに、start.isLazyがtrueの時に使われているLazyDeferredCoroutineは、DeferredCoroutineの子クラスです。
![]() |
|---|
asyncのコード例と実行結果です。
【例】
![]() |
|---|
【実行結果 出力】
2
1
catch: java.lang.Exception: error
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.Exception: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:14)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
coroutine 3で発生した例外はcorouitne 3に紐づいた'Job'であるDeferredCoroutineが受け取り、Jobの親子関係に沿ってcorouitne 4のJobまで伝わりキャンセルされるので、println("3")は実行されません。
ちなみに、asyncに渡したラムダの中で発生した例外はasync#await()をtry-catchで囲むと捕捉できます(先ほどのコード例println(catch: $e)で実行結果のcatch: java.lang.Exception: errorが出力される部分)が、それでもcoroutineのキャンセルの伝播が止められるわけではありません。
それは、エラーの伝播は「そのコルーチンに紐づいたJobへ例外が到達したか」に依存するのに対して、asyncの中で例外が発生した場合にasync#await()はその例外をthrowする仕様でasync#await()を囲んだtry-catchでキャッチできているのはそのawaitが投げた例外だからです。
なので、以下のようにawaitを呼ばないとcatch節は呼ばれないし、
【例】
![]() |
|---|
【実行結果 出力】
2
1
Exception in thread "DefaultDispatcher-worker-1 @coroutine#4" java.lang.Exception: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:14)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
離れたところでawaitを呼びそれをtry-catchで囲んでもcatch節で例外を捕捉できます。そしてキャンセルの伝播自体は起きます。
【例】
![]() |
|---|
【実行結果 出力】
2
1
catch: java.lang.Exception: error
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.Exception: error
at FileKt$main$1$async1$1$1.invokeSuspend(File.kt:14)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
withContext
![]() |
|---|
![]() |
|---|
※長いのでスクリーンショットがコメント部と実装部の2枚になりました。
① … 現在のCoroutineContextに、引数で渡されたCoroutineContextのインスタンスを加算しています。(順番大事)
② … ScopeCoroutine
![]() |
|---|
こちらもAbstractCoroutineの子クラスのようです。
ただし、launchやasyncとの違いとして、赤く囲われた部分が重大なポイントになっています。
final override val isScopedCoroutine : Boolean get() = true とは?
overrideしている元はどこ?
→ JobSupportで定義されています。
※AbstractCoroutineはJobSupportを継承しています。
![]() |
|---|
→ isScopedCoroutineがtrueになるときは、例外をそれより内側のスコープにしか投げない、scoped coroutineと呼ばれるものになる!!
- 自分の子や孫で発生したエラーは親には伝播させません。
- 自分の親から伝播してきたキャンセルは、子や孫に伝播させます。
![]() |
|---|
こちらはScopeCoroutineの子クラス=AbstractCoroutineの子クラスのようです。
![]() |
|---|
こちらもScopeCoroutineの子クラス=AbstractCoroutineの子クラスのようです。
-
withContextはCoroutineScopeのメソッドではありません。 -
launchなどと同様suspend funなのでCoroutineScopeの中でしか呼べません。 - 【
current corouitneContext+ 引き数で渡されたcroutineContext】(順番大事)として作ったCoroutineContextをもつCoroutineScopeで引数として渡されたラムダ関数を実行します。 - ラムダ関数が終わるまで待って、結果を返します。
- ラムダ関数の中で例外が発生した場合、親のスコープには例外が伝播しません。
【例】
![]() |
|---|
【実行結果 出力】
1
catch: java.lang.Exception: error
2
- 一つ目の
launchと、withContextのスコープに紐づいたCoroutineContextのJobには親子関係があり、またひとつめのlaunchとふたつめのlaunchを起動しているCoroutineScopeに紐づいたCoroutineContextは同じものですが、エラーは伝播していません。 - エラーの伝播はしませんが、
withContextから中でキャッチされなかった例外は外に投げられています。
SupervisorJob
SupervisorJob(parent: Job? = null)
![]() |
|---|
※SupervisorJobImpl(parent: Job?)
![]() |
|---|
-
1つの子で例外が発生したとしても他の子にキャンセルを伝播させたくない!という時に使える
Job
例 : WebAPIをたたいたレスポンスデータを表示させるリストと、ローカルDBのデータを表示するリストを持っている画面で、どっちかの取得時に例外が発生したとしてももう片方までキャンセルする必要はない。 -
子Jobがエラーしても他の子Jobにキャンセルを伝播させないJob
![]() |
|---|
- 引数にJobを渡すとそのJobの子Jobになる。
- 伝播してきたキャンセルは自分の子に伝播する。
※この辺りは普通のJobと同じ
![]() |
|---|
- SupervisorJobの子Jobは(普通にlaunchなどを使う場合は)SupervisorJobではない(孫Job同士はキャンセルが伝播する)ことに注意
![]() |
|---|
【例】
![]() |
|---|
【実行結果 】出力】
1
3
5
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.RuntimeException: exception
at FileKt$main$2$2.invokeSuspend(File.kt:23)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2
【解説/捕捉】
![]() |
|---|
① … ラムダが実行されるのはSupervisoJobの子に紐づいたスコープ。キャンセルされない。
② … ラムダが実行されるのはSupervisoJobの子に紐づいたスコープ。自分の子からエラーが伝播してきたエラーを受け取る。
③ … ラムダが実行されるのはSupervisoJobの孫に紐づいたスコープ。片方で発生したエラーにより、もう片方もキャンセルする。(伝播している。)
CoroutineExceptionHandler
- CoroutineContextのひとつ。
- キャッチされなかった例外を処理できる(ことがある)。(詳しくは後述)
![]() |
|---|
【例】
キャッチできなかった例外を処理できるとき
これが
![]() |
|---|
【実行結果】
1
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.RuntimeException: テスト
at FileKt$main$2.invokeSuspend(File.kt:16)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
こうなる
- エラーが伝播しないとかではない。
- 伝播させた後の処理についてカスタマイズできるだけ。
![]() |
|---|
【実行結果】
1
例外キャッチ : java.lang.RuntimeException: テスト
※CoroutineExceptionHandlerを設定できるのは
コルーチンスコープそのものの作成時かルートのみ
= 途中で上書きしようとしても意味をなさない
【例】
![]() |
|---|
【実行結果 出力】
1
例外キャッチ2 : java.lang.RuntimeException: テスト
キャッチされなかったエラーを処理できる「ことがある」とは?
launchのなかで発生したエラーはハンドリングできるけど、asyncのなかのものはハンドリングできません。
理由をみていきます。
そもそも、どうやってlaunchはキャッチされない例外があった時にCoroutineExceptionHandlerに処理を渡しているのか
launchで使われているCoroutineContextであるStandaloneCoroutine
![]() |
|---|
赤枠のhandleJobExceptionメソッドのoverrideがポイント
overrrride元 : JobSupport#handleJobException
- 「最後まで処理されなかったExceptionを処理します。処理した時はtrueを返します。」
![]() |
|---|
- では、実際に処理している
handleCoroutineExceptionメソッドは何をしている?
![]() |
|---|
① … CoroutineContextがCoroutineExceptionHandler要素を持つ場合は、それを呼び出して処理を委任
② … ない時や、CoroutineExceptionHandlerが例外を投げた時はグローバルな(デフォルトの)ハンドラーに処理を委任
→ CoroutineExceptionHandlerがキャッチされなかった例外をハンドリングできるか否かは
起動されたcoroutineのJobSupport#handleJobExceptionの内容に左右される=coroutineを起動するメソッドの実装に左右される
- では、
ayncの場合
asyncで使用されているDefferedCoroutine
![]() |
|---|
独自にJobSupport#handleJobExceptionのoverrideはしていないようです。
※DefferedCoroutine‘の親クラスであるAbstractCoroutineも[JobSupport#handleJobException`はoverrideしていません。](https://github.com/Kotlin/kotlinx.coroutines/blob/1.5.2/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt#L37)
→
したがって、asyncはCoroutineExceptionHandler要素を持つCoroutineContextに紐づいたCoroutineScopeで起動しても、CoroutineExceptionHandlerでキャッチされなかった例外はハンドリングできません!
コルーチンビルダー
コルーチンビルダーとは
新しいcoroutineを起動するメソッド
例 : launch、asyncなど
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
- ここでは、特殊なコルーチンビルダーである
runBlockingを紹介します。
runBlocking
![]() |
|---|
-
CoroutineScopeのメソッドではない -
suspend functionでもない -
CoroutineScopeの中で呼んではいけない - 呼び出し側のスレッドを止めて、渡されたラムダ関数を実行します
- ラムダ関数は
CoroutineScopeで実行されます -
launchで起動したものなど、子コルーチンのすべての終了を待つ - ラムダ関数内でキャッチされない例外が発生した場合、
runBlockingを呼び出したスレッドに例外が伝播する(ようだ)
【例】
- 何もしなくても、起動されたコルーチンが終わるまで待つ
![]() |
|---|
【実行結果 表示】
1
2
【例】
- 元Threadを止めます
- 値を返します
![]() |
|---|
【実行結果 表示】
1
2
3
【例】
- キャッチされない例外が発生した場合、呼び出し元スレッドに例外を投げます。
![]() |
|---|
【実行結果 表示】
1
Exception in thread "main" java.lang.RuntimeException: exception
at FileKt$main$blocking$1.invokeSuspend (File.kt:10)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
【例】
- キャッチされない例外が発生した場合、呼び出し元のスレッド(だけ)に例外を投げます、
![]() |
|---|
【実行結果 表示】
4
1
Exception in thread "Thread-0" java.lang.RuntimeException: exception
at FileKt$main$1$blocking$1.invokeSuspend(File.kt:11)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
5
スコープビルダー
スコープビルダーとは
- 新しい
CoroutineScopeを作成するメソッド
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder
- ここでは2つ(
coroutineScope、supervisorScope)を紹介します。
couitneScope
- 挙動のほとんどは(
withContextの時にも出てきた)ScopeCoroutineに依存しています。→ScopeCoroutine
![]() |
|---|
-
CoroutineScopeのメソッドではない -
suspend funなのでCoroutineScopeのなかでしか呼べません - 全ての子コルーチンの終了を待ちます
- 作成する
CoroutineScopeに紐づくCoroutineContextは、current corouitneContextを引き継ぐがJobだけは必ず上書きします - しかしその
Jobはcurrent corouitneContextのJobを親とします
-→ -
ScopeCoroutineを使用しているので、子や孫から伝播してきたエラーは親には伝播させません - 子でエラーが発生した場合、ほかの子はキャンセルします
【例】
- 全ての子コルーチンの終了を待ちます
![]() |
|---|
【実行結果 表示】
1
2
3
4
5
6
7
【例】
- 子や孫から伝播してきたエラーは親には伝播させません
- 子でエラーが発生した場合、ほかの子はキャンセルします
![]() |
|---|
① … 他の子でエラーが発生したので、delayの間にキャンセルします
② … coroutineScopeの子でエラーが発生してもcoroutineScopeに紐づいたCoroutineContextのJobの親にエラーは伝播しないので、キャンセルされずに実行されます
【実行結果 表示】
1
8
2
3
4
5
catch: java.lang.RuntimeException: error
7
9
【例】
-
Jobを介して自分の親に例外を伝播させることはありませんが、キャッチされない例外が発生した場合coroutineScopeの外に例外を投げることはします
![]() |
|---|
① … coroutineScopeが外に例外を投げる=ラムダ内でエラーが発生しました
② … ①のlaunchはcoroutineScopeで失敗しているので、②は実行されません
② … 同じJobに紐づいたCoroutineScopeから起動した①がエラーで失敗したので、delayの間でキャンセルが伝播して最後のprintln("9")は実行されません
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-2 @coroutine#1" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:15)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
supervisorScope
- 挙動のほとんどは
SupervisorCoroutineに依存しているようです。(赤枠の中)
![]() |
|---|
- ベースは
ScopeCoroutineで、childCancelledだけoverrideされているようです。
![]() |
|---|
-
CoroutineScopeのメソッドではない -
suspend funなのでCoroutineScopeのなかでしか呼べません - 全ての子コルーチンの終了を待ちます
- 作成する
CoroutineScopeに紐づくCoroutineContextは、current corouitneContextを引き継ぐがJobだけは必ず上書きします
(※コメントには「SupervisorJobで上書きする」、とありますが、コードを見る限りSupervisorJobと同じようにchildCancelledをoverrideしているJobを使用指定はいるけどSupervisorJobそのものを利用しているようには見えない…?) - しかしその
Jobはcurrent corouitneContextのJobを親とします -
ScopeCoroutineを使用しているので、子や孫から伝播してきたエラーは親には伝播させません - 子でエラーが発生した場合、ほかの子はキャンセルしません
→ coroutineScopeの、上書きするJobを`SupervisorJob‘にしたバージョンです。
【例】
![]() |
|---|
① … 他の子でエラーが発生しても、キャンセルされません
② … supervisorScopeの子でエラーが発生してもsupervisorScopeに紐づいたCoroutineContextのJobの親にエラーは伝播しないので、キャンセルされずに実行されます
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:16)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
6
7
9
【例】
- キャッチされない例外が発生した場合も
supervisorScopeの外に例外を投げることもしないようです。なぜかは分からず…。
※なので、ひとつ前の例でもtry-catchでcatchできていません
![]() |
|---|
① … supervisorScopeは外に例外を投げないので、エラーは発生していません
② … 正常に実行されます
② … 同じJobに紐づいたCoroutineScopeから起動した①はエラーなしで成功したので、最後のprintln("9")も実行されます
【実行結果 表示】
1
2
8
3
4
5
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.RuntimeException: error
at FileKt$main$1$1$1.invokeSuspend(File.kt:15)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
6
7
9
コルーチンビルダー、スコープビルダーの「全ての子コルーチンの終了を待つ」について
- 子コルーチンを起動する時に
CoroutineDispatcher等を新しいものに上書きしても大丈夫だが、Jobを新しいものにすると待ってくれなくなります。(runBuilding,coroutineScope,supervisorScopeすべて) - ドキュメントにもコード上にも根拠は見つけられなったが、親子間のエラーの伝搬などがすべて
Jobに依存していることを考えると納得できる挙動ではある。
【例】
![]() |
|---|
【実行結果 表示】
1
3
2
4
【例】
![]() |
|---|
【実行結果 表示】
1
3
4
2
コルーチンビルダーとスコープビルダーの違いについて
※コルーチンビルダーとスコープビルダーの違いは、混とんとしているように見えます。
- 公式サイトの内容や各メソッドのコメントを見ると、
launchやrunBlockingはコルーチンビルダー/coroutineScopeやsupervisorScopeはスコープビルダーとして書かれているように思われますがlaunchのなかで作成しているAbstractCoroutineもCoroutineScopeを実装しています。
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder
-
final override val isScopedCoroutine : Boolean get() = trueなJobやCoroutineScopeを使用しているものをスコープビルダーと呼ぶというのでもなさそうです。
(runBlockingで使用しているBlockingCoroutineも、coroutineScopeやsupervisorScopeで使用しているScopeCoroutineもfinal override val isScopedCoroutine : Boolean get() = trueなので)
→結局、どこに着目して分類されているかに左右されている?
参考資料
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder-and-concurrency
https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler
https://zenn.dev/at_sushi_at/books/edf63219adfc31
https://zenn.dev/wm3/articles/aa85d6cc7aa0a8146863#coroutine-builder-%E3%81%A8-coroutinescope
https://mahata.gitlab.io/post/2019-04-23-coroutines-kotlin/
https://speakerdeck.com/ntaro/kotlin-contracts-number-m3kt?slide=12
その他記事内のリンクのページなど










































