Подтвердить что ты не робот

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

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

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).

Что касается меня, то в отношении шаблона 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();
}

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

Мне интересно, почему команда С# не предоставляет четкого и простого способа определения автоматических свойств коллекции, поэтому разработчики могут порадовать свою лень, создавая надежный код?

4b9b3361

Ответ 1

TL; DR, компилятор С# не имеет автосообщений, потому что существует множество способов раскрытия коллекций. При экспонировании коллекции вы должны тщательно подумать о том, как вы хотите, чтобы коллекция была инкапсулирована, и используйте правильный метод.


Причина, по которой компилятор С# предоставляет автоматические свойства, заключается в том, что они распространены и почти всегда работают одинаково, однако, поскольку вы обнаруживаете, что ситуация редко встречается при работе с коллекциями, существует many различные способы отображения коллекции, правильный метод всегда зависит от ситуации, чтобы назвать несколько:

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

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

Ответ 2

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

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

Ответ 3

Экспозиция 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:

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

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

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

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

Ответ 4

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

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

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

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

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