

ジェネリッククラスの継承が思うようにいきません。  
一応、実装自体はできているのですが、コードを簡略化する方法を探しています。  
検索しようにも、自分がやりたい方法を言語化できなかったため、こちらで質問させていただくことにしました。

### 準備

[code]

    class Weapon { }
    class Bomb : Weapon { }
    
    class MachineBase<T> where T : Weapon { }
    class Machine : MachineBase<Bomb> { }
    
[/code]

このコードを、以下のように継承します。

### ①現状のコード

[code]

    class RobotBase<T1, T2> where T1 : MachineBase<T2> where T2 : Weapon { }
    class Robot : RobotBase<Machine, Bomb> { }
    
[/code]

実装自体はできていますが、クラスを継承するたびに `<T1, T2>` と型を2つ指定する手間がかかります。  
Machineクラスが、`ジェネリック型にWeaponクラスを指定していることは明確`です。  
そのため、型指定を1つで済ませる方法があるのではないかと思い、以下のように改変しました。

### ②改変したコード（エラー）

[code]

    class RobotBase<T> where T : MachineBase<Weapon> { }
    class Robot : RobotBase<Machine> { }    //エラー
    
[/code]

型引数を1つに減らしました。  
RobotBase<T>クラスには何かしらの型を指定する必要があるため、`型制約そのものであるWeaponをジェネリック型に指定しています。`  
すると、後述のエラーが出るようになりました。

### エラーメッセージ

[code]

    "エラー    CS0311  型 'Machine' はジェネリック型またはメソッド 'RobotBase<T>' 内で型パラメーター 'T' として使用できません。'Machine' から 'MachineBase<Weapon>' への暗黙的な参照変換がありません。    "
    
[/code]

`Machine`は`MachineBase<Bomb>`を継承し、`Bomb`は`Weapon`を継承しているため、RobotBaseの<T>が、  
T = Machine = MachineBase<Bomb> = MachineBase<Weapon>  
と解釈されるのかと思ったのですが、エラーとなってしまいます。

[code]

    class Robot : RobotBase<Machine> { }    //エラー
    
[/code]

エラーの原因である上記の文を、

[code]

    class Robot : RobotBase<MachineBase<Weapon>> { }
    
[/code]

と書き換えると、エラーは消えました。

[code]

    class Robot : RobotBase<MachineBase<Bomb>> { }    //エラー
    
[/code]

と書き換えると、再びエラーが出ます。

恐らく、RobotBase<T>を実装する際、`Weaponを型指定したことがエラーの原因`だと思います。  
エラーを回避するために、型引数の指定を派生クラスに丸投げしようとすると、①のコードのように、型引数の指定が2個になってしまいます。  
引数の実装を派生クラスに任せつつ、引数の数は増やさない記法はあるのでしょうか。

### 質問したいこと

①のような継承をする場合に、型引数を1つで済ませる方法、  
または、②でエラーを出さないようにする実装方法が知りたいです。  
どちらも不可能であれば、本文のコードを根本から改変することになっても構わないので、引数の指定をできるだけ抑えた継承の方法を教えていただきたいです。


CC BY-SA 4.0
2023-01-22T16:03:11.553
93512
2023-01-27T04:46:11.523
2023-01-27T04:46:11.523
3060
56714
question
c#
ジェネリッククラスの継承がうまくできない


`MachineBase`が型引数を持つジェネリッククラスだという点が複雑さを生んでいると思いますが、そもそも型引数は必要なのでしょうか？
`Weapon`のままでは扱えないのでしょうか？

* * *

現在のクラス構成では簡単化するのは難しいと思います。そこで視点を変えて

> クラスを継承するたびに<T1, T2>と型を2つ指定する手間がかかります。

どの辺りに手間がかかるのでしょうか？ 手間がかからなければ、型引数が `T1`、`T2`のままでも問題ないかと思います。  
例えばLINQで頻繁に使われる`Select`拡張メソッドは

[code]

    public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,int,TResult> selector);
    public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,TResult> selector);
    
[/code]

と非常に複雑な型引数を持っていますが、型引数はほぼ省略することができ「手間がかかる」と言われることはほとんどないかと思います。

クラス構成はそのままに、補助メソッドを用意するなど手間がかからない設計にするのはどうでしょうか？


CC BY-SA 4.0
2023-01-22T22:43:44.163
93513
2023-01-22T22:43:44.163
4236
93512
answer


解決にはならないのですが、なぜエラーになるのかです。

  * `RobotBase<T>`の型引数`T` に `Machine` を使おうとしていて、これは `MachineBase<Bomb>` を継承している
  * `RobotBase<T>`の型引数`T` は `MachineBase<Weapon>` を継承しろという制約

