从协变接口派生时,协变会丢失

Covariance gets lost when deriving from covariant interface

在我们的 C# .NET WinForm 应用程序中,我们有一个名为 ControlWithLabel 的自定义控件。我想通过模板将其增强到 ControlWithLabel<TControl>。问题是,我们有数百个像 if (something is ControlWithLabel) 这样的检查,并且被测试的对象可以是多个派生类型(TextBoxWithLabel、ComboBoxWithLabel 等)。我怎样才能将它转换为模板解决方案,而无需重写每张支票并将其与每一种可能性相乘,例如 if (something is ControlWithLabel<TextBox>) || (something is ControlWithLabel<ComboBox>) || ... etc ... ?

我尝试使用协变接口,但它没有像我预期的那样工作。派生到通用非模板接口时,接口的协变性会丢失。

public class ControlWithLabel<TControl> : IControlWithLabel<TControl> where TControl : Control, new()
{
    public ControlWithLabel()
    {
        this.Control = new TControl();
        this.Label = new Label();
    }

    public Label Label
    {
        get;
        private set;
    }

    public TControl Control
    {
        get;
        private set;
    }
}

public class ControlWithLabel : ControlWithLabel<Control>, IControlWithLabel
{
}

public interface IControlWithLabel<out TControl> where TControl : Control
{
    Label Label
    {
        get;
    }

    TControl Control
    {
        get;
    }
}

public interface IControlWithLabel : IControlWithLabel<Control>
{
}

public class TextBoxWithLabel : ControlWithLabel<TextBox>
{
    public void SpecialMethodForTextBox()
    {
        // Special code ...
    }
}

public partial class FormMain : Form
{
    public FormMain()
    {
        InitializeComponent();
    }

    private void _buttonTest_Click(object sender, EventArgs e)
    {
        TextBoxWithLabel textBoxWithLabel = new TextBoxWithLabel();

        // this works, but then I need to rewrite and multiply every check
        if (textBoxWithLabel is ControlWithLabel<TextBox>)
            MessageBox.Show("textBoxWithLabel is ControlWithLabel<TextBox>");

        // this is not working, since classes cannot be covariant
        if (textBoxWithLabel is ControlWithLabel<Control>)
            MessageBox.Show("textBoxWithLabel is ControlWithLabel<Control>");

        // this is not working at all
        if (textBoxWithLabel is ControlWithLabel)
            MessageBox.Show("textBoxWithLabel is ControlWithLabel");

        // this works, but then I need to rewrite and multiply every check
        if (textBoxWithLabel is IControlWithLabel<TextBox>)
            MessageBox.Show("textBoxWithLabel is IControlWithLabel<TextBox>");

        // this works, but then I need to rewrite every check
        if (textBoxWithLabel is IControlWithLabel<Control>)
            MessageBox.Show("textBoxWithLabel is IControlWithLabel<Control>");

        // this is not working - covariance is lost!! Why?
        if (textBoxWithLabel is IControlWithLabel)
            MessageBox.Show("textBoxWithLabel is IControlWithLabel");
    }
}

我应该怎么做才能普遍使用 if (something is ControlWithLabel)if (something is IControlWithLabel) 而不是 if (something is IControlWithLabel<Control>)

您的接口继承是向后的。创建 interface IControlWithLabel,然后让 IControlWithLabel<T> 继承自 IControlWithLabel。那么 IControlWithLabel<T> 的所有实例也是 IControlWithLabel.

您的实现需要像这样继承通用接口和非通用接口:

public class TextBoxWithLabel : ControlWithLabel<TextBox>
{
    public void SpecialMethodForTextBox()
    {
        // Special code ...
    }
}

public class ControlWithLabel<TControl> 
    IControlWithLabel, 
    IControlWithLabel<TControl> where TControl : Control, new()
{
    public ControlWithLabel()
    {
        Control = new TControl();
        Label = new Label();
    }

    public Label Label { get; private set; }

    public TControl Control { get; private set; }
}

public interface IControlWithLabel<out TControl> where TControl : Control
{
    Label Label { get; }

    TControl Control { get; }
}

public interface IControlWithLabel
{
    Label Label { get; }
}

那么您应该可以做到以下几点:

var textBoxWithLabel = new TextBoxWithLabel();

// This will work just fine
if (textBoxWithLabel is IControlWithLabel)
{
    MessageBox.Show("textBoxWithLabel is IControlWithLabel");
}

唯一的问题是您将丢失 .Control 属性,除非您检查 is IControlWithLabel<TextBox>

