在 C# 中将子类型的列表作为超类型列表返回

本文关键字:列表 类型 超类 返回 | 更新日期: 2023-09-27 18:17:51

我想让"以某种方式"完成以下工作:

public class GenericsTest<T> where T: ISomeInterface
{
    private List<T> someList = new List<T>();
    public List<ISomeInterface> GetList()
    {
        return someList; //of course this doesn't even compile
    }
}

有没有办法做到这一点(就像 Java 中的通配符(而我只是错过了它?还是做不到?

编辑:首先,感谢您的关注和回答/评论。其次,肯定有办法做到这一点,可能最简单和最有效的性能(虽然不确定(是创建一个正确类型的新列表,并迭代添加另一个列表的元素(在我们的例子中是someList(。问题是所有这些新的方差事物,"ins"和"outs"是否有办法以"泛型方式"做到这一点。在Java中,这可能是:

public class GenericsTest<T extends SomeInterface> {
    private List<T> someList = new ArrayList<>();
    public List<? extends SomeInterface> getList() {
        return someList;
    }
}

所以我在徘徊,如果 C# 中有? extends等效项。我可以接受"否"作为简短的答案,我只是觉得我应该先问。

编辑2:一些用户认为,而另一些用户则坚持认为这是与选角有关的问题的重复。由于我标记了最适合我的答案,为了清楚起见,我解释一下。伙计们,我不想投。我只是在寻找具有extends约束 Java 通配符的等效项。当然,Java只有编译时泛型,这可能会有所不同。如果 C# 中的等效项是用强制转换完成的,但我知道"Cat 列表中有一个 Dog 对象"的问题,所以我的问题有所不同。

在 C# 中将子类型的列表作为超类型列表返回

我相信这可以解决问题:

public List<ISomeInterface> GetList()
{
    return someList.Cast<ISomeInterface>().ToList();
}

您还可以执行以下操作:

public List<ISomeInterface> GetList()
{
    return someList.OfType<ISomeInterface>().ToList(); 
}

不同之处在于第一个选项将执行硬转换,这意味着如果其中一个元素未实现ISomeInterface则会引发异常。第二个版本将仅返回实现ISomeInterface的实例,并且只会跳过未实现的实例。

在您的示例中,您可以安全地使用 Cast 选项,因为您可以保证所有实例都实现 ISomeInterface

您可以使用

OfType<T>

public class GenericsTest<T> where T : ISomeInterface
{
    private List<T> someList = new List<T>();
    public List<ISomeInterface> GetList()
    {
        return someList.OfType<ISomeInterface>().ToList(); 
    }
}

在 C# 中,泛型方差的工作方式与 Java 中不同。如果要利用它,则需要使用变体的接口(或委托(。这些接口包括IEnumerable<T>IReadOnlyList<T>(.Net 4.5 中的新功能(,但不包括IList<T>,因为这不是类型安全的。

这意味着您可以执行以下操作:

public class GenericsTest<T> where T: ISomeInterface
{
    private List<T> someList = new List<T>();
    public IEnumerable<ISomeInterface> GetList()
    {
        return (IEnumerable<ISomeInterface>)someList;
    }
}

这就是问题所在。 让我们假设这段代码将编译:

class foo: ISomeInterface {}
class bar: ISomeInterface {}
GenericsTest<foo> testobject = GenericsTest<foo>();
List<ISomeinterface> alist = testobject.GetList();

在内部,testobject确实有一个foo列表,但是通过允许我们投射到List<ISomeinterface>如果我们尝试将bar插入列表中,调用者就无法知道会发生不好的事情。 这就是它不被允许并且不能轻易被允许的原因。

解决该问题的方法是:

  1. 在内部使用List<ISomeInterface>(如果集合变得异构是可以的(
  2. 如果只读列表正常,则创建具有所需类型的列表的副本。 如果需要在某些情况下更改列表,则可以在GenericTest类上添加类型安全方法来完成此操作。

Java和.NET的一个主要弱点是,用于封装可变对象标识的可变类型引用与用于封装其状态的可变类型引用之间没有区别。 特别是,如果代码调用getList然后尝试修改返回的列表,则不清楚这种尝试是否应该修改GenericsTest<T>持有的列表,或者它是否应该简单地修改由该方法创建的列表的副本(保留原始列表(。 目前还不清楚getList返回的列表是否应该受到未来对GenericsList<T>持有的一组项目所做的任何更改的影响。

如果您的目标是允许调用方拥有一个新列表,该列表填充了原始列表中的项目,但它可以根据需要进行操作,我建议您使用 AsList 方法或定义一个接口

interface ICopyableToNewList<out T> {
    // List<T> ToList(); // Can't be an actual interace member--must use extension method
    int Count {get;}
}

实现作用于该接口的ToList()扩展方法:

public static List<T> ToList<T>(this ICopyableToNewList<T> src)

并让您的类实现ICopyableToNewList<T> . 这样的声明将允许代码通过将GenericTest<T>转换为ICopyableToNewList<desiredType>来获取任何类型的新列表,该列表是T的超类型。