Как предотвратить изменение возвращаемой коллекции вызывающей стороной метода?

У меня есть методы, возвращающие частные коллекции вызывающей стороне, и я хочу, чтобы вызывающая сторона не изменяла возвращаемые коллекции.

private readonly Foo[] foos;

public IEnumerable<Foo> GetFoos()
{
    return this.foos;
}

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

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

((IList<Foo>)GetFoos())[0] = otherFoo;

Клонирование коллекций имеет очевидный недостаток, заключающийся в том, что есть две коллекции, которые могут развиваться независимо. Пока что рассматривал следующие варианты.

  1. Заворачиваем коллекцию в ReadOnlyCollection<T>.
  2. Возврат одного из итераторов LINQ, определенных классом Enumerable, путем выполнения фиктивной проекции, такой как list.Select(item => item). На самом деле я рассматриваю возможность использования Where(item => true), потому что возвращенный итератор кажется более легким.
  3. Написание кастомной оболочки.

Что мне не нравится в использовании ReadOnlyCollection<T>, так это то, что он реализует IList<T>, а вызов Add() или доступ к индексатору вызовут исключения. Хотя это абсолютно верно в теории, в реальном коде почти не проверяется IList<T>.IsReadOnly или IList<T>.IsFixedSize.

Использование итераторов LINQ - я заключил код в метод расширения MakeReadOnly() - предотвращает этот сценарий, но в нем есть вкус взлома.

Пишете кастомную оболочку? Изобретая колесо?

Любые мысли, соображения или другие решения?


Отмечая этот вопрос, я обнаружил этот вопрос о переполнении стека, которого не заметил » не заметил раньше. Джон Скит предлагает также использовать «LINQ hack», но еще более эффективно, используя Skip(0).


person Daniel Brückner    schedule 02.08.2009    source источник
comment
Если нет проблем с безопасностью (потенциально враждебные вызывающие абоненты), я обычно не беспокоюсь о защите от неразумного преобразования или вызова Add для ReadOnlyCollection. Глупые разработчики тоже могут использовать отражение и получать доступ к внутренним компонентам. Трудно сделать код защищенным от идиотов. Цель состоит в том, чтобы защитить общедоступный интерфейс от невиновного использования.   -  person TrueWill    schedule 22.01.2010


Ответы (4)


К сожалению, невозможно получить именно то, что вы ищете в текущей версии фреймворка. У него просто нет концепции индексируемой неизменяемой / доступной только для чтения коллекции как для конкретного типа, так и для стиля интерфейса.

Как вы отметили, ReadOnlyCollection<T> работает нормально со стороны конкретного типа. Но нет соответствующего интерфейса для реализации, который также статически доступен только для чтения.

Единственный реальный выбор - это ...

  • Определите свой собственный класс коллекции
  • Либо реализуйте только IEnumerable<T>, либо определите необходимый интерфейс только для чтения, который реализует ваша коллекция.
person JaredPar    schedule 02.08.2009
comment
Я принял этот ответ, потому что он ясно показывает, что встроенной поддержки нет. A решил придерживаться Enumerable.Skip (0). - person Daniel Brückner; 18.08.2009
comment
@ DanielBrückner: Какое реальное преимущество имеет это перед обертыванием в ReadOnlyCollection<T> и преобразованием в IEnumerable<T>? Тот факт, что результирующий объект может быть преобразован в IList<T>, значительно повысит производительность некоторых методов Linq, таких как Count и Last. - person supercat; 28.11.2012

Как насчет того, чтобы сделать глубокую копию возвращаемого объекта? [Таким образом, это не повлияет на исходную коллекцию, даже если вызывающая сторона решит внести изменения в возвращенную копию]

person Vyas Bharghava    schedule 02.08.2009
comment
Копия коллекции нежелательна, потому что вызывающая сторона просто получит снимок и не заметит изменений, внесенных в коллекцию. - person Daniel Brückner; 03.08.2009

List и Array имеют метод AsReadOnly:

public IEnumerable<Foo> GetFoos()
{
    return Array.AsReadOnly(this.foos);
    // or if it is a List<T>
    // return this.foos.AsReadOnly();
}
person Thomas Levesque    schedule 02.08.2009
comment
Эти методы возвращают коллекцию, упакованную в ReadOnlyCollection ‹T›, и мне не нравится, что этот класс реализует IList ‹T›, как описано в вопросе. - person Daniel Brückner; 03.08.2009

В таких случаях я создаю методы внутри класса для доступа к отдельным элементам частной коллекции. Вы также можете реализовать индексатор (http://msdn.microsoft.com/en-us/library/6x16t2tx.aspx) в классе, который содержит частную коллекцию.

person RollingStone    schedule 02.08.2009
comment
Реализация метода для получения одного элемента имеет как минимум два недостатка: требуется второй метод или свойство для возврата количества элементов в коллекции, и все преимущества IEnumerable ‹T› - foreach и LINQ - теряются. Для создания индексатора требуется метод или свойство, возвращающее количество элементов, а некоторые классы имеют несколько методов, возвращающих коллекции. Это отключает индексаторы. - person Daniel Brückner; 03.08.2009
comment
Да, конечно, это зависит от ваших задач. Но вы можете добавить поле, которое возвращает количество элементов в частной коллекции. Вы также можете создать внутренний класс, который будет реализовывать логику доступа. - person RollingStone; 03.08.2009