C#泛型:无法将“具体类”转换为“接口”错误。

6
尝试生成一个通用的过滤器队列,该队列将应用于图像(在此示例中,它使用OpenCVSharp.GaussianBlur过滤器,但我要让它通用,以便可以插入我创建的任何自定义过滤器)。
我在C#泛型方面遇到了一些困难,智能感知显示:

无法从'GaussianBlur'转换为'IFilter'

Intellisense建议更改以下行: filters.Enqueue(filter); 通过对接口进行强制类型转换: filters.Enqueue((IFilter<IFilterParams>)filter); 然而,我的问题是:为什么需要进行强制类型转换,当具体类实现了接口并且按照泛型定义需要它,或者我是否误解了如何使用泛型声明类。
目前的实现代码如下:
public class FilterTest
{
    private FilterCollection filters = new FilterCollection();

    /* ... other irrelevant code ... */

    public void ApplyFilters(ref Mat buffer)
    {

        var filter = new GaussianBlur(new GaussianBlurParams { KernelSize = new Size(6, 6) });
        filters.Enqueue((IFilter<IFilterParams>)filter);
        filters.Apply(ref buffer);

    }
}

我正在为FilterCollection扩展Queue<>类:

public class FilterCollection : Queue<IFilter<IFilterParams>>
{

    public void Apply(ref Mat buffer)
    {
        while (Count > 0)
            Dequeue().Apply(ref buffer);
    }

}

IFilter和IFilterParams的接口如下:

public interface IFilter<T> where T : IFilterParams
{
    void Apply(ref Mat buffer);
}

public interface IFilterParams { }

接下来是示例过滤器的实现(在这种情况下更多是一个包装器):

public class GaussianBlurParams : IFilterParams
{
    public Size KernelSize = new Size(5, 5);
    public double SigmaX = default(double);
    public double SigmaY = default(double);
    public BorderTypes BorderType = BorderTypes.Default;
}

public class GaussianBlur : IFilter<GaussianBlurParams>
{
    private GaussianBlurParams p;

    public GaussianBlur(GaussianBlurParams filterParams)
    {
        this.p = filterParams;
    }

    public void Apply(ref Mat buffer)
    {
        Cv2.GaussianBlur(buffer, buffer, p.KernelSize, p.SigmaX, p.SigmaY, p.BorderType);
    }
}

所以,假设:
  • GaussianBlur 实现了 IFilter<GaussianBlurParams>
  • 并且 IFilter<T> where T : IFilterParams
  • 而且 GaussianBlurParams 实现了 IFilterParams
转换是唯一修复此问题的方法吗?还是泛型类/接口的结构存在问题?

3
这里的基本问题是一个“正方形”是一种“形状”,但一个“形状持有者<正方形>”不是一个“形状持有者<形状>”。而且这个问题不仅仅是纠结于语言问题,特别是在集合中。我认为会有人提供更详细的解答,针对你的特定代码,但同时你也可以了解一下“协变性”和“逆变性”。 - zzxyz
1
顺便提一下,通常最好避免从集合类继承,而是使用组合。 - juharr
哇,我以前从未见过评论和答案被添加然后迅速删除。这让我很难在回复中添加自己的评论。 - Aaron Murray
1
为什么IFilter接口是泛型的?你在那里没有使用T。 - Evk
1
好的,但是在你的新方法中,我仍然不明白为什么AbstractFilter是泛型的。你可以将其变为非泛型,删除它的构造函数,似乎不会有任何有价值的东西丢失(实际上你可以直接删除整个类)。除非还有更多未显示的方法。 - Evk
显示剩余2条评论
2个回答

4
您的代码中有多个方面交织在一起,这些方面共同使得设计不够优化。乍一看,这可能看起来像是协变的问题,但是仔细观察后发现并非如此。这里的两个主要方面是通用约束和接口。为了理解我的意思,让我们看一下这两种语言元素的一些好处。 通用约束 虽然通用约束使您能够以类型安全的方式使用模式的实现多个类型,但您不能直接操作泛型类中的T类型对象。您无法创建实例,也无法依赖实例是引用类型还是值类型(尝试与null进行比较即可了解其含义),也无法访问除System.Object中定义的成员之外的任何其他成员。因此,您可以使用通用约束允许泛型类内部的代码对T类型的对象执行其他操作,例如创建实例(使用new()约束)或访问其他成员(通过将T约束为特定类型和/或一个或多个接口)。 接口 接口提供了一个合同保证,保证实现者将拥有一组定义好的成员。这个保证是针对接口的消费者而不是实现者的。这意味着您不使用接口来强制其实现者提供对接口消费者没有任何价值的成员。 在您的情况下意味着什么 您问题的关键在于代码的这一部分:
public interface IFilter<T> where T : IFilterParams
{
    void Apply(ref Mat buffer);
}

