【问题标题】:How can I safely return List<T> from method/property declared as IEnumerable<T>?如何从声明为 IEnumerable<T> 的方法/属性中安全地返回 List<T>?
【发布时间】:2014-01-08 00:46:09
【问题描述】:

假设我有 IEnumerable&lt;int&gt; 属性支持 List&lt;int&gt; 字段,所以我可以在类中修改集合,但它公开为只读。

public class Foo
{
    private List<int> _bar = new List<int>();

    public IEnumerable<int> Bar
    {
        get { return _bar; }
    }
}

但是使用这样的代码,您可以轻松地将从属性中检索到的对象转换回 List&lt;int&gt; 并对其进行修改:

var foo = new Foo();
var bar = (List<int>)foo.Bar;
bar.Add(10);

问题是:什么是最好的(最好的可读性,最容易编写,没有性能损失)避免这种情况的方法?

我可以想出至少 4 个解决方案,但没有一个是完美的:

  1. foreachyield return

    public IEnumerable<int> Bar
    {
        get
        {
            foreach (var item in _bar)
                yield return item;
        }
    }
    

    - 写和读真的很烦人。

  2. AsReadOnly():

    public IEnumerable<int> Bar
    {
        get { return _bar.AsReadOnly(); }
    }
    
    当有人试图修改返回的集合时,

    + 会引发异常
    + 不会创建整个集合的副本。

  3. ToList()

    public IEnumerable<int> Bar
    {
        get { return _bar.ToList(); }
    }
    

    + 用户仍然可以修改检索到的集合,但它与我们在类中修改的集合不同,所以我们不应该关心。
    - 创建整个集合的副本,当集合很大时可能会导致问题。

  4. 自定义包装器类。

    public static class MyExtensions
    {
        private class MyEnumerable<T> : IEnumerable<T>
        {
            private ICollection<T> _source;
            public MyEnumerable(ICollection<T> source)
            {
                _source = source;
            }
    
            public IEnumerator<T> GetEnumerator()
            {
                return _source.GetEnumerator();
            }
    
            IEnumerator IEnumerable.GetEnumerator()
            {
                return ((IEnumerable)_source).GetEnumerator();
            }
        }
    
        public static IEnumerable<T> AsMyEnumerable<T>(this ICollection<T> source)
        {
            return new MyEnumerable<T>(source);
        }
    }
    

    用法:

    public IEnumerable<int> Bar
    {
        get
        {
            return _bar.AsMyEnumerable();
        }
    }
    

    + 不需要克隆集合
    - 当您将其用作 LINQ 查询源时,某些方法不会使用 ICollection.Count,因为您不要暴露它。


有没有更好的方法来做到这一点?

【问题讨论】:

  • 2 和 4 似乎几乎相同,而且这个问题似乎是基于意见的(并且:基于上下文的又过于宽泛)。
  • 我通常使用 (ReadOnlyCollection)msdn.microsoft.com/en-us/library/ms132474(v=vs.110).aspx 它已准备好使用标准类来包装您的列表。
  • @PetkoPetkov 这就是他的选项 2 已经做的......

标签: c# list ienumerable


【解决方案1】:

问题是:避免这种情况的最佳(最佳可读性、最容易编写、没有性能损失)方法是什么?

一般来说,我不会试图避免它。我的 API 的使用者应该使用我公开的类型,如果他们不这样做,那么产生的任何错误都是他们的错,而不是我的错。因此,我真的不在乎他们是否以这种方式转换数据 - 当我更改我的内部表示时,他们得到转换异常,这就是他们的问题。

话虽如此,如果存在安全问题,我可能会使用AsReadOnly。这实际上是自记录的,并且没有真正的缺点(除了为包装器分配的少量内存,因为没有数据副本,您确实会在修改时获得有意义的异常等)。与制作您自己的自定义包装器相比,这并没有真正的劣势,而且自定义包装器意味着要测试和维护更多代码。

一般来说,我个人尽量避免无故复制。这将消除ToList() 作为一般选项。使用迭代器(您的第一个选项)并没有那么糟糕,尽管它并没有真正提供比 ReadOnlyCollection&lt;T&gt; 更多的优势。

【讨论】:

  • +1 因为我坚决同意这两点。保护其他开发人员不做愚蠢的事情不是我的工作,如果我的返回类型是IEnumerable&lt;T&gt;,请自行承担风险。除非开发人员 1) 可以访问您的源代码或 2) 正在反编译它,否则他们应该不知道这甚至是一种选择。
【解决方案2】:

总的来说,我属于“请勿打扰”阵营。如果我给你一个 IEnumerable 并且你将它转换为其他东西,那么你就是违反合同的人,如果有东西破裂,这不是我的错。如果我发现自己确实需要保护客户不破坏我的状态,我会创建一个扩展方法,例如:

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

再也不用担心了。

【讨论】:

  • 不过,这并不是万无一失的。您仍然可以通过反射访问底层集合,因此它并不是真正的“安全”;)
  • @ReedCopsey IMO 如果您需要避免反射,那么您需要在调用者和被调用者之间建立一个进程边界
【解决方案3】:

我认为您已经充分概述了许多解决方案,但我没有看到您考虑的一件事是分配开销(但性能被列为关注点)。分配很重要,让属性在每次调用同一个列表时分配一个新对象是浪费的。相反,我更喜欢生命周期等于原始列表的对象,可以在没有额外开销的情况下返回。本质上是 #4 的修改版本

【讨论】:

    【解决方案4】:

    我同意 Reed Copsey 的帖子,如果消费者希望以会破坏类数据的方式投射和管理数据,那么就让他们去做吧。

    你可以花很多时间试图“阻止”某个特定动作的发生,只是为了让一些聪明的反射破坏你所有的硬词。

    现在回答这个问题,如果你必须这样做的话。第一个选项似乎与我最相关,但是在调用该属性时我不会枚举列表。我只会返回内部集合,例如。

    public IEnumerable<int> Bar
    {
        get { return _bar; }
    }
    

    【讨论】:

      猜你喜欢
      • 2021-08-25
      • 2023-03-31
      • 2016-09-23
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 1970-01-01
      • 1970-01-01
      • 2011-08-17
      相关资源
      最近更新 更多