IEnumerable与IReadonlyCollection与ReadonlyCollection用于公开列表成员


74

我花了好几个小时思考一下暴露名单成员的话题。在与我类似的问题中,乔恩·斯基特(Jon Skeet)给出了一个很好的答案。请随时查看。

ReadOnlyCollection或IEnumerable用于公开成员集合?

我通常对公开列表非常偏执,尤其是在开发API时。

我一直使用IEnumerable公开列表​​,因为它非常安全,并且具有很大的灵活性。让我在这里使用示例:

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IEnumerable<WorkItem> WorkItems
    {
        get
        {
            return this.workItems;
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}

任何针对IEnumerable进行编码的人在这里都是非常安全的。如果以后我决定使用有序列表或其他东西,它们的代码都不会中断,而且还是不错的。IEnumerable的缺点是可以将其强制转换回此类之外的列表。

因此,许多开发人员使用ReadOnlyCollection公开成员。这是非常安全的,因为它永远都不能返回列表。对我来说,我更喜欢IEnumerable,因为它提供了更多的灵活性,如果我想实现不同于列表的东西的话。

我提出了一个我更喜欢的新主意。使用IReadOnlyCollection:

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IReadOnlyCollection<WorkItem> WorkItems
    {
        get
        {
            return new ReadOnlyCollection<WorkItem>(this.workItems);
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}

我觉得这保留了IEnumerable的一些灵活性,并且封装得很好。

我发布了这个问题,以征询我的想法。您是否喜欢此解决方案而不是IEnumerable?您认为使用ReadOnlyCollection的具体返回值更好吗?这是一场辩论,我想尝试看看我们所有人都能提出哪些优点/缺点。

预先感谢您的输入。

编辑

首先,感谢大家为这里的讨论做出了很大贡献。我确实从每一个人中学到了很多东西,并衷心感谢您。

我要添加一些其他方案和信息。

IReadOnlyCollection和IEnumerable有一些常见的陷阱。

考虑下面的示例:

public IReadOnlyCollection<WorkItem> WorkItems
{
    get
    {
        return this.workItems;
    }
}

即使接口是只读的,也可以将上面的示例转换回列表并进行突变。尽管具有相同的名称,但该接口不能保证不变性。您需要提供一个不变的解决方案,因此您应该返回一个新的ReadOnlyCollection。通过创建新列表(本质上是副本),对象的状态是安全无虞的。

Richiban在他的评论中说得最好:界面只能保证某些事情可以做,而不能保证它不能做。

参见以下示例:

public IEnumerable<WorkItem> WorkItems
{
    get
    {
        return new List<WorkItem>(this.workItems);
    }
}

可以对上面的内容进行强制转换和变异,但是您的对象仍然是不变的。

框外语句的另一个例子是集合类。考虑以下:

public class Bar : IEnumerable<string>
{
    private List<string> foo;

    public Bar()
    {
        this.foo = new List<string> { "123", "456" };
    }

    public IEnumerator<string> GetEnumerator()
    {
        return this.foo.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

上面的类可以具有按所需方式对foo进行突变的方法,但是您的对象永远不能转换为任何种类的列表并进行突变。

卡斯滕·弗曼(CarstenFührmann)对IEnumerables中的收益回报表提出了奇妙的观点。

再次谢谢大家。


2
嗯,我认为您应该阅读出色的Jon Skeet答案:stackoverflow.com/questions/491375/… [可能重复]

1
如果您从顶部阅读问题,您会发现我在我的问题中引用了John Skeet的答案作为类似问题。:)尽管答案写得很好,但我相信有些话题还没有完全解决
Marcel-Is-Hier 2014年

没问题。容易错过。:)
Marcel-Is-Hier 2014年

我不明白如何将IReadOnlyCollection永远无法转换为List,不能我们执行此工作项。??
Ananth

1
嗨Ananth。好问题。自从我想到这个问题已经有一段时间了,此后我学到了很多东西。使用WorkItmes.AsEnumerable()。ToList()将创建列表的副本,并且您的类不会发生突变。在上述情况下,由于返回了一个新的只读集合,因此您将永远无法将其强制转换回列表。这样说,您的演员表将为空。IReadOnlyCollection有一个常见的陷阱。如果您在获取中返回列表,则有人可以将其转换为列表并改变对象的状态。我将更新清晰的问题
马塞尔-是-海尔

Answers:


24

谈到类库,我认为IReadOnly *确实很有用,我认为您做对了:)

这完全是关于不可变集合的。在出现不可变元素和扩大数组之前,这是一项艰巨的任务,因此.net决定在框架中包含一些不同的可变集合,为您实现丑陋的东西,但是恕我直言,他们没有不能为不可变提供正确的方向,这是非常有用的,尤其是在共享性总是PITA的高并发情况下。

如果检查当今其他语言,例如Objective-C,您会发现实际上规则完全相反!他们总是在不同的类之间交换不可变的集合,换句话说,接口公开不可变的,并且在内部它们使用可变的集合(是的,他们当然有),相反,如果希望让外部人员更改集合,则可以公开适当的方法(如果该类是有状态的类)。

因此,我对其他语言的这种小经验使我认为.net列表功能如此强大,但是存在不可变的集合是出于某种原因:)

