Инкапсулировать коллекцию в C#

так как 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# не предоставляют некоторые понятный и простой способ определить авто-свойства коллекции, чтобы разработчики могли порадовать свою лень еще созданием надежного кода?

4 ответов


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

Its может быть хорошей идеей, чтобы убедиться, что коллекция никогда не равна null, иначе вы можете просто использовать auto-properties. Если у меня есть хорошая причина для того, чтобы хотеть больше инкапсуляции моей коллекции, я всегда использую этот метод для простоты.

2) коллекция, которая может быть изменена, но не заменена

вы можете закодировать это любым способом, но идея та же - Открытая коллекция позволяет изменять элементы, но сама базовая коллекция не может быть заменена другой коллекцией. Например:

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

я склонен использовать этот простой шаблон при работе с такими вещами, как наблюдаемой коллекции когда потребитель должен иметь возможность изменять коллекцию, но я подписался на уведомления об изменениях - если вы позволите потребителям поменять всю коллекцию, то вы просто вызовете головные боли.

3) выставить копию коллекции только для чтения

часто вы хотите, чтобы предотвратить потребителей от изменения открытой коллекции-обычно, однако вы do хотите, чтобы класс exposing мог изменять коллекцию. - простой способ сделать это-предоставить копию вашей коллекции только для чтения:

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 свойства так не пойдет. Вы также должны учитывать, что копирование многих больших коллекций таким образом потенциально неэффективно.

при создании коллекций, которые читаются только всегда имейте в виду, что элементы в элементе управления can все еще быть изменен. Опять же, это может быть хорошо, но если вы хотите, чтобы Открытая коллекция была "полностью" немодифицируемой, убедитесь, что элементы в коллекции также доступны только для чтения / неизменяемы (например, System.String).

4) коллекции, которые могут быть изменены, но только в определенном смысле

Предположим, вы хотите выставить коллекцию, в которую можно добавлять элементы, но не удалять? Вы можете предоставить свойства для самого класса exposing:

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?");

его a намного больше усилий, но 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>();

Открытая коллекция теперь encapsualtes наши правила о том, как коллекция может и не может быть изменена, а также сразу отражает изменения, внесенные в базовую коллекцию (что может быть или не может быть хорошо). Его также потенциально более эффективно для больших коллекций.


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

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

всякий раз, когда мне приходится выставлять коллекцию, я, как правило, использую IEnumerable<T> но вы не можете использовать Auto-properties для этого, очевидно.

решение, которое я бы очень хотел видеть реализованным в .NET, - это концепция неизменяемых коллекций. Это действительно решит проблему, о которой вы говорите, поскольку auto-properties будет отлично работать таким образом:

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

любой, кто каким-либо образом изменяет список, получит новый экземпляр ImmutableList и не исходный список себя.

вы можете, конечно, реализовать свой собственный ImmutableList но мне кажется странным, что .NET Framework не включает такой элемент.


разоблачение 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);
        }
    }

EDIT:

как вы упомянули Add () и удалить() методы, которые исходят из явного ReadOnlyCollection<T>.ICollection<T> реализация бросит NotSupportedException исключение, поэтому коллекция доступна только для чтения.

кроме того, чтобы убедиться, что коллекция под капотом действительно readonly вы можете проверить IsReadOnly Собственность.

MSDN говорит, что:

ReadOnlyCollection.Интерфейс ICollection.IsReadOnly Свойство true, если ICollection доступен только для чтения; в противном случае-false. По умолчанию реализация ReadOnlyCollection, это свойство всегда возвращает истинный.