用C#封装集合

发布于 2024-12-05 05:24:27 字数 1415 浏览 1 评论 0原文

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

但是,当您以这种方式处理像列表这样的可变集合时,您就破坏了封装,因为该属性的用户可以修改集合而不让容器知道(或者如果您忘记创建集合,甚至可以替换集合)设置器私有)。

对我来说,关于封装集合模式,集合封装的正确实现应该是这样的:

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

Since 3.0 C# has great syntax sugar like auto-properties which a lot simplify implementation of encapsulation principle. This is good if you use it with atomic values, so you can replace encapsulation pattern like this:

private string _name;

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

with just one line:

public string FirstName  { get; set; }

I very like this great feature as it saves a lot of developers time.


But things are not so great when you create property that points to collection.
Usually I see collection properties implemented in one of two ways.

1) Without auto-properties at all to be able to use field initializer:

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

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

2) Using auto-properties. This approach is ok if class has only one constructor:

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

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

But when you deal with mutable collections like lists such a way, you break encapsulation, as user of this property can modify collection without letting container know (or even replace collection if you forget to make setter private).

As for me, regarding to Encapsulate Collection pattern correct implementation of collection encapsulation should look like this:

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

Honestly, I do not remember if I've met this kind of implementation in the real code, even in framework sources. I think this is because people are lazy and avoid writing such amount of code just to make encapsulation just a little bit stronger.

I wondering why C# team does not provide some clear and easy way to define collection auto-properties, so developers can please their laziness still creating robust code?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

黑寡妇 2024-12-12 05:24:27

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

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

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 属性 无法以这种方式工作。您还应该考虑到以这种方式复制许多大型集合可能效率低下。

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

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

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

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

TL;DR, The C# compiler doesn't have auto-collections because there are lots of different ways of exposing collections. When exposing a collection you should think carefully about how you want the collection to be encapsulated and use the correct method.


The reason why the C# compiler provides auto-properties is because they are common and almost always work the same way, however as you are discovering the situation is rarely as simple when dealing with collections - there are many different ways of exposing a collection, the correct method always depends on the situation, to name a few:

1) A collection which can be changed

Often there is no real need to place any real restrictions on the exposed collection:

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

Its can be a good idea to make sure that the collection is never null, otherwise you can just use auto-properties. Unless I have a good reason for wanting more encapsulation of my collection I always use the this method for simplicity.

2) A collection that can be modified, but not swapped

You can code this any way you like, but the idea is the same - the exposed collection allows items to be modified but the underlying collection itself cannot be replaced with another collection. For example:

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

I tend to use this simple pattern when dealing with things like observable collections when the consumer should be able to modify the collection but I've subscribed to change notifications - If you let consumers swap the entire collection then you would just cause headaches.

3) Expose a read-only copy of a collection

Frequently you want to prevent consumers from modifying an exposed collection - usually however you do want the exposing class to be able to modify the collection. An easy way to do this is by exposing a read-only copy of your collection:

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

This comes with the property that the returned collection never changes, even if the underlying collection changes. This is often a good thing as it allows consumers to iterate through the returned collection without fear that it might be changed:

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
}

However this isn't always the expected (or desired) behaviour - for example the Controls property doesn't work this way. You should also consider that copying many large collections in this way is potentially inefficient.

When exposing collections that are read only always be aware that the items in the control can still be modified. Again this might be a good thing, but if you want the exposed collection to be "completely" unmodifiable then make sure that the items in the collection are also read-only / immutable (e.g. System.String).

4) Collections that can be modified, but only in a certain way

Suppose you want to expose a collection that items can be added to, but not removed? You could expose properties on the exposing class itself:

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

public AddItem(T item);

However if your object has many such collections then your interface can quickly get confusing and messy. Also I find this pattern to be potentially counter-intuitive at times:

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

MyClass.AddItem(item);

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

Its a lot more effort, but IMO a neater method is to expose a custom collection that encapsulates your "real" collection and the rules about how the collection can and can't be changed, for example:

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

Example use:

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

The exposed collection now encapsualtes our rules on how the collection can and can't be modified and also immediately reflects changes made to the underlying collection (which may or may not be a good thing). Its also potentially more efficient for large collections.

假情假意假温柔 2024-12-12 05:24:27
private IList<string> _list = new List<string>();

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

public IEnumerable<string> List
{
  get
  {
    ///return _list;
     return _list.ToList();
  }
}
臻嫒无言 2024-12-12 05:24:27

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

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

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

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

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

Whenever I have to expose a collection I tend to use IEnumerable<T> but you can not use Auto-properties to do this obviously.

A solution I would really like to see implemented in .NET is the concept of immutable collections. This would really solve the problem you are talking about as auto-properties would work perfectly fine this way:

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

Anybody who modifies the list in any way would get a new instance of the ImmutableList and not the original list itself.

You can of course implement your own ImmutableList but I find it kind of strange that the .NET Framework doesn't include this kind of element.

幸福丶如此 2024-12-12 05:24:27

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

 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.ICollection 实现将抛出 NotSupportedException 异常,因此集合是只读的。

另外,为了确保后台集合确实是只读的,您可以检查 IsReadOnly 属性。

MSDN 说:

ReadOnlyCollection.ICollection.IsReadOnly 属性 true 如果
ICollection 是只读的;否则为假。在默认情况下
ReadOnlyCollection 的实现,此属性始终返回
正确。

Exposing ICollection<string> is not a good idea because it allows adding and removing operations.

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

EDIT:

As you mentioned Add() and Remove() methods which comes from explicit ReadOnlyCollection<T>.ICollection<T> implementation will throw NotSupportedException exception so collection is read only.

Also, to ensure that collection under the hood is really readonly you can check IsReadOnly Property.

MSDN says that:

ReadOnlyCollection.ICollection.IsReadOnly Property true if
the ICollection is read-only; otherwise, false. In the default
implementation of ReadOnlyCollection, this property always returns
true.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文