Start Debugging
RU

C# 14: ключевое слово field и свойства, опирающиеся на field

C# 14 вводит контекстное ключевое слово field в акцессорах свойств, позволяя добавлять собственную логику к авто-свойствам без отдельного объявления резервного поля.

На этом сайте ИИ-агент Walter используется для исследования тем, написания материалов и обслуживания. Подготовка и проверка материалов

C# 14 вводит новое контекстное ключевое слово, field, которое можно использовать внутри акцессоров свойства (блоков get, set или init), чтобы обращаться к резервному хранилищу свойства. Проще говоря, field — это заполнитель, представляющий скрытую переменную, в которой хранится значение свойства. Это ключевое слово позволяет добавлять собственную логику к автоматически реализованным свойствам, не объявляя вручную отдельное приватное поле. Впервые оно появилось как preview в C# 13 (требовалось .NET 9 с версией языка preview), а официально входит в язык в C# 14.

Чем это полезно? До C# 14, если вы хотели добавить логику (например, валидацию или уведомление об изменении) к свойству, его приходилось превращать в полноценное свойство с приватным резервным полем. Это означало больше шаблонного кода и риск, что другие члены класса случайно используют это поле напрямую, обойдя логику свойства. Новое ключевое слово field решает эти проблемы, позволяя компилятору самому создавать и обслуживать резервное поле, а вы просто пишете field в коде свойства. В результате декларации свойств становятся чище и удобнее в сопровождении, а резервное хранилище не “просачивается” в остальную область видимости класса.

Преимущества и сценарии использования field

Ключевое слово field было введено, чтобы декларации свойств стали лаконичнее и менее подвержены ошибкам. Ниже — основные плюсы и сценарии, где оно полезно:

  • Отказ от ручных резервных полей: Больше не нужно писать приватное поле для каждого свойства только ради добавления собственного поведения. Компилятор предоставляет скрытое резервное поле автоматически, а вы обращаетесь к нему через ключевое слово field. Это уменьшает шаблонный код и делает определение класса чище.

  • Сохранение инкапсуляции состояния свойства: Резервное поле, созданное компилятором, доступно только через акцессоры свойства (через field), а не где-либо ещё в классе. Это исключает случайное неправильное использование поля из других методов или свойств и гарантирует, что инварианты или валидации в акцессоре свойства нельзя обойти.

  • Упрощённая логика свойств (валидация, ленивая инициализация и т. д.): Это удобный путь добавлять логику в автосвойства. Типичные сценарии:

    • Валидация или проверка диапазона: например, гарантировать, что значение неотрицательно или находится в диапазоне, прежде чем принять его.
    • Уведомление об изменении: например, генерация событий INotifyPropertyChanged после установки нового значения.
    • Ленивая инициализация или значение по умолчанию: например, в геттере инициализировать field при первом доступе или возвращать значение по умолчанию, если оно не установлено.

    В прежних версиях C# для подобных сценариев требовалось писать полноценное свойство с отдельным полем. С field их можно реализовать прямо в логике get/set, без дополнительных полей.

  • Сочетание авто- и пользовательских акцессоров: C# 14 позволяет один акцессор делать автоматическим, а другому давать тело, использующее field. Например, можно сделать собственный set, оставив get автоматическим, или наоборот. Компилятор сгенерирует то, что нужно, для не описанного вами акцессора. Раньше это было невозможно: добавление тела к одному акцессору требовало явной реализации обоих.

В целом field повышает читаемость и поддерживаемость, убирая избыточный код и оставляя только необходимое поведение. Концептуально это похоже на работу ключевого слова value в сеттере (которое представляет присваиваемое значение); здесь field представляет внутреннее хранилище свойства.

До и после: ручное резервное поле против ключевого слова field

Чтобы увидеть разницу, сравним, как объявлялось свойство, накладывающее некое правило, до C# 14 и после появления нового ключевого слова field.

Сценарий: Допустим, нужно свойство Hours, которому никогда нельзя присвоить отрицательное число. В прежних версиях C# мы поступали так:

