确保通用集合包含从两个基本对象派生的对象

3

我有一个有趣的问题,我一直在徘徊,但似乎从未找到解决方法。

我倾向于成为一名防御性程序员,所以我尝试编写能够预防问题发生而非出现问题后再进行反应的代码。为此,我有以下情况。看下面的代码:

public class Base {}
public Interface IBase {}

public class Derived : Base, IBase {}
public class Derived2 : Base, IBase {}
...
public class DerivedN : Base, IBase {}

public class X : Base {}
public class Y : IBase {}

我需要将继承自Base并实现IBase的对象列表传递给集合,我需要确保只有同时具有这两个属性的对象才会被添加到列表中。此外,可能会有任意数量的类同时具有这两个属性,因此我不能使用派生类作为约束条件。
如果我将列表类型设为Base,则可以添加Y对象。如果将其设置为IBase类型,则可以添加X类型的对象(两者都不允许)。
当然,我可以创建自己的通用集合类,具有这两种类型,并对两种类型都设置约束。但是,我不想为所有可能的集合类型都这样做,即使只是将方法调用转发到包含的类中,也需要大量的工作。
我还可以创建一个BaseWithIBase类,它从Base和IBase派生,并将其用作我的集合类型,但如果不必要,我真的不想强制另一个抽象。
我不希望这是运行时检查,因此不能遍历树并抛出异常。
有人能提出更好的解决方案吗?
注意:Base和IBase没有关系,只是指它们是不同类型的基本项目。
编辑:
似乎每个人都想坚持“你不需要那样做”,并且这“不是面向对象编程”。这与事实相去甚远。我试图从问题中删除特定内容,以防止这些问题和评论,因此我将包括我的真实情况。
该代码是基于.NET Frameworks ServiceProcess.ServiceBase类实现的Windows服务框架。我在其上添加了自己的框架,旨在基于依赖注入,并且易于测试。
该集合必须包含从ServiceBase和IService派生的对象。IService是我在代码和测试中使用的框架扩展,基本上只是这样:
public interface IService 
{
    void Start();
    void Stop();
}

此外,我还有许多其他接口:
public interface IRestartableService
{
    void Restart();
}

public interface IConfigurableService
{
    void Configure();
}

etc...等等,一个服务可能是这样的:
public class MyService : ServiceBase, IService, IConfigurableService {}

我的代码需要 IService,而 Windows 需要 ServiceBase,因此两者都是必需的,因为我使用 IService,而 Windows 使用 ServiceBase。我只需要 IService,其他接口是可选的。


1
你能给出更具体的BaseIBase的例子吗? - Andrey Shchekin
4个回答

8
您可以很简单地创建自己的包装器集合:
// TODO: Work out which collection interfaces you want to implement
public class BaseList
{
    // Or use List<IBase>, if that's how you'll be using it more often.
    private List<Base> list = new List<Base>();

    public void Add<T>(T item) where T : Base, IBase
    {
        list.Add(item);
    }
}

通过使用具有两个约束条件的通用方法,您可以确保只能使用适当的类型参数调用 Add 方法。
您可以编写两个方法将数据公开为 IEnumerable<T> - 其中一个返回 IEnumerable<IBase>(使用 Cast<T>),另一个返回 IEnumerable<Base>… 这样你就可以在任何一种类型上使用 LINQ,但当然不能同时使用两种类型。
然而,我怀疑您可能会在其他地方遇到麻烦-您可能会发现自己的代码中充斥着您通常不需要的通用方法。虽然您想要类和接口部分都存在可能是有好的理由,但值得退一步考虑它们是否真正都必要。例如,您是否可以添加一些额外的内容到接口中,以便您可以摆脱类约束?

