1. ホーム
  2. generics

[解決済み] なぜ例がコンパイルできないのか、別名、(co-, contra-, in-)分散はどのように機能するのか?

2022-05-03 04:34:35

質問

に続いて この質問 誰かScalaで次のことを説明してください。

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

という区別は理解できる。 +T T を使用するとコンパイルされます。 T ). しかし、では実際にどのようにして型パラメーターを共変させるクラスを書けばいいのでしょうか? ノンパラメトリック ? のインスタンスでしか作れないようにするにはどうしたらいいですか? T ?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

EDIT - は、以下のようになりました。

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

これはすべて良いのですが、1つだけ欲しい型パラメータが2つある状態です。このように再質問してみます。

を書くにはどうしたらいいでしょうか? 不滅の Slot というクラスがあります。 コバリアント の型は?

EDIT 2 : ウン!私は var であって val . 以下は私が望んだものです。

class Slot[+T] (val some: T) { 
}

解決方法は?

一般的に 共変量 型パラメータは、クラスがサブタイト化されるにつれて変化することが許されるものです(サブタイト化によって変化するので、"co" という接頭辞がつきます)。 より具体的には

trait List[+A]

List[Int] のサブタイプです。 List[AnyVal] なぜなら Int のサブタイプです。 AnyVal . のインスタンスを提供することができることを意味します。 List[Int] 型の値が List[AnyVal] が期待されます。 これはジェネリックが機能するための実に直感的な方法ですが、ミュータブルなデータが存在する場合に使用すると不健全(型システムを破壊する)であることが判明しています。 このため、Javaではジェネリクスは不変である。 Java の配列(誤って共変である)を使って、不健全さの簡単な例を示します。

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

型の値を割り当てただけです。 String 型の配列に Integer[] . 理由は明白ですが、これは悪い知らせです。 Java の型システムは、実はコンパイル時にこれを許してしまうのです。 JVMは、コンパイル中に ArrayStoreException 実行時に Scalaの型システムはこの問題を防いでいます なぜなら Array クラスは不変である(宣言は [A] よりも [+A] ).

と呼ばれる別のタイプの分散があることに注意してください。 反分散 . このことは、共分散がなぜ問題を引き起こすかを説明する上で非常に重要です。 共分散は文字通り共分散の反対で、パラメータが変化します。 上方 サブタイトリングで あまりに直感的でないため、あまり一般的ではありませんが、関数という非常に重要な用途があります。

trait Function1[-P, +R] {
  def apply(p: P): R
}

quoteに注目。 - の分散アノテーションです。 P 型パラメータを使用します。 この宣言は、全体として Function1 において反変数である。 P において共変であり R . したがって、以下の公理を導くことができる。

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

注目すべきは T1' のサブタイプ (または同じタイプ) でなければなりません。 T1 の場合はその逆です。 T2T2' . 英語では、次のように読むことができます。

ある機能 A は、他の関数のサブタイプ B のパラメータ型が A のパラメータ型のスーパータイプである。 B の戻り値の型は A の戻り値のサブタイプである。 B .

このルールの理由は、読者への練習として残しておきます(ヒント:先ほどの配列の例のように、関数がサブタイプ化されることで様々なケースが考えられるようになります)。

共変量と共変量についての新しい知識があれば、次の例がなぜコンパイルできないかわかるはずです。

trait List[+A] {
  def cons(hd: A): List[A]
}

問題は、その A は共変であるのに対し cons 関数は、その型パラメータが 不変量 . このように A は間違った方向に変化しています。 興味深いことに、この問題を解決するために List で共変動です。 A しかし、その場合、戻り値の型は List[A] は無効であるため cons 関数は、その戻り値の型が 共変量 .

ここで、唯一の選択肢は、a) A 不変で、共分散の直感的なサブタイプの特性を失うか、b) ローカルな型パラメータを cons メソッドで定義されます。 A を下限とする。

def cons[B >: A](v: B): List[B]

これで有効です。 想像してみてください。 A は下向きに変化していますが B に対して上向きに変化することができます。 A というのも A はその下界である。 このメソッド宣言で A は共変量であり、すべてがうまくいく。

のインスタンスを返した場合のみ、このトリックが機能することに注意してください。 List に特化したもので、より特殊性の低い型である B . を作ろうとすると List 型の値を代入しようとすることになるので、物事が破綻してしまいます。 B 型の変数に A これはコンパイラによって許可されない。 ミュータビリティがある場合は常に、ある種のミューテータが必要で、これは特定の型のメソッド・パラメータを必要とし、(アクセッサと一緒に)不変性を意味するものです。 共分散は不変データに対して機能します。なぜなら、唯一の可能な操作はアクセッサであり、アクセッサには共分散の戻り値の型が与えられる可能性があるからです。