为啥 IReadOnlyCollection 有 ElementAt 但没有 IndexOf

Posted

技术标签:

【中文标题】为啥 IReadOnlyCollection 有 ElementAt 但没有 IndexOf【英文标题】:Why IReadOnlyCollection has ElementAt but not IndexOf为什么 IReadOnlyCollection 有 ElementAt 但没有 IndexOf 【发布时间】:2016-09-22 17:53:32 【问题描述】:

我正在处理 IReadOnlyCollection 的对象。

现在我有点惊讶,因为我可以使用linq扩展方法ElementAt()。但我无权访问IndexOf()

这在我看来有点不合逻辑:我可以获取给定位置的元素,但无法获取相同元素的位置。

有什么具体原因吗?

我已经阅读了 -> How to get the index of an element in an IEnumerable?,但我对回复并不完全满意。

【问题讨论】:

【参考方案1】:

IReadOnlyCollection 是一个集合,而不是一个列表,所以严格来说,它甚至不应该有ElementAt()。这个方法在IEnumerable中定义是为了方便,IReadOnlyCollection有它是因为它继承自IEnumerable。如果查看源代码,它会检查IEnumerable 是否实际上是IList,如果是,则返回请求索引处的元素,否则继续对IEnumerable 进行线性遍历,直到请求索引,效率低。

所以,你可能会问为什么IEnumerableElementAt() 而没有IndexOf(),但我觉得这个问题不是很有趣,因为它不应该有这两种方法。 IEnumerable 不应该是可索引的。

现在,一个非常有趣的问题是为什么IReadOnlyList 也没有IndexOf()

IReadOnlyList<T> 没有 IndexOf() 没有任何理由

如果真要找个理由提,那理由就是历史了:

早在 90 年代中期,当 C# 被淘汰时,人们还没有完全开始意识到不可变性和只读性的好处,所以不幸的是,他们融入语言的 IList<T> 接口是可变的。

正确的做法是提出IReadOnlyList<T> 作为基本接口,并让IList<T> 扩展它,只添加变异方法,但事实并非如此。

IReadOnlyList<T> 是在 IList<T> 之后相当长的一段时间内发明的,到那时重新定义 IList<T> 并使其扩展 IReadOnlyList<T> 为时已晚。所以,IReadOnlyList<T> 是从零开始构建的。

他们不能让IReadOnlyList<T> 扩展IList<T>,因为那样它会继承突变方法,所以他们基于IReadOnlyCollection<T>IEnumerable<T> 代替。他们添加了this[i] 索引器,但是他们要么忘记添加其他方法,如IndexOf(),要么故意省略它们,因为它们可以作为扩展方法实现,从而使接口更简单。 但是他们没有提供任何这样的扩展方法。

所以,这里是一个扩展方法,将IndexOf() 添加到IReadOnlyList<T>

using Collections = System.Collections.Generic;

    public static int IndexOf<T>( this Collections.IReadOnlyList<T> self, T elementToFind )
    
        int i = 0;
        foreach( T element in self )
        
            if( Equals( element, elementToFind ) )
                return i;
            i++;
        
        return -1;
    

请注意,此扩展方法不如接口中内置的方法强大。例如,如果您正在实现一个期望 IEqualityComparer&lt;T&gt; 作为构造(或单独的)参数的集合,则此扩展方法将完全不知道它,这当然会导致错误。 (感谢 Grx70 在 cmets 中指出这一点。)

【讨论】:

它总是让我感到好笑,*** 上的答案如何试图为某个 API 以某种方式或另一种方式提供理由,好像 API 总是正确的,好像它是用完美的方式构建的智慧,因此必须始终找到一些理由并向提出问题的假定新手解释。 (谁通常根本不是新手,他们通常会因为看到API有问题而提出问题,并且通过提出问题来指出问题总是被认为比大吵大闹,这是我通常喜欢做的事情。) 许多 .NET API 因历史原因而变得混乱且过于复杂。我希望看到任何后续主要版本的干净利落。兼容性很好,但不以拖累 90 年代的设计错误为代价 我同意这个答案的主要建议,即IReadOnlyList&lt;T&gt; 上缺少int IndexOf(T) 方法没有充分的理由。然而,建议的解决方法存在缺陷,我不同意这种方法不必包含在接口中的说法,因为它可以用扩展方法“替换”。想象一个使用任意IEqualityComparer&lt;T&gt; 的集合,您可能会在此扩展名返回-1Contains(T) 返回true(或相反),这可能会导致麻烦。 @Grx70 我不确定我是否理解您检测到的缺陷是什么。您是指我的函数缺少接受IEqualityComparer&lt;T&gt; 的重载这一事实吗?您是指它似乎没有使用IEquatable&lt;T&gt; 的事实吗?我可以添加这些,但我认为最好将它们作为练习留给读者。足够先进以实际使用这些接口的程序员会发现这很简单。还有什么吗?【参考方案2】:

这是因为IReadOnlyCollection(实现IEnumerable)不一定实现indexing,当您想以数字方式订购List时通常需要这样做。 IndexOf 来自 IList

