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