Используйте случай, чтобы понять, почему список строк должен быть объявлен как readonly

Я пытаюсь понять, какие варианты использования потребуют от меня объявления List <string> как типа ReadOnly.

Связанный с этим вопрос: сколько памяти при создании экземпляра списка распределяется?

Ответ 1

Основная причина пометить поле как readonly так, чтобы вы знали, что обычный код не может заменить ссылку на список. Один из ключевых сценариев, когда это имеет значение, - это если у вас есть другой код в типе, который выполняет синхронизацию с списком с помощью lock(theListField). Очевидно, если кто-то поменяет экземпляр списка: все сломается. Обратите внимание, что в большинстве типов, у которых есть список/коллекция, не ожидается изменения экземпляра, поэтому этот readonly утверждает это ожидание. Общий шаблон:

private List<Foo> _items = new List<Foo>();
public List<Foo> Items => _items;

или

public List<Foo> Items {get;} = new List<Foo>();

В первом примере это должно быть прекрасно, чтобы отметить это поле как readonly:

private readonly List<Foo> _items = new List<Foo>();

Маркировка поля как readonly не влияет на распределения и т.д. Он также не делает список доступным только для чтения: просто поле. Вы все еще можете Add()/Remove()/Clear() и т.д. Единственное, что вы не можете сделать, это изменить экземпляр списка как совершенно другой экземпляр списка; вы, конечно, можете полностью изменить содержимое. И только чтение - ложь: отражение и небезопасный код могут изменять значение поля readonly.

Существует один сценарий, в котором readonly может иметь отрицательное воздействие и относится к большим полям struct и вызовам на них. Если поле readonly, компилятор копирует структуру в стек перед вызовом метода, а не выполняет метод на месте в поле; ldfld + stloc + ldloca (если поле readonly) vs ldflda (если оно не отмечено readonly); это связано с тем, что компилятор не может доверять методу не изменять значение. Он даже не может проверить, все ли в структуре readonly, потому что этого недостаточно: метод struct может переписать this:

struct EvilStruct
{
    readonly int _id;
    public EvilStruct(int id) { _id = id; }
    public void EvilMethod() { this = new EvilStruct(_id + 1); }
}

Поскольку компилятор пытается обеспечить природу поля readonly, если у вас есть:

readonly EvilStruct _foo;
//...
_foo.EvilMethod();

он хочет убедиться, что EvilMethod() не может перезаписать _foo новым значением. Отсюда гимнастика и копия в стеке. Обычно это оказывает незначительное влияние, но если структура атипично велика, это может вызвать проблемы с производительностью. Та же проблема обеспечения того, что значение не изменяется, также относится к новому модификатору аргумента in в С# 7.2:

void(in EvilStruct value) {...}

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

Эта проблема разрешена в С# 7.2 добавлением синтаксиса readonly struct - это говорит компилятору, что безопасно вызывать метод in-situ без необходимости делать дополнительную копию стека:

readonly struct EvilStruct
{
    readonly int _id;
    public EvilStruct(int id) { _id = id; }
    // the following method no longer compiles:
    // CS1604   Cannot assign to 'this' because it is read-only
    public void EvilMethod() { this = new EvilStruct(_id + 1); }
}

Весь этот сценарий не относится к List<T>, потому что это ссылочный тип, а не тип значения.

Ответ 2

С помощью readonly вы можете установить значение поля либо в объявление или в конструкторе объекта, что поле является член.

В соответствии с List это означает, что только ссылка на объект List будет неизменной (не внутренние строки). U может использовать readonly для List только для того, чтобы убедиться, что ссылка на поле не будет переопределена.

вот пример Почему Microsoft рекомендует использовать поля readonly с изменяемыми значениями?

Ответ 3

Из всех классов, с которыми вы можете работать в .NET, Strings имеют значительно странное поведение. Строка предназначена во многих случаях работать как тип значения, а не ссылочный тип. И это до того, как мы добавим в состав строки такие строки, как string-interning:

Сравнение значений для равенства в .NET: идентификация и эквивалентность

Все сказанное, я не знаю, почему кто-то отметит список <string> readonly, больше, чем список [int] или список [object].

Сколько выделено памяти: это непредсказуемо. Список будет увеличиваться по мере добавления элементов к нему. И он будет зависеть, чтобы избежать чрезмерного перераспределения. Но точный алгоритм для этого - детальная информация о структуре/классе. Если вы знаете, сколько предметов вам нужно заблаговременно, либо укажите счетчик в конструкторе, либо просто создайте список из вашей статической исходной коллекции (например, массива). Это может быть минимальным увеличением производительности и, как правило, является хорошей практикой. В то же время, интернирование строк будет пытаться ограничить, сколько памяти выделено фактическим строковым экземплярам в памяти, путем повторного использования ссылок.