Часто, передавая объект в какой-либо метод, нам бы хотелось сказать ему: «Вот, держи этот объект, но ты не имеешь право изменять его», и как-то отметить это при вызове. Плюсы очевидны: помимо того, что код становится надёжнее, он становится ещё и более читаемым. Нам не нужно заходить в реализацию каждого метода, чтобы отследить, как и где изменяется интересующий нас объект. Более того, если константность передаваемых аргументов указана в сигнатуре метода, то по самой такой сигнатуре, с той или иной точностью, уже можно предположить, что же он собственно делает. Ещё один плюс – потокобезопасность, т.к. мы знаем, что объект является read only.
В 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);
Многие наверняка скажут, что всё это прописные истины. Но возможно для кого-то эта статья и эти несложные шаблоны окажутся полезными. Если у кого есть ещё какие-то идеи, касаемо этой темы, буду рад комментариям.
Автор: Ithilgwau