C# 14: ключевое слово field и свойства, опирающиеся на field
C# 14 вводит контекстное ключевое слово field в акцессорах свойств, позволяя добавлять собственную логику к авто-свойствам без отдельного объявления резервного поля.
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 рассмотрите переименование. Стандартные соглашения об именовании (например, префикс_для приватных полей и т. п.) в большинстве случаев и так предотвращают такой конфликт.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.