通用的列表嵌套,将List<List<T>>转换为IList<IList<T>>。

6
我们正在使用一个类库,对三维测量数据进行计算,它公开了一个方法:
MeasurementResults Calculate(IList<IList<Measurement>> data)

我希望允许使用任何可索引的列表(当然是由Measurement组成的列表)调用此方法,例如:

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

使用数组调用该方法是有效的,这有点奇怪。这里是否有一些编译器技巧在起作用?而试图使用 List 调用则会出现熟悉的错误:

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

因此,我编写了一个Facade类(包含其他内容),其中包含一种将通用定义分割为参数和方法并在必要时转换为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,则可以使用数组和 List 调用该方法:

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

我怀疑这里又有一些编译器技巧在发挥作用,我想知道他们为什么没有一并将IListList搭配使用?


1
使用 T[][] 调用 IList<IList<T>> 方法是可行的,因为存在破碎的数组协变性。这种协变性是破碎的,因为编译器允许将 List<T> 分配给 IList<IList<T>> 的元素,但如果对象实际上是 T[][],则分配将在运行时失败。 - phoog
@phoog:啊哈,有趣,我不知道那个。 - Anlo
3个回答

6

如果使用的是 IEnumerable<T>,那么这样做无妨,因为它在 T 上是协变的。但如果使用的是 IList<T>,那就不安全了,因为它既用于“输入”又用于“输出”。

具体来说,请考虑以下内容:

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];

请参阅MSDN了解有关通用协变和逆变的更多信息。 点击此处

1
当然,使用IList<IList<T>>声明方法并使用T[][]调用它是可行的,因为存在破碎的数组协变性。 - phoog

3
假设我们有一个方法:
void Bar(IList<IList<int>> foo)

Bar 中,将int[] 添加到foo 是完全允许的,毕竟 int[] 实现了 IList<int>,是吗?
但是如果我们使用 List<List<int>> 调用 Bar,那么我们现在正在尝试将 int[] 添加到只接受 List<int> 的内容中!这将是不好的。因此,编译器不允许您这样做。
此外,在测试问题的不同解决方案时,我发现如果类库方法声明为 IEnumerable 而不是 IList,则可以使用数组和 List 调用该方法:
确实如此,因为如果行为合同只说“我可以输出 int”,那么就不会出错。进一步研究的关键术语是协变和逆变,并且没有人能永远记得哪一个是哪一个。
在您的特定情况下,如果 Calculate 只会读取它的输入,则将其更改为使用 IEnumerable 是绝对正确的 - 它既允许您传入任何符合条件的对象,还进一步向阅读签名的任何人传达了这个方法有意设计为仅消耗而不是改变其输入。

记住协变和逆变的简单方法是考虑嵌套时会发生什么。如果 Foo<T>协变的,那么 Foo<T>Foo<U> 之间的关系将与 TU 之间的关系相同。如果 Foo<T>逆变的,那么关系将相反。有趣的是,如果 Foo<T> 相对于 T 是协变或逆变的,那么 Foo<Foo<T>> 将是协变的(相对于 T 协变的例程通常是提供 T 的例程;接受类型为 T 的消费者的例程预计向它们提供 T)。 - supercat
当然,这很有道理。Calculate只读取数据,但我不确定是否将其更改为IEnumerable,因为我们需要使用索引属性进行随机访问。我们可以在Calculate内部执行.ToList(),但这会导致所有数据被复制到新列表中,可能会导致性能问题。似乎不存在只读可索引集合接口? - Anlo
@Anlo,这是一个好问题!但是这个这个表明答案是否定的。不过你可以自己创建IReadOnlyList<> - AakashM

1

你的实现有几个问题需要改进。首先,它可能会在结果中插入空值,而原始数据中存在对象。个人认为最好抛出异常。

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

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

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


是的,抛出异常而不是默默地将对象交换为null值绝对是一个好主意。但是您确定我的代码可以在列表中引入null值吗?“where T:IList <Measurement>”应该禁止任何无法转换为“IList <Measurement>”的内容,或者我错过了什么吗?此外,我还没有真正考虑到一个新的列表被传递给Calculate的事实。在这种情况下,由于数据仅由该方法读取,因此这并不是问题 - 但在一般情况下它肯定很重要。 - Anlo
此外,Cast 比我的 Select 更加简洁和信息丰富。 - Anlo
1
@anlo 我必须承认我忘记了这个限制条件,只看了代码,但是我想这说明了信息分散导致误解的一个很好的例子(一个完全不同的话题,但与我的一个叫做DCI的工作领域非常相关),所以感谢你提供的好例子 :) - Rune FS

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