プログラミングバリエーション

共分散と反分散の概念を理解しようとして眠れませんか? 彼らがあなたの背中でどのように呼吸するかを感じてください、しかしあなたが振り向くとき、あなたは何も見つけませんか? 解決策があります!


私の名前はニキータです。そして今日、私たちは頭の中のメカニズムが正しく働くようにします。 バリエーションのトピックに関する最もアクセスしやすい考慮事項は、例にあります。 猫へようこそ。


ブリーフィング


この投稿のバリエーションは、プログラミング言語に関係なく理解されます。 練習セクションの例は疑似言語で書かれており(奇跡的にC#に似ていることが判明しているため)、お気に入りのコンパイラーでコンパイルする必要はありません。 始めましょう。


用語のコツ


ドキュメント、技術文献、およびその他のソースでは、変動現象の異なる名前に出くわすことがあります。 これ以上怖がったり混乱したりしません。


共分散と共分散という用語は同等です(少なくともプログラミングでは)。 さらに、反分散と反分散という用語も同等です。 たとえば、共分散と反分散という用語は、WikipediaとTroelsen(翻訳中)で使用されています。 また、共分散と反変という用語は、たとえばMSDNおよびSkeet(翻訳中)にあります。


英語では、すべて共分散と反分散です。


理論


バリアント -ソース型の継承を派生型に転送します。 派生型とは、コンテナ、デリゲート、一般化を意味し、祖先と子の関係に関連付けられた型ではないと理解されます。 さまざまなタイプの変動は、共分散、反分散、不変性です。


共分散 -ソース型の継承を派生型に直接の順序で転送します。
反分散 -ソースタイプの継承を逆の順序で派生タイプに転送します。
不変性は、元の型の継承が派生物に転送されない状況です。


派生型が共分散を示す場合、元の型と共変であると言われます。 派生型が反変の場合、元の型と反変であると言われます。 派生型がどちらか一方を観察しない場合、それらは不変であると言われます。


あなたが知る必要があるのはそれだけです。 もちろん、最初に差異に遭遇した人が理解することは困難です。 したがって、特定の例を検討します。


練習する


これは何のためですか?

可変性の全体的なポイントは、派生型の継承を利用することです。 2つの型が祖先と子の関係によって関連付けられている場合、子孫オブジェクトは祖先型変数に格納できることが知られています。 実際には、これは、祖先オブジェクトの代わりに、一部の操作に子孫オブジェクトを使用できることを意味します。 したがって、より柔軟で短いコードを記述して、共通の祖先を持つ異なる子孫によってサポートされるアクションを実行することができます。


ソース階層と派生型

まず、操作する型階層について説明します。 階層の最上部にはDevice (デバイス)があり、その子孫はMouse (マウス)、 Keyboard (キーボード)です。 次に、 Mouseも子孫がありますWiredMouse (有線マウス)、 WirelessMouse (無線マウス)。



誰もがコンテナが大好きです。 それらの例を使用すると、派生型の意味を説明するのが最も簡単です。 リストを派生型として説明する場合、 Device型の派生物は
List<Device>List<Device>リスト)。 同様に、 Keyboardタイプの場合、 List<Keyboard>派生物です。 疑問があれば、今は消えていると思います。


古典的な共分散

共分散は、コンテナを使用すると簡単に学習できます。 これを行うには、階層の一部(ブランチ)- Keyboard : Device (キーボードはKeyboard : Device 、キーボードはデバイスの特殊なケース)を選択します。 繰り返しますが、リストを取得して共変派生ブランチを作成します-List List<Keyboard> : List<Device> (キーボードのリストはデバイスのリストの特殊なケースです)。 ご覧のとおり、継承は直接の順序で転送されました。



コード例を考えてみましょう。 デバイスList<Device>リストを取得し、それらに対して何らかの操作を行う関数があります。 ご想像のとおり、キーボードList<Keyboard>をこの関数に渡すことができます。


 void DoSmthWithDevices(List<Device> devices) { /*     */ } ... List<Keyboard> keyboards = new List<Keyboard> { /*   */ }; DoSmthWithDevices(keyboards); 

古典的反分散

反分散の研究のための正準は、代表に基づいて考慮されることです。 汎用デリゲートがあるとしましょう:


 delegate void Action<T>(T something); 

初期タイプのDevice派生物はAction<Device>になり、 Keyboard場合、 Action<Keyboard>ます。 受信したデリゲートは、それぞれデバイスまたはマウスで何らかのアクションを実行する機能を表すことができます。 Keyboard : Deviceブランチの場合、派生反変ブランチを作成します- Action<Device> : Action<Keyboard> (デバイス上のアクションはキーボード上のアクションの特殊なケースです-奇妙に聞こえますが、そうです)。 キーボードのキーを押すことができる場合、これはデバイスで押すことができるという意味ではありません(キーが何であるかがわからない場合があります)。 ただし、デバイスを接続できる場合は、キーボードを同じ方法(メソッド、機能)で接続できます。 ご覧のとおり、継承は逆の順序で転送されました。