public interface IFilterParams { }

特别地:
  1. 你定义了泛型约束where T : IFilterParams,但是IFilterParams没有提供任何成员。这个约束对你的设计没有任何价值。你将实现者限制在某个T上,但是你并没有从中获得任何东西,因为你不能做任何事情来处理T的实例,这些事情在没有约束的情况下也可以完成。

  2. 更进一步地说,你甚至不需要接口是泛型的。你甚至在接口提供的唯一成员中都没有使用T。就接口的保证而言,你完全可以没有它。

  3. 看一下GaussianBlur实现IFilter<T>,很明显你只在构造函数中使用GaussianBlurParams,而构造函数不是接口的一部分。所以你只是利用接口的约束来限制实现者使用一个实现IFilterParamsParams类。这甚至不是一个真正的限制,因为实现者可以使用任何其他参数类进行初始化。但主要违反了接口向其使用者提供保证而不是限制其实现者的原则。

综上所述,你可以简单地采用...

public interface IFilter
{
    void Apply(ref Mat buffer);
}

...并且您已经避免了所有面临的问题。

即使您需要使用约束where T : IFilterParams来满足接口的另一个消费者(也许有另一个接口成员在您的示例中未添加),您的FilterCollection不需要此约束。因此,您仍然可以保留非泛型的IFilter,并提供另一个接口(可能继承自IFilter,也可能不继承),该接口提供其他功能。


感谢您清晰的解释,其中一些我已经应用到我的回答/代码重构中,但是您的解释绝对比我好。我的逻辑最大的问题在于队列需要一组相同“类型”的对象,所以我使用接口来包装该类型(当然在IFilter的合同中我需要实现Apply,所以那就是唯一公开的方法),但是每个IFilter实现都需要自己的一组参数,所以我的错误是为了将界面添加到要交给队列的参数中,现在我明白为什么那是错误的了。 - Aaron Murray
原始问题是要求解释而不是正确的代码,你的解释比我在答案中做得更好,所以我将你的答案标记为正确答案(我的答案仍可用于代码纠正)。 - Aaron Murray

2

好的,感谢@zzxyz的原始评论以及添加并很快删除的评论和答案,让我更深入地研究了协变性(我通过添加通用的IFilterParams来避免协变性),并在SO中找到了答案/评论(逆变性?协变性?这个通用架构有什么问题...),帮助我纠正了我的问题,并更好地组织了代码。

现在我明白自己当时是试图“向一个碗(水果)中加入香蕉(一种水果)(水果是协变的,因为它不仅仅是一种水果类型),而我需要将香蕉加入到一个碗(香蕉)中”。我很不好地总结了其中一个不幸被删除的答案。

在研究过程中,我能够通过创建一个具有自己通用类型的抽象类来消除协变性,并完全删除IFilterParams接口,因此所有过滤器都必须实现基本的抽象类,现在不再引起协变性。

既然我现在理解了,但还不能清楚地解释(上面),修订后的代码(下面)可能会更好地解释。

首先,FilterTest类不需要进行任何更改(除了从原始示例中删除强制转换,这是问题的重点):

public class FilterTest
{
    private FilterCollection filters = new FilterCollection();

    public void ApplyFilters(ref Mat buffer)
    {

        var filter = new GaussianBlur(new GaussianBlurParams { KernelSize = new Size(6, 6) });
        filters.Enqueue(filter);
        filters.Apply(ref buffer);

    }
}

接下来,调整了队列使其不具有协变性(仅实现一个“类型”IFilter),从而公开所需的“Apply”方法。

public class FilterCollection : Queue<IFilter>
{

    public void Apply(ref Mat buffer)
    {
        while (Count > 0)
            Dequeue().Apply(ref buffer);
    }

}

public interface IFilter
{
    void Apply(ref Mat buffer);
}

最后,我们移除了IFilterParams,因为它们与问题已经不相关。现在示例过滤器的实现如下:

public class GaussianBlur : IFilter
{
    private GaussianBlurParams p;

    public GaussianBlur(GaussianBlurParams filterParams)
        : base(filterParams)
    {

    }

    public override void Apply(ref Mat buffer)
    {
        Cv2.GaussianBlur(buffer, buffer, p.KernelSize, p.SigmaX, p.SigmaY, p.BorderType);
    }
}

public class GaussianBlurParams
{
    public Size KernelSize = new Size(5, 5);
    public double SigmaX = default(double);
    public double SigmaY = default(double);
    public BorderTypes BorderType = BorderTypes.Default;
}

问题已解决,希望能对其他人有所帮助!

已删除 AbstractFilter<>,因为它不在原始问题中,会导致混淆。 - Aaron Murray

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