IList<T> 和 IReadOnlyList<T>

Posted

技术标签:

【中文标题】IList<T> 和 IReadOnlyList<T>【英文标题】:IList<T> and IReadOnlyList<T> 【发布时间】:2012-10-02 00:47:10 【问题描述】:

如果我有一个方法需要一个参数,

有一个Count 属性 具有整数索引器(仅限获取)

这个参数的类型应该是什么?我会在 .NET 4.5 之前选择 IList&lt;T&gt;,因为没有其他可索引的集合接口用于此,并且数组实现了它,这是一个很大的优势。

但是.NET 4.5 引入了新的IReadOnlyList&lt;T&gt; 接口,我希望我的方法也支持它。我怎样才能编写这个方法来支持IList&lt;T&gt;IReadOnlyList&lt;T&gt;而不违反像DRY这样的基本原则?

编辑:丹尼尔的回答给了我一些想法:

public void Foo<T>(IList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

public void Foo<T>(IReadOnlyList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

private void Foo<TList, TItem>(
    TList list, int count, Func<TList, int, TItem> indexer)
    where TList : IEnumerable<TItem>

    // Stuff


编辑 2: 或者我可以接受 IReadOnlyList&lt;T&gt; 并提供这样的帮助:

public static class CollectionEx

    public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> list)
    
        if (list == null)
            throw new ArgumentNullException(nameof(list));

        return list as IReadOnlyList<T> ?? new ReadOnlyWrapper<T>(list);
    

    private sealed class ReadOnlyWrapper<T> : IReadOnlyList<T>
    
        private readonly IList<T> _list;

        public ReadOnlyWrapper(IList<T> list) => _list = list;

        public int Count => _list.Count;

        public T this[int index] => _list[index];

        public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    

那我可以叫它Foo(list.AsReadOnly())


编辑 3: 数组实现了 IList&lt;T&gt;IReadOnlyList&lt;T&gt;List&lt;T&gt; 类也是如此。这使得很难找到实现IList&lt;T&gt; 但不实现IReadOnlyList&lt;T&gt; 的类。

【问题讨论】:

相关问题:***.com/questions/12622539/… 2021 年 1 月更新:看起来它会及时为 .NET 5 完成,但它仍然是一个悬而未决的问题:github.com/dotnet/runtime/issues/31001 【参考方案1】:

你在这里不走运。 IList&lt;T&gt; 没有实现 IReadOnlyList&lt;T&gt;List&lt;T&gt; 确实实现了这两个接口,但我认为这不是你想要的。

但是,您可以使用 LINQ:

Count() 扩展方法在内部检查实例是否实际上是一个集合,然后使用Count 属性。 ElementAt() 扩展方法在内部检查实例是否实际上是一个列表,然后使用索引器。

【讨论】:

所以答案是改变方法,使其接受 IEnumerable,任何实现 IList 或 IReadOnlyList 的对象都支持该方法。 如果只允许IList&lt;T&gt;IReadOnlyList&lt;T&gt; 作为输入很重要,我将创建两个公共重载,一个用于IList&lt;T&gt;,一个用于IReadOnlyList&lt;T&gt;,并使用IEnumerable&lt;T&gt;。否则我只会用IEnumerable&lt;T&gt; 创建一个公开版本。 谢谢,我决定不使用 LINQ,但它给了我一些想法。我用我将要使用的解决方案更新了问题。 @JeppeStigNielsen:虽然未来实现的任何合理列表类型都应该实现IList&lt;T&gt;IReadOnlyList&lt;T&gt;,但编写了许多实现IList&lt;T&gt;的好类在IReadOnlyList&lt;T&gt; 存在之前。我一直希望有一种方法可以使IList&lt;T&gt; 继承自带有索引 getter 的协变只读接口,并在运行时(如有必要)自动创建一个只读索引器,该索引器将包装读写的,但由于不存在这样的方法,现在有必要接受IList&lt;T&gt;IReadOnlyList&lt;T&gt; LINQ 扩展方法仅检查您的实例是否实现 IList。因此,如果您有一个仅实现 IReadOnlyList 而不是 IList 的类的实例,那么您就不走运了,LINQ 将笨拙地遍历整个集合(在 .net 4.7 上测试,并检查了 .net 核心的来源)。幸运的是,这种情况很少见,但可以通过自定义实现发生。【参考方案2】:

由于IList&lt;T&gt;IReadOnlyList&lt;T&gt; 不共享任何有用的“祖先”,如果您不希望您的方法接受任何其他类型的参数,您唯一能做的就是提供两个重载。

如果您决定重用代码是首要任务,那么您可以让这些重载将调用转发到接受 IEnumerable&lt;T&gt;private 方法,并按照 Daniel 建议的方式使用 LINQ,实际上让 LINQ 在运行时。

但是恕我直言,最好只复制/粘贴一次代码并保留两个独立的重载,它们仅在参数类型上有所不同;我不相信这种规模的微架构提供任何有形的东西,另一方面它需要不明显的操作并且速度较慢。

【讨论】:

我强烈反对复制/粘贴,即使在这个级别上也是如此。 @DanielHilgarth:我相信“看到第三个副本就重构”。 无条件地忽略复制/粘贴听起来不像是好的工程。 唯一的区别是输入类型,其他都完全一样。使用复制和粘贴,您会发现一些细微的错误。您可能修复了一个版本,但忘记了另一个。我不明白为什么从一开始就阻止这样的事情不是好的工程。您的方法允许相同代码的两个副本有什么优势?我不这样做的方法有什么缺点? @DanielHilgarth:当然可以,但是答案中提到的要求暗示它不会那么糟糕(仅使用Count 和索引器会变得多么复杂?)。如果我们谈论的是 5 行代码,我会复制/粘贴并记录。否则,其他一些解决方案可能会更可取。我只是说彻底禁止复制/粘贴不是一个好主意。 第一:我从来没有说过“彻底禁止复制/粘贴”,这是你对我强烈反对使用它的错误解释。对我来说,它是与 goto 处于同一级别的“工具”:在极少数情况下,它有它的用途。第二:您仍然没有提出任何原因来说明为什么要复制和粘贴随之而来的所有问题。你甚至写你会记录它。当有一个简单的修复可用时,此文档将包含复制的原因是什么?【参考方案3】:

如果您更关心维护DRY 的原则而不是性能,您可以使用dynamic,如下所示:

public void Do<T>(IList<T> collection)

    DoInternal(collection, collection.Count, i => collection[i]);

public void Do<T>(IReadOnlyList<T> collection)

    DoInternal(collection, collection.Count, i => collection[i]);


private void DoInternal(dynamic collection, int count, Func<int, T> indexer)

    // Get the count.
    int count = collection.Count;

但是,我不能真诚地说我会推荐这个,因为陷阱太大了:

DoInternal 中对collection 的每个调用都将在运行时解决。您会失去类型安全、编译时检查等。 性能下降(虽然不严重,对于单一情况,但在聚合时可能会发生)会发生

您的帮助建议是最有用的,但我认为您应该翻转它;鉴于 IReadOnlyList&lt;T&gt; interface 是在 .NET 4.5 中引入的,许多 API 不支持它,但支持 IList&lt;T&gt; interface。

也就是说,您应该创建一个AsList 包装器,它接受IReadOnlyList&lt;T&gt; 并在IList&lt;T&gt; 实现中返回一个包装器。

但是,如果您想在 API 上强调您正在使用 IReadOnlyList&lt;T&gt;(强调您没有改变数据这一事实),那么您现在拥有的 AsReadOnlyList 扩展名会更合适,但我会对AsReadOnly进行以下优化:

public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> collection)

    if (collection == null)
        throw new ArgumentNullException("collection");

    // Type-sniff, no need to create a wrapper when collection
    // is an IReadOnlyList<T> *already*.
    IReadOnlyList<T> list = collection as IReadOnlyList<T>;

    // If not null, return that.
    if (list != null) return list;

    // Wrap.
    return new ReadOnlyWrapper<T>(collection);

