Работает ли использование открытых полей только для чтения для неизменяемых структур?

Это правильный способ объявить неизменные структуры?

public struct Pair
{
    public readonly int x;
    public readonly int y;

    // Constructor and stuff
}

Я не могу думать о том, почему это может столкнуться с проблемами, но я просто хотел спросить, чтобы убедиться.

В этом примере я использовал целые. Что если бы я использовал класс вместо этого, но этот класс также неизменен, как это? Это тоже должно работать нормально, верно?

public struct Pair
{
    public readonly (immutableClass) x;
    public readonly (immutableClass) y;

    // Constructor and stuff
}

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

4 ответа

Решение

Если вы собираетесь использовать структуры, рекомендуется сделать их неизменяемыми.

Создание всех полей только для чтения - отличный способ помочь (1) документировать, что структура неизменна, и (2) предотвратить случайные мутации.

Тем не менее, есть одна морщина, которая на самом деле по странному совпадению я планировал вести блог о следующей неделе. То есть чтение только для структурного поля - ложь. Можно ожидать, что поле только для чтения не может измениться, но, конечно, может. "только для чтения" в поле структуры - это чек для написания декларации без денег на его счете. Структура не владеет своим хранилищем, и оно может изменяться.

Например, давайте возьмем вашу структуру:

public struct Pair
{
    public readonly int x;
    public readonly int y;
    public Pair(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public void M(ref Pair p)
    {
        int oldX = x;
        int oldY = y;
        // Something happens here
        Debug.Assert(x == oldX);
        Debug.Assert(y == oldY);
    }
}

Есть ли что-нибудь, что может произойти в "что-то здесь происходит", что вызывает нарушение отладочных утверждений? Конечно.

    public void M(ref Pair p)
    {
        int oldX = this.x;
        int oldY = this.y;
        p = new Pair(0, 0);
        Debug.Assert(this.x == oldX);
        Debug.Assert(this.y == oldY);
    }
...
    Pair myPair = new Pair(10, 20);
    myPair.M(ref myPair);

И что теперь происходит? Утверждение нарушено! "this" и "p" относятся к одному и тому же месту хранения. Место хранения видоизменено, и поэтому содержимое "этого" видоизменено, потому что это одно и то же. Структура не может обеспечить доступность только для чтения x и y, потому что структура не владеет хранилищем; хранилище - это локальная переменная, которая может изменять столько раз, сколько захочет.

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

Смотрите также отличную статью в блоге Джо Даффи по этому вопросу:

http://joeduffyblog.com/2010/07/01/when-is-a-readonly-field-not-readonly/

Начиная с C# 7.2, теперь вы можете объявить всю структуру неизменной:

public readonly struct Pair
{
    public int x;
    public int y;

    // Constructor and stuff
}

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

Как отмечается в ответе Эрика Липперта, это не мешает полностью переназначить саму структуру и, таким образом, обеспечивает эффект изменения полей из-под вас. Либо передавая по значению, либо используя новый in модификатор параметра может использоваться, чтобы помочь предотвратить это:

public void DoSomething(in Pair p) {
    p.x = 0; // illegal
    p = new Pair(0, 0); // also illegal
}

Это сделало бы его действительно неизменным. Полагаю, вам лучше добавить конструктор.
Если все его члены тоже неизменны, это сделает его полностью неизменным. Это могут быть классы или простые значения.

Компилятор запретит присваивание readonly поля, а также свойства только для чтения.

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

Другие вопросы по тегам