web-dev-qa-db-ja.com

ジェネリック型制約により、暗黙の参照変換エラーが発生しないのはなぜですか?

アジェンダの予定を操作するためのインターフェイスとジェネリッククラスをいくつか作成しました。

interface IAppointment<T> where T : IAppointmentProperties
{
    T Properties { get; set; }
}

interface IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
    DateTime Date { get; set; }
    T Appointment { get; set; }
}

interface IAppointmentProperties 
{
    string Description { get; set; }
}

class Appointment<T> : IAppointment<T> where T : IAppointmentProperties
{
    public T Properties { get; set; }
}

class AppointmentEntry<T> : IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
    public DateTime Date { get; set; }
    public T Appointment { get; set; }
}

class AppointmentProperties : IAppointmentProperties
{
    public string Description { get; set; }
}

型パラメーターにいくつかの制約を使用して、有効な型のみを指定できるようにしています。ただし、TIAppointment<IAppointmentProperties>を実装する必要があることを定義する制約を指定すると、コンパイラはAppointment<AppointmentProperties>であるクラスを使用するときにエラーを返します。

class MyAppointment : Appointment<MyAppointmentProperties>
{
}

// This goes wrong:
class MyAppointmentEntry : AppointmentEntry<MyAppointment>
{
}

class MyAppointmentProperties : AppointmentProperties
{
    public string ExtraInformation { get; set; }
}

エラーは次のとおりです。

The type 'Example.MyAppointment' cannot be used as type parameter 'T' in the generic type or method 'Example.AppointmentEntry<T>'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment<Example.IAppointmentProperties>'.

なぜこれが機能しないのか説明できますか?

37
Rens

簡単にしましょう:

interface IAnimal { ... }
interface ICage<T> where T : IAnimal { void Enclose(T animal); } 
class Tiger : IAnimal { ... }
class Fish : IAnimal { ... }
class Cage<T>  : ICage<T> where T : IAnimal { ... }
ICage<IAnimal> cage = new Cage<Tiger>();

あなたの質問は、なぜ最後の行が違法なのですか?

コードを単純化するために書き直したので、明確なはずです。 ICage<IAnimal>は、あなたが置くことができるケージです任意の動物ですが、Cage<Tiger> can トラのみを保持なので、これは違法でなければなりません。

違法でなければ、これを行うことができます:

cage.Enclose(new Fish());

そしてねえ、あなたは魚をトラのケージに入れるだけです。

型システムは、変換を許可しません。これは、ソースタイプの機能がターゲットタイプの機能よりもlessであってはならないという規則に違反するためです。 (これは有名な「リスコフ置換原理」の形式です。)

より具体的には、ジェネリックを乱用していると言います。自分自身を分析するには複雑すぎる型関係を作成したという事実は、全体を単純化する必要があるという証拠です。すべての型の関係をまっすぐに維持しておらず、そのことを書いた場合、ユーザーも確実にそれをまっすぐに維持することはできません。

115
Eric Lippert

エリックによる既に非常に良い答えがあります。この機会を利用して、不変性共分散およびここで反分散

定義については、 https://docs.Microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravarianceを参照してください


動物園があるとしましょう。

abstract class Animal{}
abstract class Bird : Animal{}
abstract class Fish : Animal{}
class Dove : Bird{}
class Shark : Fish{}

動物園は移動しているため、動物を古い動物園から新しい動物園に移動する必要があります。

不変性

それらを移動する前に、動物を別の容器に入れる必要があります。コンテナはすべて同じ操作を行います。動物を入れたり、動物を取り出したりします。

interface IContainer<T> where T : Animal
{
    void Put(T t);
    T Get(int id);
}

明らかに魚には水槽が必要です。

class FishTank<T> : IContainer<T> where T : Fish
{
    public void Put(T t){}
    public T Get(int id){return default(T);}
}

したがって、魚を入れたり、水槽から出たりすることができます(うまくいけば生きている):

IContainer<Fish> fishTank = new FishTank<Fish>(); //Invariance, the two types have to be the same
fishTank.Put(new Shark());          
var fish = fishTank.Get(8);

私たちがallowedIContainer<Animal>に変更すると仮定すると、誤って鳩をタンクに入れることができます。

IContainer<Animal> fishTank = new FishTank<Fish>(); //Wrong, some animal can be killed
fishTank.Put(new Shark());
fishTank.Put(new Dove()); //Dove will be killed

反分散

効率を改善するために、Zoo管理チームはロードプロセスとアンロードプロセスを分離するようにしています(管理は常にこれを行います)。したがって、2つの別個の操作があり、1つはロード専用で、もう1つはアンロードです。

interface ILoad<in T> where T : Animal
{
    void Put(T t);
}

次に、鳥かごがあります。

class BirdCage<T> : ILoad<T> where T : Bird
{
    public void Put(T t)
    {
    }
}

ILoad<Bird> normalCage = new BirdCage<Bird>();
normalCage.Put(new Dove()); //accepts any type of birds

ILoad<Dove> doveCage = new BirdCage<Bird>();//Contravariance, Bird is less specific then Dove
doveCage.Put(new Dove()); //only accepts doves

共分散

新しい動物園には、動物を降ろすためのチームがあります。

interface IUnload<out T> where T : Animal
{
    IEnumerable<T> GetAll();
}

class UnloadTeam<T> : IUnload<T> where T : Animal
{
    public IEnumerable<T> GetAll()
    {
        return Enumerable.Empty<T>();
    }
}

IUnload<Animal> unloadTeam = new UnloadTeam<Bird>();//Covariance, since Bird is more specific then Animal
var animals = unloadTeam.GetAll();

チームの観点からは、中身がどうであっても、動物をコンテナから降ろすだけです。

14
Stephen Zeng

インターフェースではなく具象型を使用してMyAppointmentクラスを宣言したためです。次のように宣言する必要があります。

class MyAppointment : Appointment<IAppointmentProperties> {
}

これで、変換は暗黙的に発生する可能性があります。

AppointmentEntry<T>を制約where T: IAppointment<IAppointmentProperties>で宣言することにより、contractを作成します。これにより、AppointmentEntry<T>mustは、IAppointmentPropertiesで宣言された型に対応します。具象クラスで型を宣言することにより、その契約に違反しました(aIAppointmentPropertiesの型を実装しますが、anyタイプ)。

6
Peter Gluck

サンプルインターフェイスを以下から再定義すると機能します。

interface ICage<T>

interface ICage<out T>

outキーワードに注意してください)

次に、次の文が正しいです:

ICage<IAnimal> cage = new Cage<Tiger>();
0
Kaginawa

他の誰かにもこのエラーメッセージが表示される場合:異なる名前空間で同じインターフェイスが2回定義されており、リンクしようとしたクラスが同じインターフェイスを使用していません。

0
CitrusO2