在这种情况下,不是帮助接口的调用者,而是在更改内部实现时避免他更改所有代码,就像IList vs List一样,但是使用IReadOnly *可以保护自己,即类,以便避免使用无用的保护代码,这种保护代码有时是您也不能编写的(过去在某些代码中,我不得不返回完整列表的克隆以避免此问题) 。


很好的答案。整个想法归结为一个词。封装。当我看到人们将列表公开为带有get和set的属性时,我总是会产生偏执。如此危险并保证会被滥用。非常有趣的是其他语言如何处理不变性
Marcel-Is-Hier 2014年

谢谢!仅出于记录目的,我在Objective-C中谈论的可变/不可变分别是NSArrayNSMutableArray,这个名称本身也可以说:)
Michael Denny

90

到目前为止,答案似乎缺少一个重要方面:

当将anIEnumerable<T>返回给调用方时,他们必须考虑返回的对象是“惰性流”的可能性,例如,使用“ yield return”构建的集合。也就是说IEnumerable<T>,对于IEnumerable的每次使用,可能都必须由调用方支付用于生成的元素的性能损失。(生产力工具“ Resharper”实际上将其指出为代码气味。)

相反,IReadOnlyCollection<T>会向呼叫者发出信号,表示不会进行延迟评估。(该Count属性,而不是在Count 扩展方法IEnumerable<T>(这是由遗传IReadOnlyCollection<T>因此它具有方法为好),信号的非lazyness。等确实存在似乎是IReadOnlyCollection不懒惰实现的事实)。

这对于输入参数也是有效的,因为请求一个方法IReadOnlyCollection<T>而不是IEnumerable<T>信号,该方法需要在集合上迭代几次。当然,该方法可以从中创建其自己的列表IEnumerable<T>并对其进行迭代,但是由于调用方可能已经手头有一个已加载的集合,因此在任何可能的情况下都可以利用它。如果呼叫者IEnumerable<T>手边只有一个,则只需要在参数上添加.ToArray().ToList()即可。

IReadOnlyCollection不能做的是防止调用方强制转换为其他集合类型。为了获得这种保护,必须使用class ReadOnlyCollection<T>

总之,只有IReadOnlyCollection<T>到做相对IEnumerable<T>的就是添加一个Count属性,从而指示没有lazyness参与。


