将类型参数限制为基础类型

8

我知道如何强制类型参数成为另一种类型的子类型:

public interface IMapping<T2> 
{
    public void Serialize<T3>(T3 obj) 
        where T3 : T2;
}
...

var mapping = MapManager.Find<Truck>();
mapping.Serialize(new TonkaTruck());

有没有一种方法可以强制类型参数成为另一个类型的超类型
public interface IMapping<T2>
{
    public void IncludeMappingOf<T1>() 
        where T2 : T1;   // <== doesn't work
}
...

var mapping = MapManager.Find<Truck>();

// Truck inherits Vehicle    
// Would like compiler safety here:
mapping.IncludeMappingOf<Vehicle>(); 

mapping.Serialize(new TonkaTruck());

目前,我需要在运行时使用IsSubclassOf来比较T1和T2,以便在IncludeMappingOf中使用。最好是一种编译安全的解决方案。有什么想法吗?

编辑: 将示例更改为不那么设计有问题。

注意:链接的问题非常相似,但没有合适的答案。希望这个问题也能为那个问题提供一些启示。

第二次编辑:

更简单的示例:

public class Holder<T2>
{
    public T2 Data { get; set; }

    public void AddDataTo<T1>(ICollection<T1> coll)
        //where T2 : T1    // <== doesn't work
    {
        coll.Add(Data);   // error
    }
}

...
var holder = new Holder<Truck> { Data = new TonkaTruck() };
var list = new List<Vehicle>();
holder.AddDataTo(list);

编译器:参数类型“T2”无法分配给参数类型“T1”。是的,我知道,我正在尝试让编译器只允许T2可以分配给参数类型T1的情况!


3
这是一个有趣的问题,但我有一种感觉,可能存在一个设计问题,为什么“卡车映射(TruckMapping)”不继承“车辆映射(VehicleMapping)”,而是继承了“IMapping”?我的意思是,听起来卡车映射应该是更通用的车辆映射的专业化版本,或者我有什么地方理解有误吗? - Simon Rapilly
@SimonRapilly:可能是对的。这是一个编造的例子,旨在帮助说明问题。我稍微改变了一下这个例子。 - CSJ
我无法看到任何实际情况下这样做有帮助的场景。你有吗? - MarcinJuraszek
@CSJ 抱歉,但是新的例子感觉更大了,现在更清晰了。你有一个容器Holder,你指定它应该至少包含T2个对象,但是你想要添加少于这个数量的对象。这违反了多态原则,Holder已经完成了它被要求做的工作。 - Simon Rapilly
@CSJ,我真的无法理解这个问题如何不违反面向对象编程的原则或成为唯一或最佳解决方案;我想我需要一个真实的例子。无论如何,不要让我阻止你。 - Simon Rapilly
显示剩余3条评论
3个回答

5
虽然w0lf的答案提供了一个直接的解决方案,但我想给出一些背景解释。
当你写下这样的代码:
class C<A> where A : B

或者

void F<A>() where A : B

表单约束A : B必须将A作为类、接口、方法等正在声明的泛型类型参数之一。

您遇到的错误并不是因为您在冒号的右侧放置了当前声明的泛型类型参数(这是合法的),而是因为您在冒号的左侧放置了外部声明的泛型类型参数(而不是当前声明的泛型类型参数)。

如果您想在某个声明上形成一个约束A : B,则A必须在该声明中引入,并且A的范围必须小于或等于B的范围。这是一种实用的语言限制的原因是,对于任何泛型类型参数T,它将对类型T的约束推断隔离到引入T的单个声明中。


当然,我明白。但是你可以看到我想要实现什么。 - CSJ
@CSJ 我明白你想要实现什么,非常清楚。我所解释的是,有一种语言限制阻止你这样做。这些语言限制本质上使通用类型约束图成为树形结构,从而更容易进行推理,但代价是失去了一些表达能力。你可以使用w0lf的建议作为解决此问题的方法。 - Timothy Shields

4
在类(接口)级别上声明通用类型和通用约束:
public interface IMapping<T1, T2> where T2 : T1
{
    void IncludeMapping(IMapping<T1, T2> otherMapping);
}

1
这意味着类的任何实例必须始终使用声明的 T3,而原始意图是您可以声明/实例化对象,然后将任意数量的不同类型传递到 Serialize 方法中,只要 T2 是派生类型。此外,问题要求将 T2 约束为 T3 的派生类型,而不是相反。编辑:啊,你刚刚编辑了你的答案。忽略我的评论的第二部分。 - Chris Sinclair
1
你可能需要更新你的编辑。你应该不要在接口方法中重新声明 <T1, T2> 和它的限制;我相信这些将是新声明的类型,与接口声明中的 <T1, T2> 没有任何关系。 - Chris Sinclair
@ChrisSinclair 你是对的;我意识到了并更新了我的答案。 - Cristian Lupascu
这个方案需要实现者事先知道哪些基本类型将被要求,并预先声明接口。它还限制了调用者:只能使用一个基本类型(T1)。如果他还想继续调用IncludeMappingOf <T0>怎么办? - CSJ

1
您可以使用扩展方法来实现您想要的效果。以 holder 为例,代码如下:
public class Holder<T2>
{
    public T2 Data { get; set; }
}

public static class HolderExtensions
{
    public static void AddDataTo<T2, T1>(this Holder<T2> holder, ICollection<T1> coll)
        where T2 : T1
    {
        coll.Add(holder.Data);
    }
}

这样就可以使你的示例调用代码编译无误:

var holder = new Holder<Truck> { Data = new TonkaTruck() };
var list = new List<Vehicle>();
holder.AddDataTo(list);

映射示例因其为接口而变得复杂。如果没有办法从现有接口实现扩展方法,则可能需要向接口添加实现方法。这意味着您仍然需要运行时检查,但调用者可以获得良好的语法和编译时检查。这将类似于:

public interface IMapping<T2>
{
    void IncludeMappingOf(Type type);
}

public static class MappingExtensions
{
    public static void IncludeMappingOf<T2, T1>(this IMapping<T2> mapping)
        where T2 : T1
    {
        mapping.IncludeMappingOf(typeof(T1));
    }
}

不幸的是,IncludeMappingOf没有类型为T1的参数,因此无法推断类型参数。 在调用它时,您被迫指定两种类型:

var mapping = MapManager.Find<Truck>();
mapping.IncludeMappingOf<Truck, Vehicle>();
mapping.Serialize(new TonkaTruck());

这通常可以通过更改API来包含一个参数(例如truckMapping.IncludeMappingOf(vehicleMapping)),更改参数所在的方法/类或使用流畅的API创建链来解决(例如mapping.Of<Vehicle>().Include())。


这非常聪明,虽然有点不太优雅(我的类的逻辑现在分布在两个类中)-- 但它确实实现了目标! - CSJ

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