Программа курса← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Свойства и статические члены

Публичные поля из прошлого урока удобны ровно до первой проверки. Свойства решают эту проблему, не меняя того, как класс выглядит снаружи.

Проблема публичного поля

public class Person
{
    public int Age;
}

var person = new Person();
person.Age = -5;      // никто не помешал

Отрицательный возраст класс принял молча. Проверку добавить некуда: присваивание полю – операция языка, в неё не вклиниться.

Классическое решение – закрыть поле и завести пару методов:

private int _age;

public int GetAge() => _age;

public void SetAge(int value)
{
    if (value < 0)
    {
        throw new ArgumentException("Возраст не может быть отрицательным");
    }

    _age = value;
}

Работает, но читается тяжело: person.SetAge(person.GetAge() + 1) вместо person.Age++.

Свойство

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

public class Person
{
    private int _age;

    public int Age
    {
        get => _age;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException("Возраст не может быть отрицательным");
            }

            _age = value;
        }
    }
}

Теперь снаружи всё выглядит просто:

person.Age = 19;                  // вызывается set, проверка срабатывает
Console.WriteLine(person.Age);    // вызывается get

Блок get возвращает значение, блок set его принимает. Внутри set доступно ключевое слово value – это то, что присваивают; объявлять его не нужно.

Поле _age, которое хранит данные, называют backing fieldПо-русски говорят «поле поддержки» или просто «поле за свойством», но в разговоре чаще звучит английский термин. – хранилищем за свойством.

Автосвойства

Если проверять нечего, писать хранилище руками незачем:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Компилятор сам создаёт скрытое поле. Это самая частая форма записи в реальном коде.

Начинайте с автосвойства, а не с публичного поля. Когда проверка понадобится, вы просто раскроете get и set в полную форму – и ни одна строка вызывающего кода не изменится. Превращение поля в свойство таким свойством не обладает: для внешнего кода это разные вещи на уровне сборки.

Ограничение доступа

Доступ к чтению и записи настраивается по отдельности:

public class Person
{
    public string Name { get; private set; }    // менять может только сам класс
    public int Id { get; }                      // только чтение, задаётся в конструкторе
    public string Email { get; init; }          // задаётся при создании, потом нельзя
}

init – это разрешение на присваивание только в момент создания объекта:

var person = new Person { Email = "a@b.ru" };    // можно
person.Email = "c@d.ru";                         // ошибка компиляции

Вычисляемые свойства

Свойство не обязано хранить значение – оно может его считать:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area => Width * Height;
}

Area не занимает памяти и всегда актуально: оно вычисляется при каждом обращении.

Свойство должно быть дешёвым. Читатель кода видит user.Orders и ждёт мгновенного ответа, а не запроса к базе данных. Если получение значения требует заметной работы, объявляйте метод – GetOrders() честно выглядит как действие.

Статические члены

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

public class Counter
{
    public static int Total;    // одно на всю программу

    public int Value;           // своё у каждого объекта

    public Counter()
    {
        Total++;
    }
}

Обращаются к нему через имя класса, а не через объект:

var a = new Counter();
var b = new Counter();

Console.WriteLine(Counter.Total);    // 2

Сколько бы объектов ни создали, Total существует в одном экземпляре.

Статические методы

Метод, которому не нужны данные конкретного объекта, объявляют статическим:

public static class MathHelper
{
    public static int Square(int n) => n * n;
}

Именно так устроен знакомый класс Math: Math.Sqrt(16) вызывается без создания объекта, потому что квадратному корню не нужно состояние.

Класс целиком можно пометить static – тогда в нём разрешены только статические члены, и создать его через new не выйдет. Это способ собрать вспомогательные функции в одном месте.

Статический метод не может обратиться к обычному полю: он не знает, о каком объекте речь. Ошибка «требуется ссылка на объект для нестатического поля» означает ровно это – вы вызвали из static-метода что-то, принадлежащее экземпляру.

Когда статика уместна

Хорошие поводы: вспомогательные функции без состояния (Math, string.Join), константы, счётчики.

Плохой повод – сделать данные доступными отовсюду, чтобы не передавать их в параметрах. Статическое состояние – это глобальная переменная: её меняет кто угодно откуда угодно, а в многопоточной программе она вдобавок становится источником трудноуловимых ошибок.