Перейти к содержимому
Теория · Классы и объекты · урок 6 из 11

Свойства

В прошлом уроке поля закрыли, а работать с ними предложили через методы. Для этажей дома это была бы пара методов: GetFloors() и SetFloors(3). Так писать длинно, зато SetFloors может проверить число. Открытое поле короче, a.Floors = 3, но проверить ничего не может: a.Floors = -1 пройдёт молча.

Представьте консьержа в подъезде. Гость не ходит по квартирам сам: он отдаёт посылку консьержу, и тот сначала её осматривает. Гостю кажется, будто он оставил посылку на столе, а на деле её приняли по правилам.

В C# такой консьерж — свойство: член класса, который читают и записывают как поле, но при каждом чтении и каждой записи выполняется код класса.

Свойство с get и set

Сделаем этажи свойством, которое не допускает меньше одного этажа:

C#
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;
    }
}

Давайте разберём класс по строкам:

  1. private int _floors = 1; — закрытое поле, в котором хранится число этажей.
  2. public int Floors { … } — свойство: тип, имя и блок с get и set. Круглых скобок после имени нет.
  3. get => _floors; выполняется при каждом чтении a.Floors и отдаёт значение поля.
  4. set => … выполняется при каждой записи. Слово value внутри set — значение, которое стоит справа от =.
  5. 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; }

Читать может любой код, а записывать — только код самого класса:

C#
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 перечисляют значения в фигурных скобках:

C#
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 написали имя свойства вместо имени поля:

C#
var a = new House();
a.Floors = 3;

class House
{
    private int _floors = 1;

    public int Floors
    {
        get => _floors;
        set => Floors = value; // ✗
    }
}
TEXT
Stack overflow.

Программа компилируется без предупреждений, но при запуске падает. Присваивание Floors = value внутри set снова вызывает set того же свойства, тот снова вызывает себя, и так до переполнения стека вызовов. Это та же рекурсия без базового случая. Свойство должно писать в поле: set => _floors = value;.

Главное

  • Свойство читают и записывают как поле, а при этом выполняется его код: get при чтении, set при записи; в set новое значение называется value.
  • Автоматическое свойство { get; set; } хранит значение в скрытом поле, которое создаёт компилятор.
  • private set разрешает запись только коду класса, init — только при создании объекта, а свойство с одним get задают при объявлении или в конструкторе.
  • Инициализатор объекта new House { Street = "Oak" } присваивает значения свойствам сразу после конструктора.