【问题标题】:Generic list of lists, converting List<List<T>> to IList<IList<T>>列表的通用列表,将 List<List<T>> 转换为 IList<IList<T>>
【发布时间】:2012-04-19 14:12:21
【问题描述】:

我们正在使用一个对 3D 测量数据执行计算的类库,它公开了一个方法:

MeasurementResults Calculate(IList<IList<Measurement>> data)

我希望允许使用任何可索引的列表列表(当然是测量)调用此方法,例如两者:

Measurement[][] array;
List<List<Measurement>> list;

使用数组调用方法可以正常工作,这有点奇怪。这里有一些编译器技巧吗?尝试使用 List 调用会出现熟悉的错误:

cannot convert from 'List<List<Measurement>>' to 'IList<IList<Measurement>>'

所以,我编写了一个外观类(也包含其他一些东西),它的方法在参数和方法之间拆分通用定义,并在必要时转换为 IList 类型:

MeasurementResults Calculate<T>(IList<T> data) where T : IList<Measurement>
{
  IList<IList<Measurement>> converted = data as IList<IList<Measurement>>;
  if(converted == null)
    converted = data.Select(o => o as IList<Measurement>).ToList();
  return Calculate(converted);
}

这是解决问题的好方法,还是您有更好的主意?

另外,在测试该问题的不同解决方案时,我发现如果使用IEnumerable 而不是IList 声明了类库方法,则可以同时使用数组和@987654328 调用该方法@:

MeasurementResults Calculate(IEnumerable<IEnumerable<Measurement>> data)

我怀疑还有一些编译器技巧在起作用,我想知道为什么他们没有让IListList 一起工作?

【问题讨论】:

  • 使用 T[][] 调用 IList&lt;IList&lt;T&gt;&gt; 方法可以工作,因为数组协方差损坏。它被破坏了,因为编译器允许您将List&lt;T&gt; 分配给IList&lt;IList&lt;T&gt;&gt; 的元素,但如果该对象确实是T[][],则分配将在运行时失败。
  • @phoog:啊哈,有趣,不知道。

标签: c# .net generics ienumerable ilist


【解决方案1】:

可以使用IEnumerable&lt;T&gt; 执行此操作,因为这是T 中的协变。使用IList&lt;T&gt; 这样做并不安全,因为它同时用于“输入”和“输出”。

特别要考虑:

List<List<Foo>> foo = new List<List<Foo>>();
List<IList<Foo>> bar = foo;
bar.Add(new Foo[5]); // Arrays implement IList<T>

// Eh?
List<Foo> firstList = foo[0];

有关 generic covariance and contravariance 的更多信息,请参阅 MSDN。

【讨论】:

  • 当然,使用IList&lt;IList&lt;T&gt;&gt; 声明方法并使用T[][] 调用它是可行的,因为数组协方差损坏。
【解决方案2】:

假设我们有一个方法

void Bar(IList<IList<int>> foo)

Bar 中,添加int[]foo 应该是完全可以接受的——毕竟int[] 实现了IList&lt;int&gt;,不是吗?

但是如果我们用List&lt;List&lt;int&gt;&gt; 调用Bar,我们现在会尝试将int[] 添加到只接受List&lt;int&gt; 的东西上!这将是。所以编译器不允许你这样做。

另外,在测试该问题的不同解决方案时,我发现如果使用IEnumerable 而不是IList 声明了类库方法,则可以同时使用数组和@987654335 调用该方法@:

确实,因为如果行为契约只是说“我可以输出 ints”,就不会出错。进一步研究的关键术语是协方差逆变,没有人*能记住哪个是哪个。

在您的特定情况下,如果 Calculate 只是读取它的输入,那么将其更改为使用 IEnumerable 绝对是正确的做法 - 它都允许您通过任何合格的对象,并且它进一步传达任何阅读签名的人,该方法被有意设计为仅使用而不是改变其输入。


*好吧,几乎没有人

【讨论】:

  • 记住协变与逆变的一种简单方法是考虑嵌套时会发生什么。如果Foo&lt;T&gt;co变体,则Foo&lt;T&gt;Foo&lt;U&gt; 之间的关系将与TU 之间的关系相同。如果Foo&lt;T&gt;相反变体,则关系将相反。有趣的是,如果Foo&lt;T&gt; 相对于T 是协变的或逆变的,则Foo&lt;Foo&lt;T&gt;&gt; 将是协变的(相对于T 协变的例程通常是提供T 的例程;接受 T 类型的消费者,预计向他们提供 T's)。
  • 当然,这是有道理的。计算只读取数据,但我不愿将其更改为 IEnumerable,因为我们需要使用索引属性进行随机访问。我们可以在Calculate 中执行.ToList(),但这会导致所有数据被复制到新列表中,这可能会导致性能问题。好像不存在只读的可索引集合接口?
  • @Anlo 现在这是一个好问题!但thisthis 建议答案是否定的。不过,请务必创建自己的IReadOnlyList&lt;&gt;
【解决方案3】:

你的实现有一些我会改变的事情。首先,它可能会在原始对象中有对象的结果中插入空值。我个人更喜欢扔它。

MeasurementResults Calculate<T>(IList<T> data) where T : IList<Measurement>
{
    return Calculate(data as IList<IList<Measurement>>
                     ?? data.Cast<IList<Measurement>>().ToList());
}

是一个简短的例子,就像我个人在你的情况下会做的那样,尽管我怀疑我是否会真正实现该方法。接口是这样写的(我希望如此),如果可能的话,我会尝试在我的实现中使用这些知识,而不是与之抗争。如果您使用您的代码(或类似的代码),对方法中的列表所做的任何更改都不会反映在原始列表中。这是一个传递给方法的新列表,由于签名要求 IList,因此可以进行更改。

除了列表中可能没有空值而不是对象之外,我已经更改为使用 cast 方法,因为这基本上是您想要完成的。 (Cast 不允许自定义转换运算符)。

【讨论】:

  • 是的,抛出异常而不是默默地将对象交换为空值绝对是个好主意。但是你确定我的代码可以在列表中引入空值吗? where T : IList&lt;Measurement&gt; 应该禁止任何不能转换为IList&lt;Measurement&gt; 的东西,还是我错过了什么?此外,我还没有真正考虑过将新列表传递给Calculate 的事实。在这种情况下,这不是问题,因为数据仅由方法读取 - 但在一般情况下它肯定很重要。
  • 另外,Cast 比我的 Select 更干净,信息量更大。
  • @anlo 我必须承认我忘记了约束,只阅读了代码,但我想这是一个很好的例子,说明为什么信息分散会导致误解(一个完全不同的主题,但与一个领域非常相关为我工作称为 DCI)所以这里有一个很好的例子 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多