Инкапсулируйте коллекцию в C # - PullRequest
22 голосов
/ 16 сентября 2011

Начиная с версии 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>();
}

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

Что касается меня, то для шаблона 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 ]

20 голосов
/ 16 сентября 2011

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) Предоставить доступную только для чтения копию коллекции

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

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

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

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

4 голосов
/ 16 сентября 2011
private IList<string> _list = new List<string>();

public IEnumerable<string> List
{
  get
  {
    ///return _list;
     return _list.ToList();
  }
}
1 голос
/ 16 сентября 2011

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

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

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

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

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

1 голос
/ 16 сентября 2011

Экспонирование 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 () и Remove () методы, которые происходят из явной реализации ReadOnlyCollection<T>.ICollection<T>, вызовут исключение NotSupportedException, поэтому коллекция будет доступна только для чтения.

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

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

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

...