【讨论】:

+1 以获得洞察力并感谢您的建议,我选择了 IList&lt;T&gt;IReadOnlyList&lt;T&gt;,因为从显式实现的接口中抛出 NotSupportedExceptions 感觉不对,令我惊讶的是,它也接受数组. (这很奇怪,因为 Array 没有实现它,IReadOnlyList&lt;T&gt; 甚至没有像 IList 这样的非通用版本)我想知道你对我的第一个示例有什么看法。我当时正在考虑使用dynamic,但正如你所说,它可能会损害性能(我会在短时间内大量调用此方法)...... ...所以我假设Count 永远不会改变(不打算线程安全)并且使用委托调用索引器的 get 访问器几乎与直接调用它一样快。所以我猜这些方法的性能差异在于创建ReadOnlyWrapper&lt;T&gt; 的实例和创建Func&lt;int, T&gt; 的实例之间(听起来像是微优化)。 @ŞafakGür ...我还通过优化您的AsReadOnlyList 实施更新了答案。很轻微,但不疼。当IReadOnlyList&lt;T&gt; 已经IReadOnlyList&lt;T&gt; 时,无需用IReadOnlyList&lt;T&gt; 包装。这种类型嗅探一直在 LINQ 中完成。 谢谢你的技巧,我会用的。是的,数组实现了IList&lt;T&gt;,但奇怪的是,new int[0] is IReadOnlyList&lt;int&gt; 返回 true。我可以使用数组来调用接受IReadOnlyList&lt;T&gt;的方法。 是的,没必要这么麻烦。只需使该方法采用IReadOnlyList&lt;T&gt;。任何实现IList&lt;T&gt; 的类也将实现IReadOnlyList&lt;T&gt;。这包括像 T[]List&lt;T&gt; 这样的 BCL 类型,并且应该包括你自己的列表类型(如果你写过的话)。【参考方案4】:

您需要的是 .Net 4.5 中可用的IReadOnlyCollection&lt;T&gt;,它本质上是一个IEnumerable&lt;T&gt;,它具有Count 作为属性,但如果您还需要索引,那么您需要IReadOnlyList&lt;T&gt;,它也将提供一个索引器.

我不了解你,但我认为这个界面是必备的,已经丢失了很长时间。

【讨论】:

I think this interface is a must have- 它是一个类,而不是一个接口。它同时实现了IList&lt;T&gt;IReadOnlyList&lt;T&gt; 我的意思是 IReadOnlyCollection 我不知何故错过了“我” IReadOnlyCollection&lt;T&gt;IReadonlyList&lt;T&gt; 有完全相同的问题:ICollection&lt;T&gt; 没有实现它。

以上是关于IList<T> 和 IReadOnlyList<T>的主要内容,如果未能解决你的问题,请参考以下文章

为啥 `IList<T>` 不从 `IReadOnlyList<T>` 继承?

如何序列化 IList<T>?

IList<T> 到 IQueryable<T>

将 IList<T> 转换为 BindingList<T>

C#利用GridView对IList<T>或List<T>进行插入、删除和修改

如何对 IList<T> 执行二进制搜索?