次の記事は、第3章から翻訳されています-Item24-Effective Kotlinのジェネリック型の差異を考慮してください:ベストプラクティス
ジェネリックスの詳細については、他の記事「Java/Kotlinのジェネリックスを理解するための記事」を参照してください。
アイテム24:ジェネリック医薬品のバリエーションに焦点を当てる
用語集
英文 | 中国語 | 説明 |
---|---|---|
タイプパラメータ | タイプパラメータ | ジェネリックスの山括弧List<T> 内T |
分散修飾子 | 型変数修飾子 | in とout |
- | サブクラスジェネリック | 標準の変換は次のようになります。typeパラメーターはサブクラスのジェネリックタイプです。説明の便宜上、ここでは「サブクラスジェネリック」と省略します。 |
- | 親クラスジェネリック | 標準的な変換は次のようになります。typeパラメーターは親クラスのジェネリックタイプです。説明の便宜上、ここでは「親クラスのジェネリックタイプ」と呼びます。 |
関数型 | 関数型 | 次のような形:(T)-> U |
翻訳者注:この記事には多くの固有名があります。理解と記憶を容易にするために、名詞の比較表をここに示します。
次のジェネリッククラスがあるとします。
class Cup<T>
复制代码
上記のジェネリッククラスのtypeパラメーターはT
、 type-variant修飾子(in
またはout
)を指定しないため、デフォルトでは不変です。不変とは、 :Cup<Int>
やなどのサブクラスジェネリックとスーパークラスジェネリックの間に継承関係がなく、間に継承関係がないことを意味します。Cup<Number>
Cup<Any>
Cup<Noting>
fun main() {
val anys: Cup<Any> = Cup<Int>() // 编译错误,类型不匹配
val nothings: Cup<Nothing> = Cup<Int>() // 编译错误
}
复制代码
それらを継承したい場合は、タイプバリアント修飾子を使用する必要があります。ここout
で、in
ジェネリックを共変にし、ジェネリックを反変にします。out
in
class Cup<out T>
open class Dog
class Puppy: Dog()
fun main(args: Array<String>) {
val b: Cup<Dog> = Cup<Puppy>() // 协变之后,子类泛型是父类泛型的子类,子类可以赋值给父类
}
复制代码
class Cup<in T>
open class Dog
class Puppy: Dog()
fun main(args: Array<String>) {
val b: Cup<Puppy> = Cup<Dog>() // 逆变之后,父类泛型是子类泛型的子类,子类可以赋值给父类
}
复制代码
次の図は、このタイプの関係を示しています。
関数型
Kotlinでは、関数型も可変です。次に例を示します。
fun printProcessedNumber(transition: (Int) -> Any) {
print(transition(42))
}
复制代码
このメソッドのパラメーターは関数型であり、次のすべてのタイプのパラメーターを受け入れることができます:(Int) -> Number
、、、、(Number) -> Any
など。(Number) -> Number
(Any) -> Number
(Number) -> Int
これらのタイプの継承関係は次のとおりです。
この継承関係から、上から下に見ると、パラメーター型は継承システムの上位型(親クラスの方向)に移動し、戻り型は下位型(の方向)に移動することがわかります。サブクラス)
Kotlinタイプの継承システムKotlinでは、すべての関数型のパラメーター型が反変であり、関数型の戻り型が共変であるため、これは偶然の一致ではありません。
Kotlinでタイプバリエーションをサポートするタイプはこれだけではありません。共分散List
(out修飾子で宣言)をサポートしMutableList
、不変である、より一般的なタイプがあります。
タイプバリアント修飾子の安全性
Javaでは、配列は共変です。多くの情報源によると、これは、配列をパラメーターとして使用する場合の形式sort
で、さまざまなタイプの配列に対して同じ並べ替えロジックをサポートできます。ただし、配列の共分散には大きな問題があります。
Integer[] numbers = {1, 4, 2, 1};
Object[] objects = numbers; // 编译没有问题
objects[2] = 'B'; // 编译没有问题,但是运行时抛出 ArrayStoreException
复制代码
Kotlinでは、配列は不変であるため、上記の問題は存在しません。
サブクラスをスーパークラスに割り当てると、暗黙のアップキャストプロセスがあります。
open class Dog
class Puppy: Dog()
class Hound: Dog()
fun takeDog(dog: Dot) {}
takeDog(Dog())
takeDog(Puppy())
takeDog(Hound())
复制代码
上記のコードには共分散は含まれていません。共分散とアップキャストにより、共分散型パラメーターを入力位置に配置すると、任意の型を入力パラメーターに渡すことができますが、これは明らかに非常に危険です。
class Box<out T> {
private var value: T? = null
// 1. 这个是会编译报错的,我们假设允许这么写,看看会发生什么
fun set(value: T) {
this.value = value
}
fun get(): T = value ?: error("value not set")
}
val puppyBox = Box<Puppy>() // 我是一个用来放 puppy 的盒子
val dogBox: Box<Dog> = puppyBox // puppyBox 向上转型为 dogBox,但我仍然是用来放 puppy 的盒子
dogBox.set(Hound()) // 如果 1 处可以这么写的话,因为 Hound 是 Dog 的子类,这里也可以正常传入了,但是,我本来明明是放小狗狗(puppy)的,你现在给我塞了一只猎犬(hound)
// 接下来举个更离谱的例子
val dogHouse = Box<Dog>() // 我是一个狗窝
val box: Box<Any> = dogHouse // 向上转型为 Box<Any>,但我仍然是个狗窝
box.set("some string") // String 是 Any 子类,可以传入,但我是狗窝啊,你给我丢一个字符串进来!
box.set(42) // Int 也是 Any 子类,可以传入,离谱,我是狗窝,你给我塞一个 Int
复制代码
したがって、これを回避するために、Kotlinはコンパイル時にこの動作を許可しません。Kotlinはパブリック入力の場所で共変型パラメーターを許可しません。
class Box<out T> {
var value: T? = null // 编译错误
fun set(value: T) { // 编译错误
this.value = value
}
}
复制代码
アクセス修飾子を次のように変更private
すると:
class Box<out T> {
private var value: T? = null
private fun set(value: T) {
this.value = value
}
}
复制代码
协变的类型参数通常只能用于作为消费者对外暴露读取方法,一个很好的例子就是 Kotlin 中的 List<T>
,在 Kotlin 中, List
只提供了可读方法,因此 List
在声明处定义成了协变(使用 out
)
对应的,逆变的类型参数如果放在公有的输出位置,也会存在问题:
open calss Car
interface Boat
class Amphibious: Car(), Boat
class Box<in T>(
// 1. 这会编译错误,我们假设这个是允许的,看看会发生什么问题
val value: T
)
val garage: Box<Car> = Box(Car()) // 我是一个车库
val amphibiousSpot: Box<Amphibious> = garage // 因为支持逆变,这里可以赋值给子类泛型
val boat: Boat = garage.value // 如果 1 是支持的话,这里赋值也是支持的,但是,我明明是个车库,我没法提供船!
// 一个更离谱的例子
val noSpot: Box<Nothing> = Box<Car>(Card()) // 我是个车库,但我先转型成一个啥也不是的东西
val noting: Noting = noSpot.value // 我没法提供一个 nothing!
复制代码
因此,为了避免这种情况发生,Kotlin 在编译时禁止了这种行为:Kotlin 禁止在公有的输出位置使用逆变的类型参数:
class Box<in T> {
var value: T? = null // 编译错误
fun get(): T = value ?: error("value not set") // 编译错误
}
复制代码
同样的,改为 private
就可以了,代码不再赘述
译者注:这和 Java 中的 PECS 是一致的:
Effective Java, 3rd Edition 的作者 Joshua Bloch 称那些你只能从中 读取 的对象为 生产者 ,并称那些你只能 写入 的对象为 消费者。因此他提出了以下助记符:
PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)
型变修饰符的位置
型变修饰符可以作用在两种位置上:声明处型变和使用处型变。声明处型变可以作用在所有使用该泛型的地方,而使用处型变则可以更加灵活地控制我们需要哪种型变。
比如对于某些只有可读方法的泛型,我们可以使用声明处型变,例如前面举例的 List<T>
, 但是像 MutableList
即可以写也可以读,那么就更加适合用使用处型变来按照我们自己的需求去定义何种型变了。
译者注: 在 Java 中只有使用处型变
总结
Kotlinには強力なジェネリック型があり、use-declarationとuse-of-variantsの両方をサポートしています
- デフォルトのタイプパラメータは不変です
out
修飾子は型パラメーターを共変にすることができますin
修飾子は、型パラメーターを反変にすることができます
Kotlinで
List
とSet
は共変でありMutableList
、、、は不変ですMutableSet
MutableMap
- 関数型のパラメーター型は反変であり、関数型の戻り型は共変です
- 共変型パラメーターは読み取り専用であり、書き込み可能ではありません
- 反変型パラメーターは書き込み専用ですが、読み取り可能ではありません