4

这是一个多么棒的网站,多年来我一直潜伏在这里阅读其他问题,但现在我有一个自己的问题。

我的同事写了一门很像下面的课。我一看到它就知道它行不通,但我无法向他解释为什么它行不通。

他在将其声明为 a 时所期望的ControlItem<Button>是,在使用基调用 Draw() 时会调用 Draw(Button) 方法。相反,我们总是最终抛出异常。

这是协方差问题吗?

public abstract class ControlItem
{
    public ControlItem()
    {
    }

    abstract public void Draw();
}

public class ControlItem<T> : ControlItem where T : Control, new()
{
    public T MyControl { get; set; }

    private ControlItem()
    {       }

    public ControlItem(T control)
        : base()
    {
        MyControl = control;
    }

    public override void Draw()
    {
        Draw(this.MyControl);
    }

    public void Draw(Control cntrl)
    {
        throw new NotImplementedException();
    }

    public void Draw(Button button)
    {
        //Do some work
    }
}
4

3 回答 3

4

这是协方差问题吗?

不,这是静态与动态调度问题。静态调度意味着重载的方法调用在编译时根据传入的变量类型绑定到适当的类型:

class Base { }
class Derived : Base { }

class Foo
{
    void Test()
    {
        Base a = new Base();
        Overload(a);    // prints "base"

        Derived b = new Derived();
        Overload(b);    // prints "derived"

        // dispatched based on c's declared type!
        Base c = new Derived();
        Overload(c);    // prints "base"
    }

    void Overload(Base obj)    { Console.WriteLine("base"); }
    void Overload(Derived obj) { Console.WriteLine("derived"); }
}

动态分派意味着函数在运行时根据存储在变量中的对象的实际类型进行绑定:

class Base
{
    public virtual void Override() { Console.WriteLine("base"); }
}

class Derived : Base
{
    public override void Override() { Console.WriteLine("derived"); }
}

class Foo
{
    void Test()
    {
        Base a = new Base();
        a.Override();   // prints "base"

        Derived b = new Derived();
        b.Override();    // prints "derived"

        // dynamically dispatched based type of object stored in c!
        Base c = new Derived();
        c.Override();    // prints "derived"
    }

    void Overload(Base obj) { Console.WriteLine("base"); }
    void Overload(Derived obj) { Console.WriteLine("derived"); }
}

最后的打印显示了两者之间的差异。C# 和大多数基于类的 OOP 语言一样,只支持对this隐式参数进行动态分派(称为“单次分派”)。换句话说,重写的方法是动态分派的,而重载的方法则不是。

在单一调度语言中伪造多个调度的典型解决方案是使用访问者模式,这对你有用。

于 2009-04-08T15:52:02.007 回答
2

这是因为编译器只知道该类型将是一个控件,所以它总是会绑定到带有 Control 参数的方法。如果您需要以不同方式处理它们,则需要在 Draw() 方法中添加显式检查:

public override void Draw() {
   Button btn = MyControl as Button;
   if (btn != null) {
      Draw(btn);
   } else {
      Draw(this.MyControl);
   }
}

请注意,这不是很“通用”......但它可能会在您的特殊情况下起到作用。

于 2009-04-08T15:40:27.370 回答
2

建立在慷慨的回答:与 C++ 模板不同,C# 泛型不在编译时实例化。C# 编译器为泛型类型生成的代码与您在代码中使用的特化完全无关。编译器会输出一段代码,该代码适用于满足约束的类型参数的任何替换。直到运行时,当一个完全指定的泛型类型的实例被实例化时,JIT 编译器才会创建特定于您的类型参数的代码。

由于生成的代码适用于满足约束条件的任何内容,因此 C# 编译器将您的MyControl成员视为类型Control(not T) 的变量,因为它可以从约束中推断出尽可能多的变量。由于编译器必须为类发出通用代码,它必须根据它所知道的来选择调用哪个方法,并且由于它不能确定在运行时是否MyControl会是 a Button,它必须选择Draw(Control).

于 2009-04-08T16:25:13.987 回答