为什么不能用派生类替换接口成员中的基类?

4

我无法创建一个能够与派生类一起工作的接口实现。我一直以为这是可能的,所以我很困惑为什么我不能这样做。如果我将BaseType更改为抽象类,也不可能实现。

public interface BaseType {}

public class FirstDerivedClass : BaseType {}

public class SecondDerivedClass : BaseType {}


public interface SomeInterface
{
    void Method(BaseType type);
}

public class Implementation : SomeInterface
{
    public void Method(FirstDerivedClass type) {}
}

'Implementation'没有实现接口成员'SomeInterface.Method(BaseType)'

为什么这是不可能的?我遇到了一个真实世界中很有用的情况。

例如,Selenium WebDriver有许多浏览器驱动程序。有一个抽象的DriverOptions类和它的派生类(ChromeOptionsFirefoxOptions等)。

这是我面临的一个真实世界问题的例子:

// Third party code - Selenium.
public abstract class DriverOptions {}
public class ChromeOptions : DriverOptions {}
public class FirefoxOptions : DriverOptions {}


// My code
public interface IWebDriverFactory
{
    IWebDriver GetWebDriver(DriverOptions options);
}

public class ChromeWebDriverFactory : IWebDriverFactory
{
    public IWebDriver GetWebDriver(ChromeOptions options)
    {
        // implementation details for initalising Chrome and use options from ChromeOptions
    }
}

public class FirefoxWebDriverFactory : IWebDriverFactory
{
    public IWebDriver GetWebDriver(FirefoxOptions options)
    {
        // implementation details for initalising Firefox and use options from FirefoxOptions
    }
}

1
covariance and contravariance,但你需要使用泛型来实现。 - Zohar Peled
你有一个例子吗? - user9993
2
那么 SomeInterface a = new Implementation(); a.Method(new SecondDerivedBaseType()); 怎么办?Implementation 如何处理这个问题?它只有 FirstDerivedBaseType 的实现。 - arekzyla
@user9993 泛型真的无法胜任。 - arekzyla
1
具体来说,当您决定实现接口时,您得到的合同规定,只要它们实现了BaseType,所有类型都是合法的。您的方法只接受其中一个子类,即使其他类型也可能实现该接口,但不能接受所有这些其他类型。因此,这不是技术限制,您所要求的并不合法,因为这毫无意义。 - Lasse V. Karlsen
显示剩余5条评论
4个回答

1
您可以使用泛型类型和逆变性来解决这个问题。您的代码应该如下所示:
    // Third party code - Selenium.
public abstract class DriverOptions { }
public class ChromeOptions : DriverOptions { }
public class FirefoxOptions : DriverOptions { }


// My code
public interface IWebDriverFactory<in T> where T:DriverOptions
{
    IWebDriver GetWebDriver(T options);
}

public class ChromeWebDriverFactory : IWebDriverFactory<ChromeOptions>
{
    public IWebDriver GetWebDriver(ChromeOptions options)
    {
        // implementation details for initalising Chrome and use options from ChromeOptions
    }
}

public class FirefoxWebDriverFactory : IWebDriverFactory<FirefoxOptions>
{
    public IWebDriver GetWebDriver(FirefoxOptions options)
    {
        // implementation details for initalising Firefox and use options from FirefoxOptions
    }
}

正如@TSmith建议的那样 -> 用法: var a = new FirefoxWebDriverFactory();


这是不可能的:IWebDriverFactory<DriverOptions> a = new FirefoxWebDriverFactory(); - arekzyla
为此,我可以创建一个没有泛型的接口IFirefoxWebDriverFactory,并在具体类中实现它。但这没有任何意义。OP想要基本接口和许多实现,他想要实现它的方式是不可能的。这应该可以工作:IWebDriverFactory<DriverOptions> a = new FirefoxWebDriverFactory();但它没有。 - arekzyla
1
我会在通用接口声明上加一个约束:(例如,public interface IWebDriverFactory<in T> where T: DriverOptions)。此外,在测试示例中考虑使用隐式类型推断来实现多态行为。(例如,var a = new FirefoxWebDriverFactory();) - TSmith
哦,我明白问题了。现在我不能这样做:interface Runner() { void Run(IWebDriver driver) }。我有一种印象,解决方案是放弃并通过每个实现的构造函数传递驱动程序选项。 - user9993
1
@user9993 记住,“a”是工厂变量,而不是IWebDriver实例。所以你需要这样做:runner.Run(a.GetWebDriver(new FirefoxOptions())); - TSmith
显示剩余5条评论