До C# 14, с ручным резервным полем:

public class TimePeriodBefore
{
    private double _hours;  // backing field

    public double Hours
    {
        get { return _hours; }
        set 
        {
            if (value < 0)
                throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
            _hours = value;
        }
    }
}

В этом коде до C# 14 нам пришлось завести приватное поле _hours для хранения значения. Геттер свойства возвращает это поле, а сеттер выполняет проверку перед присваиванием в _hours. Это работает, но многословно: есть лишний код для объявления и сопровождения _hours, и _hours доступно везде в классе (то есть другие методы могут записывать в _hours, обходя логику валидации, если не быть внимательным).

Начиная с C# 14, с ключевым словом field:

public class TimePeriod
{
    public double Hours
    {
        get;  // auto-implemented getter (compiler provides it)
        set => field = (value >= 0) 
            ? value 
            : throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
    }
}

Здесь свойство Hours объявлено без явного резервного поля. Мы используем get; без тела, обозначая автоматический геттер, и предоставляем тело для set, в котором применяется field. Выражение field = ... в сеттере говорит компилятору присвоить значение резервному полю свойства. Компилятор автоматически создаст приватное поле “за кулисами” и реализует акцессор get, возвращающий это поле. В сеттере выше, если value отрицательно, мы бросаем исключение; иначе присваиваем его field (которое его хранит). Нам не пришлось объявлять _hours самим, и тело геттера тоже не нужно писать — компилятор делает это за нас. В результате — более лаконичное определение свойства с тем же поведением.

Обратите внимание, насколько чище версия в C# 14:

  • мы убрали явное поле _hours; компилятор разбирается с ним сам.
  • акцессор get остаётся простым автоматически реализованным (get;), который компилятор превратит в “вернуть резервное поле”.
  • акцессор set содержит только важную нам логику (проверка на неотрицательность); фактическое присваивание в хранилище выполняет field = value.

При необходимости field можно использовать и в акцессоре get. Например, для ленивой инициализации можно написать так:

public string Name 
{
    get => field ??= "Unknown";
    set => field = value;
}

В этом случае при первом обращении к Name, если оно не было установлено, геттер присваивает резервному полю значение по умолчанию "Unknown" и возвращает его. Последующие чтения или любая запись будут использовать то же field. Без этой возможности понадобилось бы приватное поле и больше кода в геттере, чтобы добиться такого же поведения.

Как компилятор обрабатывает ключевое слово field

