どうもこんばんわ
11 月ってこんな暑かったっけ?
本題
Jetpack Compose
を使うときによく、以下のような画面の値を持っておくステートクラスみたいなのを作ると思うんですけど
多分、参考:https://github.com/android/nowinandroid/blob/main/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt
一画面だけなら問題ないと思いますが、似たような画面で使いたい場合に、HomeScreenUiState
の名前変えただけバージョンがコピーされてしまうのは避けたいなと思ったわけで、
解決するにはジェネリクス<T>
みたいなのを使えばいいですね!!
というわけでこれ。
参考:https://stackoverflow.com/questions/44243763/how-to-make-sealed-classes-generic-in-kotlin
<out T>
← out
って何・・・
あと<Nothing>
って何なの?
確かに動くのですが、謎ばっかりだったので調べてみた
sealed class / sealed interface
シールクラスって言うとかなんとか
TypeScript
だとUnion
が一番近いかな?
このクラスの特徴は継承する際に制限があり(同じパッケージ内で継承して宣言する必要がある?)、その代わり継承しているクラスが分かるという特徴があります。
https://kotlinlang.org/docs/sealed-classes.html#location-of-direct-subclasses
基本的には同じファイル内にすべて継承するクラスを定義するはず。
例えばパッケージが違うとエラー
制限がある代わりに、継承しているクラスが全て分かるため、when
等でinstanceof
する際に継承しているクラスが既にわかっているため、全て網羅すればelse
を追加する必要がないという点です。
これはenum
とかでも言えることですね
実際どこで使うの
例えばUI
になにかアラートか何かを出すため、データを入れておくクラスをつくろうと思います。
アラートはこの三種類、どれも Android 端末を触ったことがあれば出会ったことがあると思う。
汎用的にこんな感じかな
あ!Snackbar
とDialog
はボタンがあるので、ボタンのテキストも設定できるようにしたいですね。
うーん雲行きが怪しく
あとダイアログは外側を押したらキャンセル出来るかのフラグも欲しいかも
きつくなってきた。
アラートの種類はenum
で表現できるのに、種類ごとに必要なデータが違うから使えない・・・!でも継承可だとなんか違う!
ってときに使うといいと思います。
これをうまく使ったのが、冒頭のUiState
ですね
when
の使いやすさと相まっていいと思う
以上!sealed class
Nothing / Any
ようやく本題、はい。
Any
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/
全てのクラスのスーパークラス
全てのクラスはこのAny
を継承しているってことらしい。
toString
とかはいつ見てもあると思いますがこれは根っこの部分Any
で用意されているからなんですねえ
試しにさっき作ったクラスをインスタンス化しis Any
してみますがもちろんtrue
になります
Nothing
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing.html
これは逆に全てのクラスを継承したクラスです。
Any
が根っこならNothing
はてっぺんです←?
全てのクラスを継承しているとかいう意味不明なクラスなので、インスタンス化することができません。
存在しない値を表現する際に使うらしい。例えばthrow
した場合Nothing
が返される。
もしNothing
を返す関数があればそれは例外を投げるか、無限ループになって呼び出し元に戻らない?になるらしいです。
イマイチ使い方が思いつかないですが、もう一個、多分こっちが本題!、
インスタンス化して使うことはできませんが、全てのクラスを継承した型として使うことができます。
全てのクラスを継承しているクラス(インスタンス化できないので建前でしか無いですが)はアップキャストと組み合わせることで効果を発揮します。
アップキャストはまぁ調べれば出ると思いますが、子クラスを親クラスにキャストするやつです。あの安全な方のキャスト
Android
だとImageView / TextView (子)
からView (子が継承している親のクラス)
にキャストするみたいな感じです。
話を戻して、アップキャストと組み合わせることでこんな事ができます。いや難しいなこれ
てかこれ考えたん賢くない???頭めっちゃいいと思う
で、これを次の<out T>
と組み合わせることで、Jetpack Compose
のUiState
クラスが共通化できちゃうわけ
どこで使われてるの
たとえばエルビス演算子(?:
)は、null 以外
なら左側の値、null
なら右側の値を返すという便利な演算子がありますが、
これthrow
を右側に置くことも出来ます。これはnull
の場合は例外を投げる。
一見すると、Kotlin
のコンパイラが?:
の右側にthrow
が来た場合はNonNull
として返すという処理が特別に入っているのかと思ってしまいますが、
そうではなく、throw
はNothing
を返すため、先述の通りString
が選ばれるというわけです。
これはTODO()
とかいう初見殺し関数でも使われている技で、TODO()
を入れると、とりあえずコンパイルが通るようになるのは、
TODO()
はまだ未実装だよって例外を投げる。するとNothing
を返すため、すべてを継承しているNothing
で型が解決できているという話なだけです。
ジェネリクス
ジェネリックなのかジェネリクスなのか、どっちなんだろう
https://kotlinlang.org/docs/generics.html
そこらの言語と同じ奴。
ただ、アップキャストの話をちょっと↑でしましたが、ジェネリクスだとうまく行かないことがあるんですね。(てなことをドキュメントに書いてる)
たとえばこんなコード
String
はAny
を継承しているので、アップキャストにより入れられるはず・・・が入れようとするとエラーになる。継承してるのになぜ?
これは訳あってそうしているらしく、以下のようにダウンキャストの可能性が捨てきれないんですね。(詳しくは Java ジェネリクス 共変
とかで調べてみてください)
参考: https://kotlinlang.org/docs/generics.html#variance
(なので以下のコードは動きません)
話を戻して、で、これを解決する方法があります。
Java
でも出来るらしいですが、Kotlin
だとout
を利用することでこの問題を解決できます。
これだとなんで通るようになるのかですが、<out T>
だとT
は返り値としてしか使うことができず、setter
やvar
等の値を変更する箇所でT
を使おうとすると怒られるわけです。
上記の問題はsetter
を使われてしまった場合に値が変わってしまう可能性があるために出来ないようにしていた、、、
が、変化しないと<out T>
で宣言することでこの問題が解決!ついでに変化しないので継承している子クラスも入れられるようになりました。
これらを組み合わせると
こういうステートが作れます。
一体それぞれが何の役割をしているか、わかった気がしませんか?
sealed interface
- 継承しているクラスを
Loading
/Successful
/Error
の3つに制限
when
やin
でクラスを比較する際はこの3つを見ればおっけー
object Loading
- 値を持たない場合は
object
でいいらしい
- もちろん
sealed interface
を継承してね
data class Successful<T>
- 成功時の状態と型
- 型をパラメータとして取ることで共通化
data class Error
- ジェネリクスに入れている
Nothing
- 全てのクラスを継承している型
Nothing
- 成功時以外は型を
Nothing
にすることで、アップキャストが働きNothing
以外の型を探そうとし、結果成功時の型が使われるようになる
<out T>
- ステート自体を共通化したいのでジェネリクスしたい!
Loading
/Error
の型をどうすればいいの?
- 全てのクラスを継承した型
Nothing
を使うとアップキャストが効いていい感じ
<T>
にout
をつけることで、親クラスを指定した際に子クラスも型パラメータに入れることを許容するように
- 先述の通り
Nothing
という特殊な型により、自動的に成功時の型が使われるようになる
・・・ってことで合ってる?
使われている所
emptyList()
という空の配列を返す関数があります。
こんな感じに、List
自体がnullable
の場合にエルビス演算子と合わせて使うとNonNull
になるので便利。
これは一見emptyList()
が中でnew List<T>
しているのかな?と思いきやここでもNothing
が活用されています。
EmptyList
と呼ばれるList<Nothing>
型の配列を返しています。
この関数はジェネリクスで<T>
を取ってはいますが、実際には使っておらず、シングルトンで作られた、中身のない配列、List<Nothing>
を返しています。
https://github.com/JetBrains/kotlin/blob/c6f337283d59fcede75954eebaa589ad1b479aea/libraries/stdlib/src/kotlin/collections/Collections.kt#L72
また、Kotlin
のList
はout T
(今見たらE
だった)といった感じでout
が付いています。
これにより、List
のジェネリクスの型<T>
にT
とT
を継承した型を入れられるようになっています。そしてNothing
は先述の通りすべての子なのでこのT
にいれることが出来ているわけです。
おわりに
間違ってたらすいません
参考にしました
難しい
https://stackoverflow.com/questions/44243763/how-to-make-sealed-classes-generic-in-kotlin
https://phoneappli.hatenablog.com/entry/2021/03/30/180749
https://stackoverflow.com/questions/55953052/kotlin-void-vs-unit-vs-nothing