Свойства
В прошлом уроке поля закрыли, а работать с ними предложили через методы. Для этажей дома это была бы пара методов: GetFloors() и SetFloors(3). Так писать длинно, зато SetFloors может проверить число. Открытое поле короче, a.Floors = 3, но проверить ничего не может: a.Floors = -1 пройдёт молча.
Представьте консьержа в подъезде. Гость не ходит по квартирам сам: он отдаёт посылку консьержу, и тот сначала её осматривает. Гостю кажется, будто он оставил посылку на столе, а на деле её приняли по правилам.
В C# такой консьерж — свойство: член класса, который читают и записывают как поле, но при каждом чтении и каждой записи выполняется код класса.
Свойство с get и set
Сделаем этажи свойством, которое не допускает меньше одного этажа:
var a = new House();
a.Floors = 3;
Console.WriteLine(a.Floors); // 3
a.Floors = -1;
Console.WriteLine(a.Floors); // 1
class House
{
private int _floors = 1;
public int Floors
{
get => _floors;
set => _floors = value < 1 ? 1 : value;
}
}Давайте разберём класс по строкам:
private int _floors = 1;— закрытое поле, в котором хранится число этажей.public int Floors { … }— свойство: тип, имя и блок сgetиset. Круглых скобок после имени нет.get => _floors;выполняется при каждом чтенииa.Floorsи отдаёт значение поля.set => …выполняется при каждой записи. Словоvalueвнутриset— значение, которое стоит справа от=.a.Floors = -1запускаетsetсvalue, равным-1. Условиеvalue < 1истинно, и в поле попадает1.
Снаружи a.Floors выглядит как поле, но обойти проверку нельзя: другого пути к _floors у внешнего кода нет. В больших программах на неверное значение обычно отвечают ошибкой, а не исправляют его молча. Но место для проверки то же — set.
Здесь консьерж и свойство расходятся. Консьерж может оставить посылку у себя, а свойство само ничего не хранит. Число лежит в поле _floors, а у свойства есть только код get и set.
Автоматическое свойство
Часто проверка не нужна, а поле всё равно хочется закрыть. Тогда пишут автоматическое свойство: public string Owner { get; set; } = "";. Компилятор сам создаёт скрытое поле, а get и set читают и пишут его. Значение после = — начальное, как у поля.
Зачем такое свойство, если есть открытое поле? Проверку потом можно добавить в set, и код, который пишет a.Owner = "Anna", менять не придётся. А ещё у свойства можно ограничить запись.
{ get; set; }
Читать и записывать свойство может любой код, как открытое поле.
{ get; private set; }
Читать может любой код, а записывать — только код самого класса:
var a = new House();
a.Paint("red");
Console.WriteLine(a.Color); // red
class House
{
public string Color { get; private set; } = "white";
public void Paint(string color) => Color = color;
}Метод Paint — код класса, поэтому он меняет Color. А строка a.Color = "red"; снаружи не скомпилируется: ошибка CS0272, запись закрыта.
{ get; init; }
Записать значение можно только при создании объекта, а потом свойство доступно лишь для чтения. Для этого после new перечисляют значения в фигурных скобках:
var a = new House { Street = "Oak", Number = 7 };
Console.WriteLine(a.Street + " " + a.Number); // Oak 7
class House
{
public string Street { get; init; } = "";
public int Number { get; init; }
}Конструкцию { Street = "Oak", Number = 7 } называют инициализатором объекта. Сначала выполняется конструктор, потом свойствам присваиваются значения из скобок. Пустые круглые скобки после new House можно не писать.
Инициализатор задаёт и свойства с обычным set. Особенность init в том, что после создания объекта запись закрыта: строка a.Number = 8; уже не скомпилируется, ошибка CS8852.
{ get; }
Свойство только с get записать можно лишь при объявлении, через =, или в конструкторе. Это значение, которое объект получает при постройке и больше никогда не меняет.
Свойство или поле
На собеседовании часто спрашивают, чем свойство отличается от поля. Ответ из двух частей:
- снаружи свойство выглядит как поле, но чтение и запись запускают его код
getиset; - поэтому свойство может проверить значение или закрыть запись, а поле — нет.
Частая ошибка
В set написали имя свойства вместо имени поля:
var a = new House();
a.Floors = 3;
class House
{
private int _floors = 1;
public int Floors
{
get => _floors;
set => Floors = value; // ✗
}
}Stack overflow.Программа компилируется без предупреждений, но при запуске падает. Присваивание Floors = value внутри set снова вызывает set того же свойства, тот снова вызывает себя, и так до переполнения стека вызовов. Это та же рекурсия без базового случая. Свойство должно писать в поле: set => _floors = value;.
Главное
- Свойство читают и записывают как поле, а при этом выполняется его код:
getпри чтении,setпри записи; вsetновое значение называетсяvalue. - Автоматическое свойство
{ get; set; }хранит значение в скрытом поле, которое создаёт компилятор. private setразрешает запись только коду класса,init— только при создании объекта, а свойство с однимgetзадают при объявлении или в конструкторе.- Инициализатор объекта
new House { Street = "Oak" }присваивает значения свойствам сразу после конструктора.