Как жить без const?

в 10:36, , рубрики: .net, const, метки: , ,

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js