1
最终目标是能够针对任何给定的配置获取正确的工厂。 没有泛型
interface IFactory
{
    bool CanYouMakeIt(IConfig config);
    IMadeThis MakeIt(IConfig config);
}

您可以让所有工厂实现接口,然后将它们全部存储在 List<IFactory> 中。只需使用给定的配置实例查询它们并询问它们是否能够制造它。您也可以省略 CanYouMakeIt(),如果该工厂无法制造,则让 MakeIt 返回 null 即可。
class SampleFactory : IFactory
{
    public bool CanYouMakeIt(IConfig config) => config.GetType() == typeof(SampleConfig);
    public IMadeThis MakeIt(IConfig config) => new SampleMadeThis(config);
}

使用泛型

注意: IFactory 可以省略,但是将其存储在更紧密类型的列表中会更方便。

interface IFactory {}
interface IFactory<T> :IFactory where T : IConfig
{
    IMadeThis MakeIt(T config);
}

class SampleFactory : IFactory<SampleConfig>
{
    public IMadeThis MakeIt(SampleConfig config) => new SampleMadeThis(config);
}

现在调用部分比较棘手。像Autofac这样的DI框架可以使这个过程更加容易,但让我们看看我们该如何去做。
我这里没有进行任何错误检查...
class TheUltimateFactory
{
    private readonly IDictionary<Type,IFactory> _factories;

    public TheUltimateFactory(IFactory[] factories)
    {
        _factories = factories.ToDictionary(f => GetFactoryType(f.GetType()), f => f);
    }

    private Type GetFactoryType(Type type)
    {
        var genericInterface = type.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IFactory<>));
        return genericInterface.GetGenericArguments()[0];
    }

    public IMadeThis MakeIt(IConfig config)
    {
        var configType = config.GetType();
        var method = typeof(IFactory<>).MakeGenericType(configType).GetMethod("MakeIt");
        return (IMadeThis)method.Invoke(_factories[configType], new object[]{config});
    }
}

我并没有运行这段代码,只是从脑海中想出了这个例子,来说明如何使用反射。
使用后一种方法,你基本上是用动态调用代码来替换强类型的工厂定义。

这也是一种很酷的方法;它非常接近责任链模式(只需要让工厂顺着每个链接下落,而不是要求客户端去到下一个链接)。 - TSmith

1

解释

为什么这不可能?我遇到了一个实际情况,这将非常有用。

无论有用与否,这都是不合逻辑的。你部分违反了接口规定的合同。

为了向您展示为什么您的结论无效,让我们看一下我们采取的单独步骤。

public class Food {}
public class Meat :  Food {}
public class Poultry : Food {}
public class Tofu :  Food {}

到目前为止还没有什么不寻常的。

假设我有一家名为Chez Flater的餐厅。我想雇用厨师,因此我起草了一份合同,规定了我的厨师在工作时应该做什么:

public interface IChezFlaterChef
{
    void Cook(Food food);
}

注意这个接口定义了什么。用语言来表达这个契约:
如果你想成为IChezFlaterChef,你必须能够烹饪任何属于食物的东西。
public class VeganChef : IChezFlaterChef
{
    public void Cook(Tofu tofu) {}
}

你现在应该明白为什么这个类违反了契约。它想成为一个IChezFlaterChef,但实际上它并没有做我要求厨师做的所有工作。VeganChef只想烹饪豆腐。我不会雇用他。
你可能会回答:“但是我只有需要烹饪的豆腐。” 今天可能是正确的,但如果明天我决定重新添加肉类到菜单中呢?
以下代码应该始终正常工作:
IEnumerable<Food> myFood = GetSomeFoods();

foreach(var food in myFoods)
{
    var availableChef = FindAvailableChef();

    chef.Cook(food);
}

如果您按照自己的想法实现VeganChef,当他是可用的厨师并且您要求他做一些不是豆腐的菜时,这段代码可能会在意料之外的时间爆炸。