5
不幸的是,IReadOnly *中没有任何内容表明实现是否声称是不可变的,或者甚至是只读的(只读对象可以与不允许更改对象的代码安全地共享,但允许请参阅将来由具有相关“特殊”引用的其他人对对象所做的更改;许多实现IReadOnly *的类不满足该条件。
2015年

1
@Richiban:我会称呼它IReadableList<T>。我会保留IReadOnlyXX<t>一些保证没有合法实现会暴露出变异手段的东西(这意味着它们可以与不允许修改基础集合的事物安全地共享),但不能保证不会将基础数据存储区以其他方式修改。
超级猫

3
@supercat是的,IReadableList还不错...但是IReadOnly*完全没有任何意义,因为接口只能保证某些事情可以做,而不能保证它不能做。
里奇班

2
@nawfal不幸的是,ICollection <T>和IReadOnlyCollection <T>都继承自IEnumerable <T>,但彼此不继承。它们都提供了Count属性,但这是要抓住的地方:LINQ的Count()扩展方法针对ICollection <T>(而不是IReadOnlyCollection <T> 进行了优化。这是最不幸的。github.com/microsoft/referencesource/blob/master/System.Core/…–

2
@dfhwze您的评论令人不安。我检查了Microsoft的几种衍生产品IReadOnlyCollection<T>。幸运的是,它们全部也实现ICollectionICollection<T>。微软仍然为人们编写自己的衍生产品留下了陷阱IReadOnlyCollection<T>
卡斯滕·弗曼(CarstenFührmann)

5

看来您可以只返回适当的接口

...
    private readonly List<WorkItem> workItems = new List<WorkItem>();

    // Usually, there's no need the property to be virtual 
    public virtual IReadOnlyList<WorkItem> WorkItems {
      get {
        return workItems;
      }
    }
...

由于workItems现场是事实List<T>,因此很自然的想法恕我直言是揭露最宽的界面,该界面IReadOnlyList<T>的情况下


5
您不能将其转换回List<>问题中提到的IEnumerable<>s吗?
罗林

1
@Rawling:由于ReadOnlyCollection继承自List,因此将其强制转换为列表将使其保留为ReadOnlyCollection。这样做然后尝试添加元素将导致NotSupportedException:集合是只读的。
贾斯汀·斯塔克

1
另一种选择是使用List.Skip(0)返回IEnumerable而不是强制转换,以防止将IEnumerable显式强制转换回List。参见stackoverflow.com/a/491591/1025728
贾斯汀·J·史塔克2014年

2
@JustinJStarkReadOnlyCollection不参与此处。界面List<T>后面有一个隐藏的内容IReadOnlyList<T>,作为问题和第二条注释的注释,您只需将其投射回即可List<T>
Rawling 2014年

3
由于返回的值仍可以强制转换List<T>和修改,因此我将添加调用AsReadOnly并返回。这将确保内部值不被修改。 get { return workItems.AsReadOnly(); }
SD的JG,

0

!! IEnumerable vs IReadOnlyList!

IEnumerable从一开始就与我们同在。多年来,这是代表只读集合的​​事实上的标准方法。从.NET 4.5开始,但是,还有另一种方法:IReadOnlyList。

这两个收集接口都是有用的。

<>


0

我关注转换合同和IReadOnly *合同,以及此类合同的“正确”用法。

如果某些代码“足够聪明”以执行显式转换并破坏接口协定,那么它也足够“聪明”以使用反射或进行其他有害的事情,例如访问ReadOnlyCollection包装对象的基础List 。我不反对这种“聪明”的程序员。

我唯一保证的是,暴露了上述IReadOnly * -interface对象之后,我的代码不会违反该合同,也不会修改返回的集合对象。

这意味着我编写的代码例如返回List-as-IReadOnly *,并且很少选择实际的只读具体类型或包装器。使用IEnumerable.ToList足以返回IReadOnly [List | Collection]-调用List.AsReadOnly对于仍然可以访问ReadOnlyCollection包装的基础列表的“聪明”程序员来说,几乎没有增加任何价值。

在所有情况下,我都保证IReadOnly *返回值的具体类型是热切的。如果我曾经写过一个返回IEnumerable的方法,则特别是因为该方法的约定是“支持流式” fsvo的约定。

就IReadOnlyList和IReadOnlyCollection而言,无论是否有意进行排序,只要存在对索引有意义的“'”隐含稳定排序,我都使用前者。例如,数组和列表可以作为IReadOnlyList返回,而HashSet最好作为IReadOnlyCollection返回。调用方始终可以根据需要将I [ReadOnly] List分配给I [ReadOnly] Collection:此选择与公开的合同有关,而不是程序员(“聪明人”或其他人)会做什么。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.