每当我想指定特定输出是只读时,我曾经用IEnumerable<T>
作为返回类型创建接口。我喜欢它,因为它是简约的,隐藏了实现细节并将被调用者与调用者分离。
但是最近我的一位同事认为IEnumerable<T>
应该保留用于仅涉及延迟评估的场景,否则对于调用方法不清楚,异常处理应该采用它的方式 - 围绕方法调用或围绕迭代。然后,对于具有只读输出的急切评估案例,我应该使用ReadOnlyCollection
。
对我来说听起来很合理,但你会推荐什么?你同意IEnumerable的约定吗?或者有更好的IEnumerable异常处理方法吗?
如果我的问题不清楚,我做了一个样本课来说明问题。这里的两个方法具有完全相同的签名,但它们需要不同的异常处理:
public class EvilEnumerable
{
IEnumerable<int> Throw()
{
throw new ArgumentException();
}
IEnumerable<int> LazyThrow()
{
foreach (var item in Throw())
{
yield return item;
}
}
public void Run()
{
try
{
Throw();
}
catch (ArgumentException)
{
Console.WriteLine("immediate throw");
}
try
{
LazyThrow();
}
catch (ArgumentException)
{
Console.WriteLine("No exception is thrown.");
}
try
{
foreach (var item in LazyThrow())
{
//do smth
}
}
catch (ArgumentException)
{
Console.WriteLine("lazy throw");
}
}
}
更新1。问题不仅限于ArgumentException。它是关于创建友好类接口的最佳实践,它告诉您它们是否返回惰性求值结果,因为这会影响异常处理方法。
答案 0 :(得分:10)
这里真正的问题是延迟执行。在参数检查的情况下,您可以通过添加第二种方法来实现:
IEnumerable<int> LazyThrow() {
// TODO: check args, throwing exception
return LazyThrowImpl();
}
IEnumerable<int> LazyThrowImpl() {
// TODO: lazy code using yield
}
例外情况发生;即使对于非延迟结果(例如List<T>
),您也可能会遇到错误(可能是在迭代时另一个线程调整列表)。上述方法可以让您尽可能提前做到,以减少yield
和延迟执行的意外副作用。
答案 1 :(得分:3)
理论上,我同意你的同事。如果有一些“工作”来创建结果,并且该工作可能会导致异常,并且您不需要延迟评估,那么确实使结果变得懒惰只会使问题复杂化。
然而在实践中,你无法查看返回类型并推断任何有用的东西。即使您创建了返回类型ReadonlyCollection,它仍然可能会延迟评估和抛出,例如(ReadonlyCollection没有密封,因此子类可以显式实现IEnumerable接口并做一些古怪的事情。)
在一天结束时,.Net类型系统无法帮助您对大多数对象的异常行为做出太多推理。
实际上,我会选择返回一个ReadonlyCollection而不是IEnumerable,但是因为它比IEnumerable暴露了更多有用的方法(比如O(1)访问第n个元素)。如果您要生成由数组支持的清单集合,那么您也可以向消费者提供有用的方面。 (您可能还会返回调用者拥有的“新鲜”数组。)答案 2 :(得分:2)
我不同意你的同事。该方法的文档应遵循标准,该文档可能会引发异常。将返回类型声明为IEnumerable不会使其只读。它是否只读取决于返回的接口的实际实现。调用者不应该依赖它是可写的,但这与只读的集合非常不同
答案 3 :(得分:0)
在Eric Lippert的帖子中讨论了类似的问题:
http://blogs.msdn.com/ericlippert/archive/2007/09/05/psychic-debugging-part-one.aspx
http://blogs.msdn.com/ericlippert/archive/2007/09/06/psychic-debugging-part-two.aspx
但是,我不会将它视为一般反对IEnumerable的论据。老实说,大多数情况下,当我枚举的集合抛出异常时,我不知道如何处理它,基本上它会转到一些全局错误处理程序。当然,我同意抛出异常的那一刻可能会令人困惑。