.NET的IList必须是有限的吗?假设我编写了一个实现IList<BigInteger>
我们现在已经实现了除Count()之外的只读IList所需的所有方法。
这很酷,还是滥用IList?
Fibonacci数字很快变得不切实际(因此IList<BigInteger>
以上)。有界无限序列可能更合理,它可以实现IList<long>
或IList<double>
。
附录II:Fibonacci序列可能是一个不好的例子,因为计算远值是昂贵的 - 要找到必须计算所有早期值的第n个值。因此,正如Mošmondor所说,人们可能会将其设为IEnumerable并使用.ElementAt
。然而,存在其他序列,其中人们可以快速计算远距离值而无需计算早期值。 (令人惊讶的是digits of pi are such a sequence)。这些序列更“强烈”,它们真正支持随机访问。
编辑:没有人反对无限的IEnumerables。他们如何处理Count()?
答案 0 :(得分:17)
对于大多数开发人员来说,IList
和ICollection
意味着您有一个预先评估的内存中集合可供使用。特别是IList
,有一个固定时间Add
* 和索引操作的隐式契约。这就是LinkedList<T>
does not implement IList<T>
的原因。我认为FibonacciList违反了这个默示合同。
请注意a recent MSDN Magazine article中的以下段落,讨论向.NET 4.5添加只读集合接口的原因:
对于处理类型集合的大多数场景,
IEnumerable<T>
已足够,但有时您需要的功率超出其提供的范围:
- 实现:
IEnumerable<T>
不允许您表达集合是否已经可用(“物化”),或者是否每次迭代它时计算它(例如,如果它代表LINQ查询)。当算法需要对集合进行多次迭代时,如果计算序列很昂贵,则会导致性能下降;由于在后续传递中再次生成对象时身份不匹配,它也会导致细微的错误。
正如其他人所指出的,还有一个问题是你会为.Count
返回什么。
将IEnumerable
或IQueryable
用于此类数据集合是完全正确的,因为期望可以对这些类型进行延迟评估。
关于编辑1:.Count()
接口未实现IEnumerable<T>
:它是extension method。因此,开发人员需要预期它可能需要花费任何时间,并且他们需要避免在他们实际上不需要知道项目数量的情况下调用它。例如,如果您只是想知道IEnumerable<T>
是否有任何项,那么最好使用.Any()
。如果您知道要处理的项目数量最多,则可以使用.Take()
。如果集合中包含的项目超过int.MaxValue
,.Count()
将遇到操作溢出。因此,有一些解决方法可以帮助减少与无限序列相关的危险。显然,如果程序员没有考虑到这些可能性,它仍然会导致问题。
关于编辑2:如果你计划以索引是恒定时间的方式实现你的序列,那很容易解决我的要点。尽管如此,Sixlettervarariables的答案仍然适用。
*显然还有更多内容:Add
仅在IList.IsFixedSize
返回false
时才有效。只有在IsReadOnly
返回false等情况下才能进行修改。IList
首先是一个经过深思熟虑的界面:最后可以通过引入只读集合接口来解决这个问题。 .NET 4.5。
在给出了一些额外的想法之后,我个人认为IEnumerable<>
s也不应该是无限的。除了实现.ToList()
之类的方法之外,LINQ还有几个非流式操作,如.OrderBy()
,它们必须在返回第一个结果之前使用整个IEnumerable<>
。由于许多方法假设IEnumerable<>
是完全可以安全遍历的,因此产生IEnumerable<>
违反Liskov替换原则是违反无限期遍历的本身不安全的。{/ p>
如果您发现您的应用程序通常需要Fibonacci序列的片段作为IEnumerables,我建议创建一个具有类似于Enumerable.Range(int, int)
的签名的方法,这允许用户定义起始和结束索引
如果你想开始一个Gee-Whiz项目,你可以想象开发一个基于Fibonacci的IQueryable<>
提供者,用户可以使用有限的LINQ查询语法子集,如下所示: / p>
// LINQ to Fibonacci!
var fibQuery = from n in Fibonacci.Numbers // (returns an IQueryable<>)
where n.Index > 5 && n.Value < 20000
select n.Value;
var fibCount = fibQuery.Count();
var fibList = fibQuery.ToList();
由于您的查询提供程序可以将where
子句计算为lambda表达式,因此您可以有足够的控制权来实现Count
方法和.GetEnumerator()
,以确保查询的限制性足以产生真正的答案,或者在调用方法后立即抛出异常。
但是这个聪明的东西,对任何现实生活中的软件都可能是一个非常糟糕的主意。
答案 1 :(得分:2)
我认为符合要求的实现必须是有限的,否则你会为ICollection<T>.Count
返回什么?
/// <summary>
/// Gets the number of elements contained in the <see cref="ICollection{T}" />.
/// </summary>
int Count { get; }
另一个考虑因素是CopyTo
,它在正常超载下永远不会停留在Fibonacci案例中。
这意味着Fibonacci序列的适当实现只是IEnumerable<int>
(使用生成器模式)。 (Ab)使用IList<T>
只会导致问题。
答案 2 :(得分:1)
在您的情况下,我宁愿“违反”IEnumerable
并与yield return
合作。
:)
答案 3 :(得分:1)
无限集合可能最好是IEnumerable<T>
,而不是IList<T>
。您也可以在实现时使用yield return
语法,如此(忽略溢出问题等):
public IEnumerable<long> Fib()
{
yield return 1;
yield return 1;
long l1 = 1;
long l2 = 1;
while (true)
{
long t = l1;
l1 = l2;
l2 = t + l1;
yield return l2;
}
}
答案 4 :(得分:1)
修改强>
根据@ StriplingWarrior的评论,有一天时间反思我的答案。我担心我必须做出逆转。我昨晚开始尝试这个,现在我想知道放弃IList
会给我带来什么损失?
我认为仅仅实现IEnumerable
更明智,并声明一个本地Count()
方法,该方法会抛出NotSupportedException
方法,以防止枚举器在OutOfMemoryException
发生之前运行。我仍然会添加IndexOf
和Contains
方法以及Item
索引器属性来公开更高性能的替代方案,例如Binet的公式,但是,我可以自由地更改这些成员的签名以使用扩展数据类型可能是偶数System.Numerics.BigInteger
。
如果我实现了多个系列,我会为这些成员声明一个ISeries
接口。谁知道,也许这样的事情最终会成为框架的一部分。
我不同意看似共识的观点。虽然IList
有许多成员无法为无限系列实现,但它确实有一个IsReadOnly
成员。在ReadOnlyCollection<>
的情况下,使用NotSupportedException
实现大多数成员似乎是可以接受的。在这个先例之后,我不明白为什么如果这是其他一些功能上的副作用,这是不可接受的。
在这个特定的Fibbonaci系列案例中,有一些algortihms,见here和here,用于缩短正常的计量方法,我认为这会产生极大的性能优势。通过IList
公开这些好处对我来说似乎是有道理的。
理想情况下,.Net会支持其他一些更合适的超类接口,稍微接近IEnumerable<>
但是,在未来版本到达之前,这必须是一种明智的方法。
我正在制作IList<BigInteger>
的实现来说明
答案 5 :(得分:1)
正如@CodeInChaos在注释中指出的那样,IList的Item属性具有签名
T this[ int index ] { get; set; }
我们看到IList被整数编入索引,因此它们的长度受Int32.MaxValue的限制。更大索引的元素将无法访问。在写这个问题时我发生了这种情况,但我把它遗漏了,因为考虑到这个问题很有趣。
答案 6 :(得分:0)
总结我到目前为止看到的内容:
您可以在NotSupportedException
Count()
,从而完成6个中的5个
我会说这可能已经足够了,但正如servy
所指出的那样,对于任何非计算和缓存的数字,索引器的效率都非常低。
在这种情况下,我会说唯一适合你不断计算的合约是IEnumerable。
你拥有的另一个选择是创建一个看起来很像IList
的东西,但实际上并不是这样。