つまり、`MachineBase<Bomb>` は `MachineBase<Weapon>` を継承しているか という問題になります。  
「`MachineBase<Bomb>` 型のオブジェクトを
`MachineBase<Weapon>`型の変数に代入可能か」とも言い換え可能です。そしてこれはできません。

根源が同じなわかりやすい例がこちら(もちろんエラーになる)

[code]

    List<string> strs = new List<string>();
    List<object> objs = strs;
    obj.Add(1); 
    
[/code]

`List<T>`が`MathineBase<T>`に  
`Bomb`が`string`  
`Weapon`が`object`に対応しています。

`obj`と`strs`は同じリストを参照しているので、  
`List<object> objs = strs;` を合法とすると  
`obj.Add(1); `で `string`型しか入らないはずの `strs` に `int`型の値が入れられることになります。  
そんなことがあってはならないので、`C : P`という関係があるからと言って  
何の制限もなしに`A<C>`を`A<P>`として扱ってはならないのです。

実のところ型引数に`in`/`out`制限をつければ扱えますが、インターフェースとデリゲートにしか適用できません。


CC BY-SA 4.0
2023-01-23T08:02:16.467
93521
2023-01-23T08:02:16.467
13127
93512
answer


クラス名からの判断なので確実ではないですが、そもそもこの例では継承で解決するべきではないように見えます。  
WeaponとBombはis-
aの関係、MachineBaseとMachine、RobotBaseとRobotは抽象クラスとその具象化クラスということで、継承であっても問題なさそうです。  
しかしMachineBaseとWeapon、RobotBaseとMachineBaseはhas-
aの関係に見えるので、例えばコンストラクタで渡してメンバ変数に保持した方が良いのではないでしょうか。

もし本当にすべて継承で解決しなければならない理由があるのであれば、その理由を追記していただけるとよりよい回答が得られるかもしれません。

参考：[is-a](https://ja.wikipedia.org/wiki/Is-a)、[has-a](https://ja.wikipedia.org/wiki/Has-a)


CC BY-SA 4.0
2023-01-23T14:44:08.743
93530
2023-01-23T14:44:08.743
70
93512
answer


インターフェースで共通化すれば、とりあえずビルドは通りますが…  
正直な所、Bladean Mericleさんの回答に賛成で、もうちょっといい設計がありそうな気がします。

[code]

    interface IWeapon { }
    interface IMachine { }
    
    class Weapon : IWeapon { }
    class Bomb : Weapon { }
    
    class MachineBase<T> : IMachine where T : IWeapon { }
    class Machine : MachineBase<Bomb> { }
    
    class RobotBase<T> where T : IMachine { }
    class Robot : RobotBase<Machine> { }
    
[/code]


CC BY-SA 4.0
2023-01-25T02:21:36.933
93555
2023-01-26T00:33:36.087
2023-01-26T00:33:36.087
41943
41943
93512
answer


Bladean Mericleさんやradianさんの意見に賛成です。  
名前付けをもう少し具体的にするといいかもしれません。  
おそらく本当にやりたいことはこんな感じではないでしょうか？

[code]

    // 1. GameObject（衝突可能な存在）
    public class GameObject
    {
        void Init() { }
        void Move() { }
        void Draw() { }
    }
    public class Bomb : GameObject{ }
    
    // 2. Weapon (これ自身が衝突判定欲しい場合はGameObjectかも)
    public interface IWeapon
    {
        public GameObject? OnTrigger();
    }
    public class BombThrower : IWeapon
    {
        public GameObject? OnTrigger()
        {
            /* リロードやインターバルなど条件に問題がなければ */
            return new Bomb();
        }
    }
    
    // 3. Robotの腕
    public class RobotArm
    {
        public IWeapon? EquippedWeapon { get; protected set; }
    }
    public class BombThrowerEquippableArm : RobotArm
    {
        public BombThrowerEquippableArm(BombThrower weapon) { this.EquippedWeapon = weapon; }
    }
    
    // 4. Robot
    public abstract class RobotBase : GameObject
    {
        // public RobotBody? RobotBody { get; protected set; }
        public RobotArm? LeftArm { get; protected set; }
        public RobotArm? RightArm { get; protected set; }
    }
    public class Robot_TypeBombThrower : RobotBase
    {
        public Robot_TypeBombThrower(BombThrowerEquippableArm leftArm, RobotArm rightArm)
        {
            // this.RobotBody = robotBody;
            this.LeftArm = leftArm;
            this.RightArm = rightArm;
        }
    }
    
[/code]


CC BY-SA 4.0
2023-01-27T02:25:41.973
93579
2023-01-27T02:25:41.973
25396
93512
answer