Start Debugging

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 повышает читаемость и поддерживаемость, убирая избыточный код и оставляя только необходимое поведение. Концептуально это похоже на работу ключевого слова 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:

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

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

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

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

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

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

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

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

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

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

Поскольку field не было зарезервированным словом в более ранних версиях C#, возможна (хоть и редкая) ситуация, когда некий код использовал “field” как имя переменной или поля. С появлением контекстного ключевого слова 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 
}

Источники

  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.

< Назад