上記から、機能がデバイス上で何かを実行できる場合、それはキーボード上でも同様に実行できることが論理的です。 つまり、 Action<Device>デリゲートオブジェクトを、 Action<Keyboard>デリゲートオブジェクトを受け入れる関数に渡すことができます。 コードで検討してください:


 void DoSmthWithKeyboard(Action<Keyboard> actionWithKeyboard) { /*  actionWithKeyboard   */ } ... Action<Device> actionWithDevice = device => device.PlugIn(); DoSmthWithKeyboard(actionWithDevice); 

少しの不変性

派生型が元の型に対して不変である場合、 Keyboard : Deviceブランチの場合、共変( List<Keyboard> : List<Device> )ブランチも反変種( Action<Device> : Action<Keyboard> )ブランチも形成されません。 これは、派生型間に関係がないことを意味します。 ご覧のとおり、継承は転送されません。



もしもし?


明らかでない共分散

タイプAction<T>デリゲートは共変である場合があります。 つまり、 Keyboard : Deviceブランチの場合、共変ブランチが形成されます- Action<Keyboard> : Action<Device> 。 したがって、 Action<Keyboard>デリゲートオブジェクトは、 Action<Device>デリゲートオブジェクトを受け入れる関数に渡すことができます。


 void DoSmthWithDevice(Action<Device> actionWithDevice) { /*  actionWithDevice   */ } ... Action<Keyboard> actionWithKeyboard = keyboard => ((Device)keyboard).PlugIn(); DoSmthWithDevice(actionWithKeyboard); 

明白な反変

コンテナは反変の場合があります。 つまり、 Keyboard : Deviceブランチの場合、反変ブランチが形成されます- List<Device> : List<Keyboard> 。 したがって、 List<Keyboard>関数では、 List<Device>渡すことができます。


 void FillListWithKeyboards(List<Keyboard> keyboards) { /*    */ } ... List<Devices> devices = new List<Devices>(); FillListWithKeyboards(devices); 

神聖な意味

上記で検討したエキゾチックな品種には、おそらく学術的価値があります。 実際の問題を見つけることは困難です。そのような機会があれば解決するのは簡単です。 共分散と反分散が実行時エラーを引き起こす可能性があることを覚えておく価値があります。 それらを排除するには、特定の制限を導入する必要があります。 コンパイラは、原則として、そのような制限を導入しません。


コンテナのセキュリティ

派生型が共変である場合、コンテナーは読み取り専用にして、セキュリティを確保する必要があります。 そうしないと、 List<Device>キャストすることにより、間違ったタイプのオブジェクト( DeviceMouseなど)をList<Keyboard>に書き込むことが可能になります。


 List<Device> devices = new List<Keyboard>(); devices.Add(new Device()); //    

派生型が反変の場合、セキュリティ上の理由から、コンテナは書き込み専用にする必要があります。 そうでない場合、対応するリスト( List<Keyboard>List<Mouse>など)にキャストすることにより、 List<Device>から間違ったタイプ( KeyboardMouseなど)のオブジェクトを読み取ることが可能です。


 List<Keyboard> keyboards = new List<Device>(); keyboards.Add(new Keyboard()); keyboards[0].PressSpace(); //    

代議員のための二重基準

デリゲートに適しているのは、出力値の共分散と入力パラメーターの反分散です(参照渡しを除く)。 これらの条件が満たされている場合、ランタイムエラーは発生しません。


デブリーフィング


提示された例は、分散の原理を理解するのに十分です。 対応する仕様でお気に入りの言語のさまざまなタイプのサポートに関するデータを探してください。 何か問題が発生した場合-目を閉じて、息を吐き、お茶を飲みます。 その後、再試行してください。 ご清聴ありがとうございました。


UDP


おそらく、バリエーションのより正確な定義がEric Lippertによって提案されています。 記事へのリンク提供してくれたAlex_sikに感謝します


割り当ての互換性は、よりプライベートな型の値を、より一般的な型の互換性のある変数に割り当てる機能です。
バリアントとは、派生型に対するソース型の割り当ての互換性を保持することです。
共分散とは、導関数のソースタイプの割り当ての互換性を直接の順序で保持することです。
反分散とは、元の型の割り当ての互換性を逆順で導関数に保持することです。



Source: https://habr.com/ru/post/J218753/


All Articles