这是一个问题。我的餐厅将无法高效运作,因为我现在必须处理一个拒绝工作(=抛出异常)的厨师,而且我将不得不寻找第二个厨师。
我特别要求所有的厨师都能烹饪任何食物,因为我不想发生这种情况。或者我不应该期望我的厨师们能够做到这一点;或者我不应该雇佣素食主义者厨师。编译器指出我不应该雇佣这位厨师:

'Implementation'未实现接口成员'SomeInterface.Method(BaseType)'

相当于

[素食主义者厨师]没有做到预期的[烹饪任何食物]。

换句话说,编译器正在阻止您开设餐厅,因为它知道一旦餐厅开业,这位素食主义者厨师会给您带来麻烦。

当你在编写代码时,你无法知道在这个特定的餐厅中将使用哪些具体的厨师和食品,因为员工和菜单尚未确定(我们仍在开发餐厅本身)。
如果我写下这段代码:
public string GetEmployeeName(Employee e)
{
    return e.Name;
}

这取决于一个逻辑,即每个员工都保证有一个名字属性。

但你的VeganChef不能烹饪所有食物。由于我要求我的IChezFlaterChef能够烹饪任何食物,这意味着VeganChef不能被聘为IChezFlaterChef


解决方案

您当前的IChezFlaterChef合同并不正确:

如果您想成为IChezFlaterChef,您必须能够烹饪任何食品

更准确地说,您想要的表达方式应该是:

如果您想成为特定类型的IChezFlaterChef,则必须能够烹饪该特定类型的食品

作为餐厅老板,我已经改变了对于在我的餐厅里成为一名厨师所需的期望。现在,我愿意雇用只能烹饪一种特定食品的专家。

当一名厨师想成为一个IChezFlaterChef时,显而易见的第一个问题就是“为什么食品?”这些信息由泛型类型提供:

public interface IChezFlaterChef<TFood> where T : Food
{
    void Cook(TFood food);
}

注意 where T : Food。这确保您无法执行诸如创建 IChezFlaterChef<DateTime> 这样没有意义的操作。

public class VeganChef : IChezFlaterChef<Tofu>
{
    public void Cook(Tofu tofu) {}
}

现在这个类是有效的,因为它满足了IChezFlaterChef<TFood>定义的契约。

假设您想要一个可以烹饪两种食物的厨师。您可以这样做,但必须分别实现接口(每个接口使用不同的食品类型):

public class MeatAndPoultryChef : IChezFlaterChef<Meat>, IChezFlaterChef<Poultry>
{
    //Needed for IChezFlaterChef<Meat>
    public void Cook(Meat meat) {}

    //Needed for IChezFlaterChef<Poultry>
    public void Cook(Poultry poultry) {}
}

0

阅读问题

为什么派生类不能替换接口成员中的基类?

我正在解释为什么您不能这样做,而不是展示您可以以其他方式实现它。

根据您的契约接口 interface SomeInterface ,实现此接口的每个类都必须拥有一个方法 Method(BaseType type) ,该方法可以接受实现了 BaseType 的所有那些类的对象。

也就是说,实现了 BaseType 的所有这些类的对象都可以传递到 Method() 中。

但是在您的派生类 class Implementation : SomeInterface 中,当尝试覆盖该方法时,它只能接受 FirstDerivedClass 类型的对象。

这意味着,

如果有人创建另一个类,例如 Demo ,它实现了 BaseType ,则根据接口的方法,此类的对象也可以传递到 Method(BaseType type) 中,但是由于您的派生类 Implementation 只有接受 FirstDerivedClass 类型的方法,因此它将无法接受 Demo 的对象。

你的代码中已经有一个名为SecondDerivedClass的类(与Demo相同)。

所以它违反了契约。

在你的派生类Implementation中,方法应该接受实现了BaseType的所有类的对象。

简单地说,可以在Implementation的方法中接受BaseType类型的实例。

public void Method(Base type) {}而不是public void Method(FirstDerivedClass type) {}

在这个方法的第一行,你可以通过FirstDerivedClass obj = (FirstDerivedClass)type将类型从Base转换为FirstDerivedClass

这一行在编码时不需要动态生成,因为你知道应该在这里使用哪种类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接