在C#中封装集合

时间:2011-09-16 12:04:44

标签: c# .net automatic-properties

由于3.0 C#具有很好的语法糖,如auto-properties,因此大大简化了封装原理的实现。如果你将它与原子值一起使用,这是很好的,所以你可以像这样替换封装模式:

private string _name;

public string Name 
{
  get { return _name; }
  set { _name = value; }
}

只有一行:

public string FirstName  { get; set; }

我非常喜欢这个很棒的功能,因为它节省了很多开发人员的时间。


但是当你创建指向集合的属性时,事情并不是那么好。 通常我会看到以两种方式之一实现的集合属性。

1)根本没有自动属性可以使用字段初始值设定项:

private List<string> _names = new List<string>();

public List<string> Names
{
    get { return _names; }
}

2)使用自动属性。如果class只有一个构造函数,那么这种方法就可以了:

public List<string> Names { get; private set; }

public .ctor()
{
    Names = new List<string>();
}

但是当你以这样的方式处理像列表这样的可变集合时,你会打破封装,因为这个属性的用户可以修改集合而不让容器知道(如果忘记将setter设为私有,甚至可以替换集合。)

至于我,关于Encapsulate Collection模式,集合封装的正确实现应如下所示:

private readonly List<string> _names = new List<string>();

public ICollection<string> Names
{
    get { return new ReadOnlyCollection<string>(_names); }
}

public void Add_Name(string name)
{
    _names.Add(name);
}

public void Remove_Names(string name)
{
    _names.Remove(name);
}

public void Clear_Names()
{
    _names.Clear();
}

老实说,我不记得我是否在实际代码中遇到过这种实现,即使在框架源代码中也是如此。我认为这是因为人们懒惰并且避免编写大量代码只是为了使封装更加强大。

我想知道为什么C#团队没有提供一些明确而简单的方法来定义集合自动属性,所以开发人员可以取悦他们的懒惰仍然创建健壮的代码?

4 个答案:

答案 0 :(得分:19)

TL;DR,C#编译器没有自动集合,因为有很多不同的方式来公开集合。在展示集合时,您应该仔细考虑如何封装集合并使用正确的方法。


C#编译器提供自动属性的原因是因为它们很常见并且几乎总是以相同的方式工作,但是当你发现处理集合时情况很少这么简单 - 有很多暴露集合的不同方式,正确的方法总是取决于具体情况,仅举几例:

1)可以更改的集合

通常不需要对暴露的集合施加任何真正的限制:

public List<T> Collection
{
    get
    {
        return this.collection;
    }
    set
    {
        if (value == null)
        {
            throw new ArgumentNullException();
        }
        this.collection = value;
    }
}
private List<T> collection = new List<T>();

确保集合永远不为null是个好主意,否则你可以只使用自动属性。除非我有充分的理由想要更多地封装我的集合,否则我总是使用这种方法。

2)可以修改但不能交换的集合

您可以按照自己喜欢的方式编写代码,但想法是一样的 - 暴露的集合允许修改项目,但底层集合本身不能替换为其他集合。例如:

public IList<T> Collection
{
    get
    {
        return this.collection;
    }
}
private ObservableCollection<T> collection = new ObservableCollection<T>();

当消费者应该能够修改集合但我订阅了更改通知时,我倾向于在处理observable collections之类的事情时使用这个简单的模式 - 如果你让消费者交换整个集合那么你会只会引起头痛。

3)公开集合的只读副本

通常,您希望阻止消费者修改已公开的集合 - 通常您执行希望公开类能够修改集合。一种简单的方法是公开集合的只读副本:

public ReadOnlyCollection<T> Collection
{
    get
    {
        return new ReadOnlyCollection<T>(this.collection);
    }
}
private List<T> collection = new List<T>();

这带来了返回集合永远不会更改的属性,即使基础集合发生更改也是如此。这通常是一件好事,因为它允许消费者遍历返回的集合而不用担心它可能会被更改:

foreach (var item in MyClass.Collection)
{
    // This is safe - even if MyClass changes the underlying collection
    // we won't be affected as we are working with a copy
}

