无法在类型参数化方法中将子 class 隐式转换为父 class

Cannot implicitly convert child class to parent class in type parametrized method

我最近不得不帮助解决某人使用通用方法 returning 时遇到的问题,虽然有多个问题需要解决,但我理解并可以解释所有问题 - 除了最后一个让编译器接受 return 类型的障碍。虽然我最终成功地编译了程序并且 运行 正确,但我仍然不能完全遵循 为什么 必须这样做的逻辑。

我在下面用一个最小的例子重现了这个问题。给定一个非常简单的父子 class 结构,只有一个泛型方法:

abstract class AbstractParentClass
{
    public abstract T DoThing<T>() where T : AbstractParentClass;
}

class ConcreteChildClass : AbstractParentClass
{
    public override T DoThing<T>()
    {
        return this;
    }
}

这将导致 Cannot implicitly convert type 'ConcreteChildClass' to 'T' 的 return 行出现错误。有点奇怪,因为 T 被限制为 AbstractParentClass 的一个实例,而 this 是其中之一,但是没关系,当然,所以我们会明确地做到这一点:

public override T DoThing<T>()
{
    return (T) this;
}

现在错误消息显示为 Cannot convert type 'ConcreteChildClass' to 'T'。什么?如果 T 是不受约束的,那么当然,我知道我们不能保证它,但是像我们这样的继承,它肯定应该是一个简单的转换?

我们可以通过显式转换到父级来接近 class:

public override T DoThing<T>()
{
    return (AbstractParentClass) this;
}

现在错误显示为 Cannot implicitly convert type 'AbstractParentClass' to 'T'. An explicit conversion exists (are you missing a cast?)。为什么我们不能隐式转换为约束的确切类型 - 什么可能的情况会导致约束类型的 class 无法转换为...本身?但至少现在可以解决了,错误信息甚至直接告诉我们如何去做:

public override T DoThing<T>()
{
    return (T)(AbstractParentClass) this;
}

现在一切 运行 都很好。如果需要,我们甚至可以让它看起来更漂亮:

public override T DoThing<T>()
{
    return this as T;
}

在整个过程中,如果 T 不受约束,我当然理解为什么这些转换不可能像写的那样。但确实如此,并且编译器在某种程度上不应该有任何问题。出于某种原因,编译器是否只是不考虑允许的隐式转换的类型约束?根据我的经验,C# 中的所有此类情况背后都有一个很好的理由,我只是为了我的生活无法弄清楚这个。

如果有人对为什么编译器在处理这段(看似!)非常简单的代码时遇到问题有任何见解,我将不胜感激对允许这些转换的问题的解释。

因为从技术上讲你可以这样做:

class ConcreteChildClass2 : AbstractParentClass
{
    public override T DoThing<T>()
    {
        return null;
    }
}

var ccc = new ConcreteChildClass();
var ccc2 = ccc.DoThing<ConcreteChildClass2>();

即使你进行了转换,这也会在运行时爆炸:

System.InvalidCastException: Unable to cast object of type 'ConcreteChildClass' to type 'ConcreteChildClass2'.

如果您改为 return this as T,您将得到 null 作为结果而不是转换异常。

换句话说,约束不保证 DoThing returns 与定义 class 相同的类型,它只保证它 returns 某种类型继承 AbstractParentClass.

“为什么”是即使 ConcreteChildClass 是一个 AbstractParentClass 并且 T 是一个 AbstractParentClass,但这并不一定保证 ConcreteChildClassT.

对于real-world类比,CatPetDogPet,但Cat不是Dog.

D Stanley 的回答显示了如何使用 T 的值调用 DoThing(),这对于您正在处理的 object 类型来说是不正确的。

根据您的用例,您可以尝试使用其他一些模式来解决此问题。

例如,您可以在 parent class 上使用递归枚举声明类型,类似于 Java 对其枚举所做的操作:

abstract class AbstractParentClass<T> where T : AbstractParentClass<T>
{
    public abstract T DoThing();
}

class ConcreteChildClass : AbstractParentClass<ConcreteChildClass>
{
    public override ConcreteChildClass DoThing()
    {
        return this;
    }
}

这使得您无法在调用 DoThing() 时告诉 ConcreteChildClass 使用不同的 class 作为其通用类型。您强制要求任何正确遵循该模式的 child class 都必须 return 是正确类型的实例。但是,它迫使您在任何地方传播通用类型:您不能只引用 AbstractParentClass 而不包含一些通用参数。您可以使用 non-generic 接口来解决这个问题,让代码使用它并不关心它给出的是哪种实现类型。但它仍然有点笨拙,因为(不像 Java 枚举)你不能真正强制每个实现扩展 parent class 使用自己作为通用类型。