如何安全地从声明为IEnumerable <t>的方法/属性返回List <t>?</t> </t>

时间:2014-01-08 00:46:09

标签: c# list ienumerable

假设我有IEnumerable<int>属性支持List<int>字段,因此我可以在类中修改集合,但它是以只读方式公开显示的。

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

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

但是使用这样的代码,您可以轻松地将从属性中检索到的对象转换回List<int>并修改它:

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,因为您不会公开它。


  5. 有没有更好的方法呢?

4 个答案:

答案 0 :(得分:13)

  

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

一般来说,我不会试图避免它。我的API的消费者应该使用我公开的类型,如果他们不这样做,任何错误都是他们的错,而不是我的。因此,我并不关心他们是否以这种方式投放数据 - 当我更改内部表示时,他们会获得强制转换异常,这就是他们的问题。

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

一般情况下,我个人试图避免无理由地复制 。这将取消ToList()作为一般选项。使用迭代器(你的第一个选项)并不是那么糟糕,尽管它并没有提供比ReadOnlyCollection<T>更多的优势。

答案 1 :(得分:1)

总的来说,我在不打扰营地。如果我给你一个IEnumerable而你把它投入其他东西你就是那个违反合同的人,如果有什么东西坏了就不是我的错。如果我发现自己处于一个我真正需要保护客户端免受破坏状态的位置,我会创建一个扩展方法,如:

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

再也不用担心了。

答案 2 :(得分:0)

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

的修改版本

答案 3 :(得分:0)

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

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

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

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