然而,这并不总是预期的(或期望的)行为 - 例如Controls property不能以这种方式工作。您还应该考虑以这种方式复制许多大型集合可能效率低下。

当公开只读的集合时,请始终注意控件中的项目仍然可以修改。同样,这可能是一件好事,但如果您希望公开的集合“完全”不可修改,那么请确保集合中的项目也是只读/不可变的(例如System.String)。

4)可以修改但只能以某种方式修改的集合

假设您要公开一个可以添加但不删除项目的集合?您可以在公开类本身上公开属性:

public ReadOnlyCollection<T> Collection
{
    get
    {
        return new ReadOnlyCollection<T>(this.collection);
    }
}
private List<T> collection = new List<T>();

public AddItem(T item);

但是,如果您的对象有很多这样的集合,那么您的界面很快就会变得混乱和混乱。此外,我发现这种模式有时可能违反直觉:

var collection = MyClass.Collection;
int count = collection.Count;

MyClass.AddItem(item);

Debug.Assert(collection.Count > count, "huh?");

它需要付出更多努力,但IMO更简洁的方法是公开一个自定义集合,它封装了您的“真实”集合以及有关如何更改集合的规则,例如:

public sealed class CustomCollection<T> : IList<T>
{
    private IList<T> wrappedCollection;

    public CustomCollection(IList<T> wrappedCollection)
    {
        if (wrappedCollection == null)
        {
            throw new ArgumentNullException("wrappedCollection");
        }
        this.wrappedCollection = wrappedCollection;
    }

    // "hide" methods that don't make sense by explicitly implementing them and
    // throwing a NotSupportedException
    void IList<T>.RemoveAt(int index)
    {
        throw new NotSupportedException();
    }

    // Implement methods that do make sense by passing the call to the wrapped collection
    public void Add(T item)
    {
        this.wrappedCollection.Add(item);
    }
}

使用示例:

public MyClass()
{
    this.wrappedCollection = new CustomCollection<T>(this.collection)
}

public CustomCollection<T> Collection
{
    get
    {
        return this.wrappedCollection;
    }
}
private CustomCollection<T> wrappedCollection;
private List<T> collection = new List<T>();

公开的集合现在将我们的规则包含在如何修改集合的规则中,并立即反映对底层集合所做的更改(这可能是也可能不是一件好事)。对于大型馆藏来说,它也可能更有效。

答案 1 :(得分:4)

private IList<string> _list = new List<string>();

public IEnumerable<string> List
{
  get
  {
    ///return _list;
     return _list.ToList();
  }
}

答案 2 :(得分:1)

公开ICollection<string>并不是一个好主意,因为它允许添加和删除操作。

 public sealed class CollectionHolderSample
    {
        private readonly List<string> names;

        public CollectionHolderSample()
        {
            this.names = new List<string>();
        }

        public ReadOnlyCollection<string> Items
        {
            get
            {
                return this.names;
            }
        }

        public void AddItem(string item)
        {            
            this.names.Add(item);
        }
    }

修改

正如您所提到的Add()Remove()方法来自显式ReadOnlyCollection<T>.ICollection<T>实现会抛出NotSupportedException异常,因此集合是只读的。

此外,为了确保引擎盖下的收集真的是只读,您可以检查IsReadOnly Property

MSDN说:

  

ReadOnlyCollection.ICollection.IsReadOnly Property true如果   ICollection是只读的;否则,错误。在默认情况下   ReadOnlyCollection的实现,此属性始终返回   真。

答案 3 :(得分:1)

每当我必须公开一个集合时,我倾向于使用IEnumerable<T>,但你不能使用自动属性来做到这一点。

我真的希望在.NET中实现的解决方案是不可变集合的概念。这将真正解决您所谈论的问题,因为自动属性可以通过这种方式完美地运行:

 public ImmutableList<Foo> {get; private set; }

以任何方式修改列表的任何人都会获得ImmutableList的新实例,而不是原始列表本身。

您当然可以实现自己的ImmutableList,但我发现.NET Framework不包含这种元素有点奇怪。