从基类方法返回派生类型

8

我有一个类层次结构,看起来类似于这样:

public class Base
{
    private List<string> attributes = new List<string>();

    public T WithAttributes<T>(params string[] attributes)
        where T : Base
    {
        this.attributes.AddRange(attributes);
        return this as T;
    }
}

public class Derived : Base
{
}

我希望您能使用流畅API语法从派生类中调用 Base.WithAttributes 方法,并返回派生实例,就像下面的示例一样。
void Main()
{
    Derived d = new Derived();

    // CS0411 The type arguments for method 'UserQuery.Base.WithAttributes<T>(params string[])' cannot be inferred from the usage.
    d.WithAttributes("one", "two"); 

    // Works, but type arguments must be explicity specified. 
    d.WithAttributes<Derived>("one", "two");

    // Works without explicitly specifying, but no access to private members!
    d.WithAttributesEx("one", "two");
}

public static class Extensions
{
    public static T WithAttributesEx<T>(this T obj, params string[] attributes)
        where T : Base
    {
        // No access to private members, argh!
        // obj.attributes.AddRange(attributes);
        return obj as T;
    }
}
  1. 为什么编译器不能在第一个示例中推断类型参数?
  2. 使用扩展方法调用时为什么会起作用?
  3. 有没有办法在基类上将其作为实例方法工作而不显式指定类型参数?

相关:https://dev59.com/5Y7da4cB1Zd3GeqP_Eva#32123354


它不起作用是因为Base<T>中的TWithAttributes<T>中的T之间没有链接。尝试将标识符更改为Base<F>,它将正常工作。您没有在扩展方法中使用通用的Base - Heretic Monkey
即使没有在混合中使用Base<T>,它也无法正常工作。Base<T>WithAttributes<T>中的T之间不应该有链接。我已经更新了示例以删除它。 - tbridge
1
那么你希望 Derived.WithAttributes<T> 推断类型参数的方式是什么呢?因为 T 可以是任何继承自 BaseBase 本身的东西,包括不是 DerivedBase 的东西。 - Heretic Monkey
@MikeMcCaughan 因为在具有几乎相同签名的扩展方法的情况下,它没有推断问题。 - tbridge
2
@tbridge 在扩展方法中的 this 使得这两者不等价。在实例方法中,T 是任何继承自 Base 的 T。在扩展方法中,T 是调用该方法的实例的类型。 - Orphid
1个回答

18

为什么编译器无法在第一个示例中推断类型参数?

类型推断使用方法参数来推断类型参数。在第一个示例中,没有可以用来推断类型参数的方法参数。

为什么在使用扩展方法调用时它能够工作?

扩展方法实际上是一个静态方法,你要“扩展”的对象作为参数传递给扩展方法调用:

Extensions.WithAttributesEx<T>(d, "one", "two")

如上所述,类型推断使用方法参数来查找类型参数。在这里,类型参数可以从第一个方法参数的类型中推断出来,该参数为Derived

是否有办法使其作为基类的实例方法工作,而不需要明确指定类型参数?

使基类成为泛型,并将其参数化为派生类(这被称为奇异递归模板模式):

public class Base<T>
    where T : Base<T>
{
    private List<string> attributes = new List<string>();

    public T WithAttributes(params string[] attributes)            
    {
        this.attributes.AddRange(attributes);
        return this as T;
    }
}

public class Derived : Base<Derived>
{
}

使用方法:

Derived d = new Derived().WithAttributes("one", "two").WithAttributes("three");

@MikeRoibu 嗯,你可以在基类中实现一些IBase接口。然后你仍然会拥有构建派生对象的漂亮流畅的API,并且你将能够处理IBase类型的列表。 - Sergey Berezovskiy
不行,那样做不了。接口不知道 T 是什么,所以你不能在接口中定义 WithAttributes。因此,你可以有一个 List<IBase> 列表,但是你不能调用 list[0].WithAttributes(),因为你不知道它应该返回什么。你必须调用 ((Base<DerivedA>)list[0]).WithAttributes(),如果 list[1] 不同,那么就是 ((Base<DerivedB>)list[1]).WithAttributes(),明白我的意思吗? - Mike Roibu
@MikeRoibu 我从未建议在接口中定义 WithAttributes - Sergey Berezovskiy
1
在某些情况下,您必须从列表中提取后转换类型,然后才能调用应用该模式的方法。这是我认为应该提到的该模式的缺点,因为它经常会出现。 - Mike Roibu
1
那不是重点。我的意思是“如果您有一个存储为IBase的DerivedA和DerivedB列表,您将无法浏览该列表并对它们调用.WithAttributes”,您必须了解这很容易发生。即使它们没有存储为IBase,也没有其他选择来存储它们并能够调用.WithAttributes。这只是模式的结果。 - Mike Roibu
显示剩余3条评论

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