协方差和逆变,编译时错误

时间:2012-10-07 17:19:50

标签: c# covariance contravariance

我一直在阅读协方差和逆变 - Wikipedia讨论以下内容:

  

假设您有一个代表一个人的班级。一个人可以看医生,所以这个班可能有一个方法虚无效的人::看(医生d)。现在假设您要创建Person类Child的子类。也就是说,孩子是一个人。然后,人们可能想要制作医生,儿科医生的子类。如果孩子只访问儿科医生,我们希望在类型系统中强制执行。然而,一个天真的实现失败:因为一个孩子是一个人,孩子::看(d)必须带任何医生,而不仅仅是一个儿科医生。

这是一个“天真的实现”:

public interface IDoctor
{
}

public interface IPerson
{
    void VisitDoctor(IDoctor doctor);
}

public class Adult : IPerson
{
    public void VisitDoctor(IDoctor doctor)
    {
        Console.WriteLine("Adult saw doctor of type: {0}", doctor.GetType().Name);
    }
}

public class Child : IPerson
{
    public void VisitDoctor(IDoctor doctor)
    {
        Console.WriteLine("Child saw doctor of type: {0}", doctor.GetType().Name);
    }
}

public class AdultDoctor : IDoctor
{
}

public class ChildDoctor : IDoctor
{
}

这些测试:

[Test]
public void AdultSeesDoctor()
{
    var adult = new Adult();
    adult.VisitDoctor(new AdultDoctor());
    adult.VisitDoctor(new ChildDoctor());  // <-- Would like this to fail
}

[Test]
public void ChildSeesDoctor()
{
    var child = new Child();
    child.VisitDoctor(new AdultDoctor());  // <-- Would like this to fail
    child.VisitDoctor(new ChildDoctor());
}

输出:

  

成人锯医生类型:AdultDoctor

     

成人锯医生类型:ChildDoctor

     

孩子见过类型的医生:AdultDoctor

     

孩子见过类型的医生:ChildDoctor

现在,我可以实现以下内容,如果成年人试图去看儿童医生,或者如果孩子试图去看望成年医生(抛出System.InvalidCastException),则会引发运行时错误:

public interface IVisitDoctors<T> where T : IDoctor
{
    void VisitDoctor(T doctor);
}

public class Child : IPerson
{
    private readonly ChildDoctorVisitor _cdv = new ChildDoctorVisitor();

    public void VisitDoctor(IDoctor doctor)
    {
        _cdv.VisitDoctor((ChildDoctor)doctor);
    }
}

public class Adult : IPerson
{
    private readonly AdultDoctorVisitor _adv = new AdultDoctorVisitor();

    public void VisitDoctor(IDoctor doctor)
    {
        _adv.VisitDoctor((AdultDoctor)doctor);
    }
}

您是否可以强制Adult类仅访问AdultDoctor类型的医生,以便在{{1}类型的医生处理时发生编译时错误被访问过(对ChildDoctor}类来说反之亦然?

2 个答案:

答案 0 :(得分:1)

您不需要co或contra-variance:

public interface IDoctor<TPatient> where T : IPerson<TPatient>
{
}

public interface IPerson<T> where T : IPerson<T>
{
    void VisitDoctor(IDoctor<T> doctor);
}

public class Adult : IPerson<Adult>
{
    void VisitDoctor(IDoctor<Adult> doctor) {  }
}

public class AdultDoctor : IDoctor<Adult>
{
}

现在以下内容将无法编译:

Adult a = new Adult();
a.VisitDoctor(new ChildDoctor());

虽然这样会:

Adult a = new Adult();
a.VisitDoctor(new AdultDoctor());

这称为curiously recurring template pattern。在这种情况下,它用于通过接口类型Adult获取具体实现者类型(IPerson)。这意味着IPerson可以访问的医生类型可以限制为与实施者相同的类型。

您也可以在Java Enum class中看到它。 compareTo方法允许您比较枚举,但需要定期模板以确保您只能比较相同类型的枚举。

然而,它非常难看,所以您可能需要考虑将您的设计更改为:

public interface IDoctor<TPatient>
{
    void SeePatient(TPatient patient);
}

public interface IAppointments<T>
{
    void MakeAppointment(T patient, IDoctor<T> doctor);
}

因此,您可以不再需要IPerson来获取类型参数。

答案 1 :(得分:1)

我明白你想要达到的目标。但是如果你继续使用这个界面:

public interface IPerson
{
    void VisitDoctor(IDoctor doctor);
}

然后对实施者的任何进一步限制将打破 Liskov principle 。而这很可能会在以后对您的代码造成一些问题。

您可以做的是将该限制放入您的界面声明

public interface IPerson<TDoctor>
    where TDoctor : class, IDoctor...
{
    void VisitDoctor(TDoctor doctor);
}