Свойство выглядит как поле для кода, который его использует (order.Total, order.Total = 5), но на самом деле это пара методов: метод доступа get, который возвращает значение, и метод доступа set, который его принимает. Такая прослойка позволяет классу проверять значения, вычислять их или запрещать запись, не меняя того, как их используют вызывающие.
Поля и свойства
Открытое поле даёт прямой доступ к хранилищу объекта. Кто угодно может положить туда что угодно:
public class Account
{
public decimal Balance; // any code can write -500 here
}
Свойство ставит код между вызывающим и хранилищем. Вот полная, написанная вручную форма: закрытое поле хранения (backing field) плюс свойство с обоими методами доступа. Внутри set ключевое слово value это присваиваемое значение.
Вывод:
250
Rejected a negative balance
250
Помимо проверки, свойства важны потому, что большая часть экосистемы .NET работает с ними и игнорирует поля: интерфейсы могут объявлять свойства, но не поля экземпляра, привязка данных в WPF и MAUI привязывается к свойствам, а System.Text.Json по умолчанию сериализует открытые свойства и пропускает поля.
Автосвойства
Когда методы доступа только читали бы и писали поле, пусть их напишет компилятор. { get; set; } это автоматически реализуемое свойство: компилятор сам создаёт скрытое поле хранения.
Вывод:
Notebook: 9.99, 10 left
Выглядит как поле, так зачем же? Потому что превращение открытого поля в свойство позже ломает скомпилированный код, который его использует (и аргументы ref и out), а превращение автосвойства в полное свойство с проверкой для вызывающих незаметно. Начав с { get; set; }, вы бесплатно сохраняете эту возможность.
private set и свойства только для чтения
Большая часть состояния должна быть доступна для чтения всем, а меняться только самим объектом. Дайте сеттеру более узкий модификатор доступа:
Вывод:
Ines: 2 items, total 15.75
Здесь три вида «только для чтения»:
{ get; private set; }: класс может менять его в любой момент, внешний код не может.{ get; }(автосвойство только для чтения, C# 6): можно присвоить только в конструкторе или инициализаторе. После создания объекта его никто не может изменить, даже сам класс. Так делают неизменяемое свойство.=> expression(вычисляемое свойство): никакого хранилища вообще. Выражение выполняется при каждом чтении, поэтомуTotalвсегда соответствует списку.
Вычисляемое свойство должно быть дешёвым и без побочных эффектов, потому что свойства читают не задумываясь, в циклах и в отладчике. Если получение значения требует настоящей работы (запроса к базе данных, большого вычисления), сделайте его методом вроде CalculateTotal(), чтобы стоимость была видна.
Методы доступа с телом-выражением
Полные свойства с однострочными методами доступа могут использовать => для каждого метода доступа:
private string title;
public string Title
{
get => title;
set => title = value?.Trim() ?? "";
}
public decimal Total => SumPrices(); это сокращение для свойства только с геттером; get => ... это та же идея внутри свойства, у которого есть и сеттер.
Логика в геттере и сеттере: уведомления и ленивые значения
Поскольку методы доступа это методы, они могут делать больше, чем хранить значение. Два распространённых шаблона: генерация уведомления об изменении из сеттера и вычисление значения при первом чтении с кэшированием.
Вывод:
(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark
Changed это событие, а шаблон сеттера именно так выглядит в реализациях INotifyPropertyChanged в WPF и MAUI. nameof(Theme) сохраняет строку синхронной при переименовании свойства.
Ошибка бесконечной рекурсии
Самая частая ошибка со свойствами это сеттер, который присваивает самому свойству, а не полю хранения:
public string Name
{
get { return Name; } // calls get again, forever
set { Name = value; } // calls set again, forever
}
Каждый метод доступа вызывает сам себя, и программа завершается с StackOverflowException, который нельзя перехватить. Исправление: читать и писать отдельное поле (name со строчной буквы) или использовать автосвойство. C# 14 добавляет ключевое слово field именно для этого случая: внутри метода доступа field обозначает сгенерированное компилятором поле хранения, поэтому set => field = value.Trim(); работает без объявления поля.
Методы доступа init (C# 9)
Свойство только для чтения вынуждает передавать каждое значение через конструктор. C# 9 добавил init, сеттер, который разрешён только при создании объекта: в конструкторе или в инициализаторе объекта.
public class Product
{
public string Sku { get; init; }
public decimal Price { get; init; }
}
var p = new Product { Sku = "MUG-01", Price = 8.50m }; // fine: during creation
p.Price = 4m; // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
// in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor
В результате получается неизменяемый объект с синтаксисом инициализаторов, и именно это используют классы-записи для своих позиционных свойств. Чтобы «изменить» такой объект, создайте новый; у записей это делается в одну строку через with.
В C# 7 ближайший эквивалент это свойство только для чтения плюс параметр конструктора:
public class Product
{
public string Sku { get; }
public decimal Price { get; }
public Product(string sku, decimal price) { Sku = sku; Price = price; }
}
Члены required (C# 11)
Инициализатор объекта не может заставить вызывающего что-либо задать: new Product { } компилируется, даже если Sku никогда не должен быть пустым. C# 11 добавил модификатор required:
public class User
{
public required string Email { get; init; }
public string DisplayName { get; init; } = "";
}
var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" }; // error CS9035: Required member 'User.Email'
// must be set in the object initializer or attribute constructor.
required работает с set и init. Конструктор, который сам задаёт обязательные члены, может сообщить об этом атрибутом [SetsRequiredMembers], чтобы вызывающих через этот конструктор не просили задавать их повторно.
Частые ошибки
- Рекурсия в методе доступа.
set { Name = value; }вызывает сам себя. Используйте поле хранения или автосвойство. - Дорогие или имеющие побочные эффекты геттеры. Читатели ожидают, что чтение свойства быстрое и его можно безопасно повторять. Для работы используйте метод.
- Открытые поля «пока что». Превращение поля в свойство позже ломает двоичную совместимость, а сериализаторы по умолчанию игнорируют поля. Начинайте с автосвойства.
- Открытые сеттеры у всего.
{ get; set; }у баланса или статуса приглашает некорректные состояния. Используйтеprivate set,initили только чтение и меняйте состояние через методы. - Возврат изменяемой коллекции из свойства только для чтения.
public List<string> Tags { get; }не даёт вызывающим заменить список, но не мешает добавлять в него. ВозвращайтеIReadOnlyList<string>, когда коллекция не должна меняться снаружи.
Часто задаваемые вопросы
Что означает { get; set; } в C#?
Это объявление автоматически реализуемого свойства: компилятор создаёт скрытое закрытое поле, метод доступа get, который его возвращает, и метод доступа set, который его присваивает. public string Name { get; set; } для вызывающего кода ведёт себя как открытое поле, но на самом деле это пара методов, поэтому позже можно добавить проверку или сделать его private set, не меняя код, который его использует.
Чем поле отличается от свойства в C#?
Поле это переменная, хранящаяся в объекте. Свойство это пара методов (get и set), которые снаружи выглядят как поле. Свойства позволяют классу проверять значения, вычислять результаты или ограничивать запись, и именно с ними работают привязка данных, сериализаторы и интерфейсы. Открытые данные следует предоставлять через свойства; поля обычно остаются закрытыми.
Что такое private set в C#?
public int Stock { get; private set; } позволяет любому коду читать свойство, но менять его может только код внутри класса. Так класс открывает состояние, которым владеет, например баланс или количество, оставляя все изменения за методами, соблюдающими правила.
Что делает init в C#?
init (C# 9) это сеттер, который работает только во время создания объекта: в конструкторе или инициализаторе объекта. public string Sku { get; init; } разрешает new Product { Sku = "A-1" }, но отвергает последующее product.Sku = "B-2" с ошибкой CS8852. Он даёт неизменяемым объектам синтаксис инициализаторов.
Что такое required-свойство в C#?
Свойство с пометкой required (C# 11) должно задаваться в каждом инициализаторе объекта, создающем тип; его отсутствие это ошибка компиляции (CS9035). Оно хорошо сочетается с init: public required string Email { get; init; } нужно передать при создании, и потом его нельзя изменить.