【问题标题】:Properly exposing a List<T>?正确公开 List<T>?
【发布时间】:2009-08-25 07:28:17
【问题描述】:

我知道我不应该在属性中公开List&lt;T&gt;,但我想知道这样做的正确方法是什么?例如,这样做:

public static class Class1
{
    private readonly static List<string> _list;

    public static IEnumerable<string> List
    {
        get
        {
            return _list;
            //return _list.AsEnumerable<string>(); behaves the same
        }
    }

    static Class1()
    {
        _list = new List<string>();
        _list.Add("One");
        _list.Add("Two");
        _list.Add("Three");
    }
}

将允许我的调用者简单地转换回List&lt;T&gt;

    private void button1_Click(object sender, EventArgs e)
    {
        var test = Class1.List as List<string>;
        test.Add("Four"); // This really modifies Class1._list, which is bad™
    }

所以如果我想要一个真正不可变的List&lt;T&gt;,我是否总是需要创建一个新列表?例如,这似乎有效(转换后测试为空):

    public static IEnumerable<string> List
    {
        get
        {
            return new ReadOnlyCollection<string>(_list);
        }
    }

但我担心每次有人尝试访问我的列表时都会克隆我的列表是否会产生性能开销?

【问题讨论】:

  • 将 List(of T) 公开为属性有什么问题?
  • 我不明白这是什么意思?我的意思是,我只是将属性公开为 List... ???这是线程安全的问题吗?
  • 你需要扩展什么?
  • 这篇文章举了一个例子:如果你想在添加/删除项目时得到通知,你就输了,因为你不能用 List 做到这一点,所以你必须对您的公共界面。同样在我的情况下,它更多的是关于获得一个只读列表而没有太多开销。

标签: c# .net


【解决方案1】:

List&lt;T&gt; 暴露为属性实际上并不是万恶之源。特别是如果它允许预期的用法,例如foo.Items.Add(...)

您可以为AsEnumerable() 编写一个演员安全的替代方案:

public static IEnumerable<T> AsSafeEnumerable<T>(this IEnumerable<T> data) {
    foreach(T item in data) yield return item;
}

但你目前最大的问题是线程安全。作为静态成员,您可能会遇到很大的问题,尤其是在 ASP.NET 之类的东西中。即使ReadOnlyCollection 超过现有列表也会受到此影响:

        List<int> ints = new List<int> { 1, 2, 3 };
        var ro = ints.AsReadOnly();
        Console.WriteLine(ro.Count); // 3
        ints.Add(4);
        Console.WriteLine(ro.Count); // 4

所以简单地用AsReadOnly 包装是不够 足以使您的对象线程安全;它只是防止消费者添加数据(但他们仍然可以在您的其他线程添加数据时枚举它,除非您同步或制作副本)。

【讨论】:

  • s/exiting/existing/ :P(但 +1!)
  • 我总是忘记 yield 的存在...... ASP.net 中的线程安全并不是什么大问题,因为我尽量不在 ASP.net 中使用可变静态类,因为它们是跨请求共享的。我假设如果我想要一个“快照”,无论如何都没有办法绕过副本,所以 return new List&lt;string&gt;(_list); 会“解决”这个问题,如果调用者将它转换回 List 他只会修改他自己的副本而不是矿。真正的要点是让 Class1._list 对外部调用者“不可变”,并在 Class1 中公开 Add/Remove 函数。
  • 是的,除了有人更改列表中 objects 的属性之外,完整副本可以防止所有邪恶。
【解决方案2】:

【讨论】:

  • AsReadOnly 只是调用ReadOnlyCollection 的构造函数,方式与问题中的示例代码完全相同。
  • 我仍然会使用它。要点是它的存在是有原因的,并且它的文档详细说明了它的作用。话虽如此,不,我的回答并没有添加 Marc 没有的任何内容!
  • 在 Henrik 和 Marc 的回答之间纠结。接受这个是因为我不知道 AsReadOnly(),因为它非常简洁,因为它解决了问题,因为我可以缓存它(创建一个私有只读静态 ReadOnlyCollection _listWrapper;,在构造函数中设置它,然后归还),而且这似乎是正确的做法(我看到 Eric Lippert 提到它是一种很好的包装方式)。线程安全可能是一个问题,但无论如何这是一个普遍问题。
  • 我很荣幸。公平竞争,您对选择的考虑程度。不过,我敢肯定,我并不是唯一一个对 Marc 富有洞察力的回答 +1 的人 - 希望这些分数能减轻他没有得到绿色的痛苦!谢谢!