Когда вы используете field внутри акцессора свойства, компилятор тихо генерирует скрытое резервное поле для этого свойства (очень похоже на то, что он делает для автоматически реализуемого свойства). Вы никогда не видите это поле в исходном коде, но компилятор присваивает ему внутреннее имя (например, что-то вроде <Hours>k__BackingField) и использует его для хранения значения свойства. Под капотом происходит следующее:

  • Создание резервного поля: Если хотя бы один акцессор свойства использует field (или у вас есть автоматически реализованное свойство без тел), компилятор создаёт приватное поле для хранения значения. Объявлять это поле самостоятельно не нужно. В примере TimePeriod.Hours выше компилятор сгенерирует поле для значения часов, а оба акцессора get и set будут работать с этим полем (либо неявно, либо через ключевое слово field).
  • Реализация геттера/сеттера:
    • Для автоматически реализованного акцессора (как get; или set; без тела) компилятор автоматически генерирует простую логику возврата или присваивания резервного поля.
    • Для акцессора, в котором вы предоставили тело с field, компилятор встраивает вашу логику и трактует field в сгенерированном коде как ссылку на резервное поле. Например, set => field = value; в скомпилированном виде превращается в нечто вроде set { backingField = value; }, и любая дополнительная логика, которую вы написали, сохраняется вокруг.
    • Можно сочетать автоматические и пользовательские акцессоры. Например, если написать тело для set (с field) и оставить get как get;, компилятор сгенерирует get за вас. И наоборот, можно написать пользовательский get (например, get => ComputeSomething(field)) и оставить автоматически реализованный set;; в этом случае компилятор сгенерирует сеттер, который просто присваивает резервному полю.
  • Поведение эквивалентно ручным полям: Скомпилированный результат при использовании field по сути такой же, как если бы вы вручную написали приватное поле и использовали его в свойстве. Никаких штрафов по производительности или магии — кроме экономии шаблонного кода. Это сугубо удобство во время компиляции. Например, две реализации Hours выше (с field и без) компилируются в очень похожий IL: у обеих есть приватное поле для хранения значения и акцессоры свойства, манипулирующие этим полем. Разница лишь в том, что одну из них компилятор C# 14 написал за вас.
  • Инициализаторы свойств: Если применить инициализатор к свойству, использующему field (например, public int X { get; set => field = value; } = 42;), инициализатор напрямую инициализирует резервное поле до запуска конструктора, как и для традиционных автосвойств. Логика сеттера во время конструирования объекта не вызывается. (Это важно, если в сеттере есть побочные эффекты — для начального значения, заданного через инициализатор, они не выполнятся. Если нужна инициализация через сеттер, лучше присваивать свойство в конструкторе, а не использовать инициализатор.)
  • Атрибуты на резервном поле: Если нужно применить атрибуты к сгенерированному резервному полю, C# позволяет атрибуты, направленные на поле через синтаксис [field: ...]. Это уже было возможно с автосвойствами и работает и здесь. Например, можно написать [field: NonSerialized] public int Id { get; set => field = value; }, чтобы пометить автогенерируемое поле как не сериализуемое. (Это работает только при наличии резервного поля у свойства, то есть когда есть хотя бы один акцессор, использующий field, или это автосвойство.)

TLDR: компилятор создаёт приватное резервное поле и связывает с ним акцессоры свойства. Вы получаете функциональность полноценного свойства при минимуме кода. С точки зрения реализации свойство по-прежнему остаётся настоящим автосвойством — у вас просто появилась “точка входа” для встраивания логики.

Правила синтаксиса и использования field

