В C/C++ для этих целей существует ключевое слово const. Многие скажут, что такой механизм слишком ненадёжен, однако, в C# нет и такого. И возможно он появится в будущих версиях (разработчики этого не отрицают), но как же быть сейчас?
1. Неизменяемые объекты (Immutable objects)
Самый известный подобный объект в C# — это строка (string). В нём нет ни одного метода, приводящего к изменению самого объекта, а только к созданию нового. И всё с ними вроде бы хорошо и красиво (они просты в использовании и надёжны), пока мы не вспомним о производительности. К примеру, найти подстроку можно и без копирования всего массива символов, однако, что если нам нужно, скажем, заменить символы в строке? А что если нам нужно обработать массив из тысяч таких строк? В каждом случае будет производиться создание нового объекта строки и копирование всего массива. Старые строки нам уже не нужны, но сами строки ничего не знают об этом и продолжают копировать данные. Только разработчик, вызывая метод, может давать или не давать право на изменение объектов-аргументов, но как это сделать?
2. Интерфейс
Один из вариантов – создать для объекта read only интерфейс, из которого исключить все методы, изменяющие объект. А если этот объект является generic’ом, то к интерфейсу можно добавить ещё и ковариантность. На примере с вектором это будет выглядеть так:
interface IVectorConst<out T> { T this[int nIndex] { get; } } class Vector<T> : IVectorConst<T> { private readonly T[] _vector; public Vector(int nSize) { _vector = new T[nSize]; } public T this[int nIndex] { get { return _vector[nIndex]; } set { _vector[nIndex] = value; } } } void ReadVector(IVectorConst<int> vector) { ... }
(Кстати, между Vector и IVectorConst (или IVectorReader – кому как нравится) можно добавить ещё и контравариантный IVectorWriter.)
И всё бы ничего, но ReadVector’у ничто не мешает сделать downcast к Vector и изменить его. Однако, если вспомнить const из C++, данный способ ничем не менее надёжен, как столь же ненадёжный const, никак не запрещающий любые преобразования указателей. Если вам этого достаточно, можно остановиться, если нет – идём дальше.
3. Отделение константного объекта
Запретить вышеупомянутый downcast мы можем только одним способом: сделать так, чтобы Vector не наследовал от IVectorConst, то есть отделить его. На том же примере с вектором, это будет выглядеть следующим образом:
struct VectorConst<T> { private readonly T[] _vector; public VectorConst(T[] vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } struct Vector<T> { private readonly T[] _vector; private readonly VectorConst<T> _reader; public Vector(int nSize) { _reader = new VectorConst<T>(_vector = new T[nSize]); } public T this[int nIndex] { set { _vector[nIndex] = value; } } public VectorConst<T> Reader { get { return _reader; } } }
Теперь наш VectorConst отделён и, отдавая его кому-то, мы можем спать спокойно, будучи уверенными, что наш вектор останется в неизменном виде. Всё, чем нам пришлось за это заплатить, — это инициализация структуры VectorConst копированием ссылки на _vector и дополнительная ссылка в памяти. При передаче VectorConst в метод происходит вызов свойства и такое же копирование. Таким образом, можно сказать, что по производительности это практически равносильно передаче в метод экземпляра T[], но с защитой от изменений. А чтобы не вызывать явно лишний раз свойство Reader, можно добавить в Vector оператор преобразования:
public static implicit operator VectorConst<T>(Vector<T> vector) { return vector._reader; }
4. Вариативность
И опять есть одно «но»: наши структуры не вариативны. Для этого отнаследуем их от IVectorConst и IVector. Но здесь возникает один нюанс… Чтобы не пришлось впоследствии править код, было бы неплохо лишить разработчика возможности указывать в аргументах методов VectorConst, а только IVectorConst. Для этого можно скрыть структуру VectorConst внутри Vector, объявив её как private. Но при этом мы теряем в производительности: доступ к структуре через интерфейс отнимает гораздо больше времени. И даже сделав её классом, мы немного выиграем, устранив распаковку, но доступ через интерфейс всё равно будет дольше прямого обращения к классу. К тому же, C# запрещает создание операторов преобразования к интерфейсам, поэтому мы лишились ещё и «красивого» вызова методов с VectorConst в качестве аргумента. Посему, воздержимся от этого (хотя такой подход имеет право на жизнь). Итак, вот что у нас в итоге получилось:
interface IVectorConst<out T> { T this[int nIndex] { get; } } interface IVector<in T> { T this[int nIndex] { set; } } struct VectorConst<T> : IVectorConst<T> { private readonly T[] _vector; public VectorConst(T[] vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } struct Vector<T> : IVector<T> { private readonly T[] _vector; private readonly VectorConst<T> _reader; public Vector(int nSize) { _reader = new VectorConst<T>(_vector = new T[nSize]); } public T this[int nIndex] { set { _vector[nIndex] = value; } } public VectorConst<T> Reader { get { return _reader; } } public static implicit operator VectorConst<T>(Vector<T> vector) { return vector._reader; } }
Стоит однако учесть, что если нам понадобится использовать ковариантность IVectorConst, нам всё равно придётся вызывать свойство Reader, несмотря на наличие оператора преобразования:
class A { } class B : A { } private static void ReadVector(IVectorConst<A> vector) { ... } var vector = new Vector<B>(); ReadVector(vector.Reader);
Многие наверняка скажут, что всё это прописные истины. Но возможно для кого-то эта статья и эти несложные шаблоны окажутся полезными. Если у кого есть ещё какие-то идеи, касаемо этой темы, буду рад комментариям.
ссылка на оригинал статьи http://habrahabr.ru/post/199520/