【问题标题】:Encapsulate Collection in C#在 C# 中封装集合
【发布时间】:2011-09-16 12:04:44
【问题描述】:

自 3.0 以来,C# 具有出色的语法糖,例如自动属性,大大简化了封装原则的实现。如果您将它与原子值一起使用,这很好,因此您可以像这样替换封装模式:

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) 使用自动属性。如果类只有一个构造函数,这种方法是可以的:

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

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

但是当您以这种方式处理诸如列表之类的可变集合时,您会破坏封装,因为此属性的用户可以在不让容器知道的情况下修改集合(如果您忘记将 setter 设为私有,甚至可以替换集合)。

就我而言,关于封装集合模式,集合封装的正确实现应该是这样的:

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# 团队没有提供一些清晰简单的方法来定义集合自动属性,以便开发人员可以在创建健壮代码时满足他们的懒惰?

【问题讨论】:

  • cmets 中有人提到(但不知何故删除了他的评论)ICollection 包含修改方法,这会引发 NotSupported 异常。因此,制作 ReadOnlyCollection 类型的只读公共属性(在我们的例子中为 Name)是更好的解决方案。

标签: c# .net automatic-properties


【解决方案1】:

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>();

确保集合永远不会为空是个好主意,否则您可以只使用自动属性。除非我有充分的理由希望对我的集合进行更多封装,否则为了简单起见,我总是使用这种方法。

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>();

公开的集合现在封装了我们关于如何修改和不能修改集合的规则,并且还立即反映了对基础集合所做的更改(这可能是一件好事,也可能不是一件好事)。对于大型集合,它也可能更有效。

【讨论】:

  • 关于 Collection getter 的注意事项,每次调用 getter 时创建一个新的集合实例真的有意义吗?
  • @Sll 你在说哪个吸气剂?自从您发表评论后,我想我已经更新了问题...
  • @sll,你是对的,实际上它没有意义。但这并不是一个很大的努力,因为 ReadOnlyCollection 只存储指向源列表的链接,其他任何东西:java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/…
【解决方案2】:
private IList<string> _list = new List<string>();

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

【讨论】:

  • 不幸的是,您的解决方案不允许修改集合,而这通常是需要的。更重要的是,将 List 属性从 IEnumerable 转换为 IList 允许您对其进行修改,这是封装中的大漏洞
  • @Veton 如果您需要一个确实需要以某种方式封装/只读的集合(例如ControlCollection 类),那么您确实需要实现自己的集合来封装这些规则.
  • @Justin 为什么要重新发明轮子? ReadOnlyCollection 完全足够了。另请注意,ControlCollection 不是不可变的:msdn.microsoft.com/en-us/library/…
  • @Vitaliy Ulantikov 不理解您的评论。为什么不能修改收藏?您可以轻松地在 _list 中添加或删除对象。关键是任何其他通过属性​​“List”使用_list的对象只能迭代而不能改变。
  • @sound 如果您返回 ControlCollection,任何人都可以在不通知您的情况下对其进行修改 - 这是一个问题。如果您将转换为 IEnumerable 的 IList 返回 - 任何人都可以将其转换回 IList 并进行修改。
【解决方案3】:

每当我必须公开一个集合时,我倾向于使用IEnumerable&lt;T&gt;,但显然你不能使用自动属性来执行此操作。

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

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

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

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

【讨论】:

  • IEnumerable 不像 ICollection 那样为您提供 Count 属性和 Contains(T item) 方法。但我认为 IList 将是这里最好的,因为它提供了更多的功能。
【解决方案4】:

公开ICollection&lt;string&gt; 不是一个好主意,因为它允许添加和删除操作。

 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);
        }
    }

编辑:

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

此外,为了确保后台的集合确实是只读的,您可以查看IsReadOnly Property

MSDN 说:

ReadOnlyCollection.ICollection.IsReadOnly 属性如果 ICollection 是只读的;否则为假。在默认 ReadOnlyCollection 的实现,这个属性总是返回 真的。

【讨论】:

  • 您的示例无法编译(返回列表为 ReadOnlyCollection!)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-06
  • 2011-12-22
  • 2013-01-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多