Используя ключевое слово field, держите в уме следующие правила и ограничения:

  • Только внутри акцессоров свойств/индексаторов: field можно использовать только внутри тела акцессора свойства или индексатора (блока кода или выражения для get, set или init). Это контекстное ключевое слово, то есть вне акцессора свойства field не имеет особого значения (оно будет считаться обычным идентификатором). Если попытаться использовать field в обычном методе или вне свойства, вы получите ошибку компиляции — компилятор не поймёт, на какое резервное поле вы ссылаетесь.
  • Контекстное ключевое слово (не полностью зарезервировано): Поскольку field не является глобально зарезервированным ключевым словом, технически у вас могут быть переменные или члены с именем field в других частях кода. Однако внутри акцессора свойства field рассматривается как ключевое слово и ссылается на резервное поле, а не на какую-либо переменную с именем field. См. ниже раздел о конфликтах имён.
  • Использование в акцессорах get/set/init: field можно использовать в акцессорах get, set или init. В сеттере или init-акцессоре field обычно присваивается (например, field = value;). В геттере вы можете возвращать или модифицировать field (например, return field; или field ??= defaultValue;). Использовать field можно как только в одном акцессоре, так и в обоих — в зависимости от потребностей:
    • Если использовать field только в одном акцессоре, второй можно оставить автоматически реализованным (get; или set; без тела), и компилятор всё равно создаст резервное поле и подключит всё необходимое.
    • Если использовать field в обоих акцессорах, это тоже нормально: вы фактически прописываете и логику get, и логику set (но без ручного объявления поля). Это может пригодиться, когда и чтение, и запись требуют особой обработки. Например, сеттер может налагать условие, а геттер выполнять преобразование или ленивую загрузку при первом доступе, и оба используют одно и то же field.
  • На field нельзя ссылаться вне акцессора: Нельзя сохранить ссылку field и использовать её где-то ещё, как и нельзя напрямую обращаться к сгенерированному компилятором резервному полю вне свойства. По сути это резервное поле анонимно в исходном коде (хотя у компилятора у него есть внутреннее имя). Если нужно работать со значением, делайте это через свойство или внутри его акцессоров с помощью field.
  • Не для событий: Ключевое слово field предназначено для свойств (и индексаторов). Для акцессоров add/remove событий оно недоступно. (События в C# тоже могут иметь резервные поля для делегата, но команда языка решила не распространять field на акцессоры событий.)
  • Не смешивать с явными объявлениями полей: Если вы решили объявить собственное резервное поле для свойства, не стоит использовать field в акцессорах этого свойства. В таком случае вы просто ссылаетесь на ваше явное поле по имени, как и прежде. Ключевое слово field призвано заменить необходимость в явном поле в подобных сценариях. Иначе говоря, у свойства либо неявное поле, управляемое компилятором (когда вы используете field или авто-акцессоры), либо вы управляете им сами — но не одновременно.

Кратко: используйте field внутри акцессоров вашего свойства, чтобы обратиться к скрытому хранилищу этого свойства, и нигде больше. За пределами свойств следуйте обычным правилам области видимости C#.

Обработка конфликтов имён (когда у вас есть собственная переменная field)

Поскольку field не было зарезервированным словом в более ранних версиях C#, возможна (хоть и редкая) ситуация, когда некий код использовал “field” как имя переменной или поля. С появлением контекстного ключевого слова field в акцессорах такой код мог стать неоднозначным или сломаться. Дизайн языка учитывает это:

  • field в акцессоре затеняет идентификаторы: Внутри акцессоров свойства новое ключевое слово field затеняет любой идентификатор с именем field, который мог бы быть в этой области видимости. Например, если у вас была локальная переменная или параметр с именем field внутри сеттера (возможно, из старого кода), компилятор теперь интерпретирует field как ключевое слово резервного поля, а не как вашу переменную. В C# 14 при попытке объявить или использовать переменную с именем field в акцессоре будет ошибка компиляции, поскольку field теперь должно быть ключевым словом.
  • Используйте @field или this.field, чтобы обратиться к настоящему полю: Если у вас действительно есть член класса, буквально названный “field” (не рекомендуется, но возможно), или переменная в области видимости с именем “field”, к ним можно обращаться, экранируя имя. C# позволяет ставить перед идентификатором @, чтобы использовать его, даже если это ключевое слово. Например, если в классе есть private int field; и вам нужно обратиться к нему в акцессоре, можно написать @field, чтобы обращаться к нему как к идентификатору. Аналогично можно использовать this.field для явной ссылки на член класса. Использование @ или квалификатора обходит интерпретацию контекстного ключевого слова и позволяет обратиться к настоящей переменной.
private int field = 10; // a field unfortunately named "field" 
public int Example
{
    get { return @field; } // use @field to return the actual field 
    set { @field = value; } // or this.field = value; either works 
}
  • Тем не менее, если есть возможность, лучше переименовать член, чтобы избежать путаницы. В современном C# field сам по себе в акцессоре должно быть зарезервировано за резервным полем компилятора. Более того, при обновлении старой кодовой базы до C# 14 компилятор предупредит, если найдёт использования field, ранее ссылающиеся на что-то иное, и подскажет устранить неоднозначность.
  • Полностью избегать этого имени: В качестве общей рекомендации старайтесь не использовать field как имя идентификатора в коде. Теперь, когда оно (в контексте) является ключевым словом, обращение с ним как с обычным именем будет смущать читателей и может приводить к ошибкам. Если вы использовали field в качестве имени переменной, при переходе на C# 14 рассмотрите переименование. Стандартные соглашения об именовании (например, префикс _ для приватных полей и т. п.) в большинстве случаев и так предотвращают такой конфликт.

Источники

  1. field – Field backed property declarations​
  2. ​C# Feature Proposal Notes – “field keyword in properties”
  3. ​What’s new in C# 14

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.