1
@MystereMan:我描述的内容似乎不像你建议的任何一种方案。如果其中的一种方案正是你所想的,那么请原谅,但真的不太清楚。如果你想使用单个List<T>,你怎么期望从中获取值?恐怕您可能需要编写额外的代码才能满足您的要求。 - Jon Skeet
我说“当然我可以创建自己的通用集合类”的部分,是我认为我表达得非常明确的地方。我不反对编写额外的代码,我反对为了重新创建各种集合类、扩展方法和它们所支持的接口提供的所有功能而编写大量额外的代码。这旨在成为框架类中的实用程序项,因此我不能吝啬只实现当前正在使用的内容。 - Erik Funkenbusch
是的,我意识到仅实现IEnumerable本身就非常强大。我有我的IService列表,因为这是我99.9%的工作内容,我只需调用_services.Cast<ServiceBase>().ToArray()传递给ServiceBase.Run()。所以这真的是一个具有分裂身份的对象案例,它们被BCL视为ServiceBase,并由我的框架视为IServices,而包含不是一个选项,因为每个服务都是独立的,但我必须将数组传递给ServiceBase(因为它支持在单个主机exe中托管多个服务)。 - Erik Funkenbusch
@MystereMan:没错。所以我展示的类提供了一个集合,保证每个元素都具有两个标识,并且您可以使用任一标识获取视图。那么这样做有什么限制呢?请注意,在这种情况下,您最好使用List<IBase>而不是我展示的List<Base> - 我将进行编辑以解释这一点。 - Jon Skeet
是的,我开始同意了。像往常一样,你是理性的代表。 - Erik Funkenbusch
显示剩余6条评论

0

对于你的问题,没有一个很好的答案,因为设计本身并不是真正适合C#/.NET中实现的OOP。

如果你绝对需要一个集合,在这个集合中每个元素静态地提供两个独立的接口,那么一个包装器集合或一些类似Wrapper<TFirst, TSecond, T> : IBoth<TFirst, TSecond>的包装器类将解决你的问题。

例子:

public interface IBoth<TFirst, TSecond> {
    TFirst AsFirst();
    TSecond AsSecond();
}

public class Wrapper<T, TFirst, TSecond> : IBoth<TFirst, TSecond>
   where T : TFirst, TSecond
{
    private readonly T _value;

    public Wrapper(T value) {
        _value = value;
    }

    public TFirst AsFirst() {
        return _value;
    }

    public TSecond AsSecond() {
        return _value;
    }
}

然而,真正的问题是你为什么需要那个。并不是说标准的面向对象编程模型是完美的,但如果原始设计决策得到审查,很多问题通常可以更容易地解决。


实际代码具有许多不同的接口和特定的基类(基于Windows服务和ServiceBase)。其想法是允许混合接口提供不同的功能,而不需要泄漏单个接口,就像集合类本身已经实现了许多具有不同功能的接口一样。 - Erik Funkenbusch
关于您的编辑,我也已经提到过我可以这样做,但如果不必要的话,我不想引入另一个抽象。我意识到我可能不得不这样做,但我宁愿尝试不这样做。 - Erik Funkenbusch
为什么要将这些接口设为强制性的?另外,如果ServiceBase API是必须的,您可以将这些方法包含在相关接口中。 - Andrey Shchekin
ServiceBase 是必需的,因为这是 Windows Service 的 .NET 实现。我必须将 ServiceBase 对象集合传递给 ServiceBase.Run() 方法。我不能向 .NET 框架传递其他抽象。 - Erik Funkenbusch
好的,但我认为你不会找到一个完美的解决方案。在某些语言中,它可能只需要 Collection<Base&IBase> 这样简单,但在 C# 中不行。 - Andrey Shchekin

0
另一个选择是在大部分代码中完全忽略ServiceBase,并为与不支持接口的代码通信创建一个ServiceBaseAdapter。这样的适配器可以在其方法被调用时调用您的接口方法。

也许我误解了你的意思,但如果你的意思是我所想的,那么这并不适用于这里,因为ServiceBase与操作系统紧密集成,并且服务必须由框架运行。 - Erik Funkenbusch
我的意思基本上是 ServiceBase.Run(services.Select(s => new ServiceBaseAdapter(s)).ToArray()) - Andrey Shchekin

-2
尝试像这样做:

List<object> collection = new List<object>();
foreach(var obj in collection.OfType<Base>().OfType<IBase>())
{
// Do what ever you want
}

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