Рекомендации по возврату объекта только для чтения

11

У меня есть вопрос "передового опыта" об ООП в C # (но он вроде относится ко всем языкам).

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

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Очевидно, что лучшим подходом является «вариант C», но очень немногие объекты имеют вариант ReadOnly (и, конечно, ни один из пользовательских классов не имеет его).

Если бы вы были пользователем класса А, ожидали бы вы изменения

List<string> someList = ( new A() ).Items;

распространять на оригинальный (A) объект? Или можно вернуть клон при условии, что это было написано в комментариях / документации? Я думаю, что такой подход клонирования может привести к довольно трудным для отслеживания ошибкам.

Я помню, что в C ++ мы могли возвращать const-объекты, и вы могли вызывать только методы, помеченные как const. Я думаю, что нет такой функции / шаблон в C #? Если нет, то почему бы им не включить это? Я считаю, что это называется const правильность.

Но опять же, мой главный вопрос о том, «чего бы вы ожидали» или как справиться с вариантом А против Б.

Никогда не переставай учиться
источник
2
Посмотрите в этой статье об этом предварительном просмотре новых неизменяемых коллекций для .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Вы уже можете получить их через NuGet.
Кристоф Клас

Ответы:

10

Помимо ответа @ Jesse, который является лучшим решением для коллекций: общая идея состоит в том, чтобы спроектировать ваш открытый интерфейс A таким образом, чтобы он возвращал только

  • типы значений (например, int, double, struct)

  • неизменяемые объекты (например, «строка» или пользовательские классы, которые предлагают только методы только для чтения, как и ваш класс А)

  • (только если это необходимо): копии объектов, как показано в «Варианте B»

IEnumerable<immutable_type_here> само по себе является неизменным, так что это всего лишь частный случай вышеизложенного.

Док Браун
источник
19

Также обратите внимание, что вы должны программировать для интерфейсов, и в целом то, что вы хотите вернуть из этого свойства, является IEnumerable<string>. A List<string>- это что-то вроде «нет-нет», потому что оно, как правило, изменчиво, и я зашёл так далеко, чтобы сказать, что, ReadOnlyCollection<string>поскольку разработчик ICollection<string>чувствует, что он нарушает принцип подстановки Лискова.

Джесси С. Слайсер
источник
6
+1, использование IEnumerable<string>именно то, что здесь нужно.
Док Браун
2
В .Net 4.5 вы также можете использовать IReadOnlyList<T>.
svick
3
Примечание для других заинтересованных сторон. При рассмотрении метода доступа к свойству: если вы просто возвращаете IEnumerable <string> вместо List <string>, выполняя return _list (+ неявное приведение к IEnumerable), тогда этот список можно преобразовать обратно в List <string> и изменить, и изменения будут размножаться в основной объект. Вы можете вернуть новый List <string> (_list) (+ последующее неявное приведение к IEnumerable), который защитит внутренний список. Подробнее об этом можно узнать здесь: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
Лучше ли требовать, чтобы любой получатель коллекции, которому требуется доступ к ней по индексу, был готов копировать данные в тип, который облегчает это, или лучше требовать, чтобы код, возвращающий коллекцию, предоставлял его в форме, которая доступен по индексу? Если поставщик, скорее всего, не получит его в форме, которая не облегчает доступ по индексу, я бы посчитал, что ценность освобождения получателей от необходимости делать свои собственные избыточные копии превысила бы стоимость освобождения поставщика от необходимости поставлять форма произвольного доступа (например, только для чтения IList<T>)
суперкат
2
В IEnumerable мне не нравится то, что вы никогда не знаете, работает ли он как сопрограмма или это просто объект данных. Если он работает как сопрограмма, это может привести к незначительным ошибкам, так что иногда это полезно знать. Вот почему я склоняюсь к тому, чтобы предпочесть ReadOnlyCollection или IReadOnlyList и т. Д.
Стив
3

Некоторое время назад я столкнулся с подобной проблемой, и мое решение было ниже, но оно действительно работает, только если вы тоже управляете библиотекой:


Начните с вашего класса A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Затем выведите из этого интерфейс только с частью get свойств (и любыми методами чтения) и сделайте так, чтобы A реализовал

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Тогда, конечно, когда вы хотите использовать версию только для чтения, достаточно простого приведения. Это как раз то, чем вы на самом деле являетесь, но я подумал, что предложу метод, с помощью которого я достиг этого

Энди Хант
источник
2
Проблема в том, что его можно привести обратно к изменяемой версии. И его нарушение LSP, состояние база, наблюдаемое через методы базового класса (в данном случае интерфейса), может быть изменено новыми методами из подкласса. Но это, по крайней мере, связывает намерение, даже если оно не осуществляет его.
user470365
1

Для тех, кто находит этот вопрос и все еще интересуется ответом - теперь есть еще один вариант, который выставляет ImmutableArray<T>. Это несколько моментов , почему я думаю , что это лучше , IEnumerable<T>или ReadOnlyCollection<T>:

  • это ясно заявляет, что это является неизменным (выразительный API)
  • в нем четко указано, что коллекция уже сформирована (иными словами, она не инициализируется лениво, и ее можно многократно повторять)
  • если количество элементов известно, не скрывает , что knowlegde как IEnumerable<T>делает
  • поскольку клиент не знает, что является реализацией, IEnumerableили IReadOnlyCollectон / она не может предположить, что она никогда не изменится (другими словами, нет гарантии, что элементы коллекции не были изменены, а ссылка на эту коллекцию остается неизменной)

Также, если APi необходимо уведомить клиента об изменениях в коллекции, я бы выставил событие как отдельный член.

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

Павел Ахмадулин
источник