除非我遗漏了某些要求,否则您不需要协方差?

public interface IControlWithLabel
{
    Label Label { get; }
}

public interface IControlWithLabel<TControl> : IControlWithLabel where TControl : Control, new()
{  
    TControl Control { get;}
}

//never instantiated, common base
public abstract class ControlWithLabel : IControlWithLabel
{
}

public class ControlWithLabel<TControl> : ControlWithLabel, IControlWithLabel<TControl>
{
}

感谢所有其他答案,我终于找到了解决方案。 None 的答案使得使用一般的 ControlWithLabel.ControlIControlWithLabel.Control 成为可能。所以这是我的解决方案:

public interface IControlWithLabel
{
    Label Label
    {
        get;
    }

    Control Control
    {
        get;
    }
}

/// <summary>
/// Here we use covariance
/// </summary>
public interface IControlWithLabel<out TControl> : IControlWithLabel where TControl : Control, new()
{
    new TControl Control
    {
        get;
    }
}

/// <summary>
/// Common base, never instantiated
/// </summary>
public abstract class ControlWithLabel : IControlWithLabel
{
    protected Control _control;

    public ControlWithLabel()
    {
        this.Label = new Label();
    }

    public Label Label
    {
        get;
        private set;
    }

    /// <summary>
    /// This property cannot be marked as 'abstract', because we want to change the return type in descendants
    /// </summary>
    public virtual Control Control
    {
        get
        {
            return _control;
        }
    }
}

public class ControlWithLabel<TControl> : ControlWithLabel, IControlWithLabel<TControl> where TControl : Control, new()
{
    public ControlWithLabel() : base()
    {
        this.Control = new TControl();
    }

    /// <summary>
    /// We cannot use 'override', since we want to return TControl instead of Control
    /// </summary>
    public new TControl Control
    {
        get
        {
            return _control as TControl;

            // This will return null if _control is not TControl.
            // This can happen, when we make an explicit cast for example TextBoxWithLabel to ComboBoxWithLabel, which requires an explicit conversion operator implementation.
            // In such case there can be still used general ControlWithLabel.Control, which always will be "not null" - for example ((ControlWithLabel)someObject).Control
            // (the general ControlWithLabel.Control will always be "not null", because the base class ControlWithLabel is marked as abstract and current class ControlWithLabel<TControl> creates the control in the constructor).
        }

        private set
        {
            _control = value;
        }
    }
}

public class TextBoxWithLabel : ControlWithLabel<TextBox>
{
    public void SpecialMethodForTextBox()
    {
        // Special code ...
    }
}

public partial class FormMain : Form
{
    public FormMain()
    {
        InitializeComponent();
    }

    private void _buttonTest_Click(object sender, EventArgs e)
    {
        TextBoxWithLabel textBoxWithLabel = new TextBoxWithLabel();

        // This works
        if (textBoxWithLabel is ControlWithLabel<TextBox>)
        {
            // We can use the general ControlWithLabel.Control
            if (((ControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is ControlWithLabel<TextBox>");
        }

        // This is not working, since classes cannot be covariant
        if (textBoxWithLabel is ControlWithLabel<Control>)
        {
            // We can use the general ControlWithLabel.Control
            if (((ControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is ControlWithLabel<Control>");
        }

        // This works!
        if (textBoxWithLabel is ControlWithLabel)
        {
            // We can use the general ControlWithLabel.Control
            if (((ControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is ControlWithLabel");
        }

        // This works
        if (textBoxWithLabel is IControlWithLabel<TextBox>)
        {
            // We can use the general INTERFACE property IControlWithLabel.Control
            if (((IControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is IControlWithLabel<TextBox>");
        }

        // This works thanks to COVARIANCE
        if (textBoxWithLabel is IControlWithLabel<Control>)
        {
            // We can use the general INTERFACE property IControlWithLabel.Control
            if (((IControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is IControlWithLabel<Control>");
        }

        // This works!
        if (textBoxWithLabel is IControlWithLabel)
        {
            // We can use the general INTERFACE property IControlWithLabel.Control
            if (((IControlWithLabel)textBoxWithLabel).Control != null)
                MessageBox.Show("textBoxWithLabel is IControlWithLabel");
        }
    }
}

在这种情况下并不真正需要协方差,但有可能检查 if (textBoxWithLabel is IControlWithLabel<Control>) 是件好事。它在某些情况下很有用。

此解决方案已经过测试并且有效。