【解决方案3】:

是和否。是的,存在性能开销,因为创建了一个新对象。不,您的列表没有被克隆,它由 ReadOnlyCollection 包装。

【讨论】:

    【解决方案4】:

    如果该类没有其他用途,您可以从 list 继承并覆盖 add 方法并让它抛出异常。

    【讨论】:

    • 您必须从 Collection&lt;T&gt; 继承才能执行此操作...List&lt;T&gt; 没有很多 virtual 成员。
    • 但这将违反 LSP,但仍然是一个有趣的“黑客想法”。
    • 这可能是一种选择,只是“感觉”不对,因为我认为框架应该已经提供了一个开箱即用的合适机制。
    【解决方案5】:

    您无需担心克隆的开销:使用 ReadOnlyCollection 包装集合不会克隆它。它只是创建一个包装器;如果底层集合发生变化,只读版本也会发生变化。

    如果您担心一遍又一遍地创建新的包装器,可以将其缓存在单独的实例变量中。

    【讨论】:

      【解决方案6】:

      我之前问过一个类似的问题:

      基于此,我建议您在内部使用List&lt;T&gt;,并将其作为Collection&lt;T&gt;IList&lt;T&gt; 返回。或者,如果只需要枚举而不是添加或添加类似的东西,IEnumerable&lt;T&gt;

      关于能够将你返回的东西投射到其他东西的问题,我只想说不要打扰。如果人们想以非预期的方式使用您的代码,他们将能够以某种方式使用您的代码。我之前也问过这个问题,我想说唯一明智的做法是公开你的意图,如果人们以不同的方式使用它,那是他们的问题:p 一些相关问题:

      【讨论】:

        【解决方案7】:

        如果您将您的列表公开为 IEnumerable,我不会担心调用者会转换回列表。您已在类的合同中明确指出,此列表中仅允许 IEnumerable 中定义的操作。因此,您已含蓄地声明该列表的实现几乎可以更改为实现 IEnumerable 的任何内容。

        【讨论】:

        • 这是真的,正如 Svish 指出的那样,如果不深度复制数据,无论如何(几乎?)不可能做到这一点,但我想设置一个小障碍来防止人们在脚下开枪太容易了。
        【解决方案8】:

        当您的枚举处于中途并且集合被修改时,AsEnumerable 和 ReadOnlyCollection 会出现问题。这些东西不是线程安全的。将它们作为数组返回并在调用时缓存它们可能是更好的选择。

        例如,

        public static String[] List{
           get{
              return _List.ToArray();
           }
        } 
        
        //While using ...
        
        String[] values = Class1.List;
        
        foreach(string v in values){
          ...
        }
        
        // instead of calling foreach(string v in Class1.List)
        // again and again, values in this context will not be
        // duplicated, however values are cached instance so 
        // immediate changes will not be available, but its
        // thread safe
        foreach(string v in values){
          ...
        }
        

        【讨论】:

        • 公共类合约中的数组被认为是不好的做法
        • 并非总是如此,但 Eric Lippert 对此有一些想法:blogs.msdn.com/ericlippert/archive/2008/09/22/…
        • @Przemaas,没有像数组这样的一般规则是不好的做法,它总是取决于情况,如果你反汇编并查看 List 的来源,它也有一个数组来存储自己的项目。当您不想修改内容,并且需要只读集合时,数组是唯一最快的方法。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-04-30
        • 1970-01-01
        • 2016-05-18
        • 1970-01-01
        • 1970-01-01
        • 2018-05-25
        • 2021-02-13
        相关资源
        最近更新 更多