想想像Dictionary这样没有索引的集合,Dictionary中没有数字索引的概念。在Dictionary中,不保证顺序,key和value之间只有一一对应的关系。因此,集合并不一定意味着数字索引。

另一个原因是因为IEnumerable 并不是真正的双向流量。可以这样想:IEnumerable 可能会在您指定时枚举项目x 并在x(即ElementAt)处找到元素,但它无法有效地知道它的任何元素是否位于哪个索引(即IndexOf)。

但是,是的,即使您以这种方式认为它仍然很奇怪,因为期望它同时具有 either ElementAtIndexOf or none。

【讨论】:

【参考方案3】:

IndexOf 是在List 上定义的方法,而IReadOnlyCollection 仅继承IEnumerable

这是因为IEnumerable 仅用于迭代实体。然而,索引不适用于这个概念,因为顺序是任意的,并且不能保证在对IEnumerable 的调用之间是相同的。此外,界面只是声明您可以迭代一个集合,而List 声明您也可以执行添加和删除操作。

ElementAt 方法确实可以做到这一点。但是我不会使用它,因为它会重复整个枚举以找到一个元素。最好使用First 或仅使用基于列表的方法。

无论如何,API 设计对我来说似乎很奇怪,因为它允许在第 n 位置获取元素的(低效)方法,但不允许获取任意元素的索引,这将是同样的低效搜索导致多达 n 次迭代。我会同意 Ian 的观点(我不推荐)或两者都不同意。

【讨论】:

我完全同意您的观点,即 Enumerables 不应该通过索引访问,但我仍然可以使用 ElementAt()。对我来说,我在给定位置获得元素有点愚蠢,但不是元素的位置。这更像是一个哲学观点:) 这确实是一个很奇怪的方法。我认为它的存在只是因为任何人都需要这个未来,但是直到现在,人们对 IndexOf-method 的***并不高。【参考方案4】:

IReadOnlyCollection&lt;T&gt; 具有 ElementAt&lt;T&gt;(),因为它是 IEnumerable&lt;T&gt; 的扩展,具有该方法。 ElementAt&lt;T&gt;() 迭代 IEnumerable&lt;T&gt; 指定的迭代次数并返回值作为该位置。

IReadOnlyCollection&lt;T&gt; 缺少IndexOf&lt;T&gt;(),因为作为IEnumerable&lt;T&gt;,它没有任何指定的顺序,因此索引的概念不适用。 IReadOnlyCollection&lt;T&gt; 也没有添加任何顺序的概念。

如果您想要IReadOnlyCollection&lt;T&gt; 的可索引版本,我会推荐IReadOnlyList&lt;T&gt;。这允许您正确地表示具有索引的不可更改的对象集合。

【讨论】:

“不要使用它”并不是一个真正的答案,但你所说的有一些价值。也许您可以通过指出ICollection&lt;T&gt; 也没有IndexOf() 方法,而IList&lt;T&gt; 有方法,让这个看起来更像一个答案。所以这只是语义问题,即集合被视为无序包(可能,但这始终是这些“为什么”问题的问题:谁知道?)。 @GertArnold 你是对的。我依靠用户最后阅读我的评论。既然您已经指出了这一点,我就可以看到问题所在。我将编辑我的答案以使其更完整。 主要问题是IReadOnlyList&lt;T&gt; also 没有实现IndexOf() 函数。 IReadOnlyCollection 没有它没什么大不了的,因为它没有索引语义,但使用列表的全部意义在于它应该是有序和可索引的。 List&lt;T&gt;IList&lt;T&gt; 都有,但 IReadOnlyList 没有。这很令人沮丧。【参考方案5】:

这可能对某人有帮助:

public static int IndexOf<T>(this IReadOnlyList<T> self, Func<T, bool> predicate)

    for (int i = 0; i < self.Count; i++)
    
        if (predicate(self[i]))
            return i;
    

    return -1;

【讨论】:

嗨,欢迎来到 ***!感谢您的贡献。请查看 Mike Nakis 的答案,并考虑您的答案是否有点重复。如果不是,请编辑您的答案并提供一些额外的解释和调用您的函数的代码示例,尤其是关于谓词参数。谢谢! 和迈克的差不多。唯一的区别是它使用谓词,所以你可以像这样使用它: var index = list.IndexOf(obj => obj.Id == id)

以上是关于为啥 IReadOnlyCollection 有 ElementAt 但没有 IndexOf的主要内容,如果未能解决你的问题,请参考以下文章

IReadOnlyCollection 上的隐式/显式转换混淆

不变性/只读语义(特别是 C# IReadOnlyCollection<T>)

List<T>.AsReadOnly() 与 IReadOnlyCollection<T>

对参数使用 IReadOnlyCollection<T> 而不是 IEnumerable<T> 以避免可能的多次枚举

有没有办法将 IReadOnlyCollection<T>/IReadOnlyList<T> 与 protobuf-net 一起使用

无法将 Dictionary<string, List<string>> 转换为 IReadOnlyDictionary<string, IReadOnlyCollect