Классы, конструкторы, свойства, индексаторы
↑ 1.4 ООП в C♯ · → Следующая
Зачем это на собесе
база ООП в C#. Спросят порядок инициализации, чем поле отличается от свойства, что такое init и required, primary constructors, статический конструктор.
Объяснение
Поля и свойства
- Поле — переменная класса. Обычно
private. - Свойство — пара методов
get/setс синтаксисом поля: контроль доступа, валидация, вычисление, совместимость API.
public class Patient
{
private readonly List<Encounter> _encounters = []; // поле, коллекция-выражение C# 12
public Guid Id { get; } = Guid.NewGuid(); // только чтение, инициализатор
public required string LastName { get; init; } // обязательно при создании, дальше не меняется
public string? Phone { get; private set; } // снаружи только чтение
public DateOnly BirthDate { get; init; }
public int Age => DateTime.Today.Year - BirthDate.Year; // вычисляемое свойство (упрощённо)
private string _email = "";
public string Email
{
get => _email;
set => _email = value.Contains('@') ? value.Trim() : throw new ArgumentException("Bad email");
}
public IReadOnlyList<Encounter> Encounters => _encounters; // наружу read-only представление
public void ChangePhone(string phone) => Phone = phone; // изменение через метод с логикой
}
var p = new Patient { LastName = "Иванова", BirthDate = new(1990, 5, 1) }; // object initializer
// p.LastName = "X"; // ошибка: initinit— присваивание только при создании (конструктор или инициализатор объекта).required(C# 11) — компилятор требует задать свойство при создании.- C# 13 и 14: ключевое слово
fieldв свойствах — доступ к авто-сгенерированному полю без явного объявления.
Конструкторы
public class ReportService
{
private readonly IDataCollector _collector;
private readonly ILogger<ReportService> _log;
public ReportService(IDataCollector collector, ILogger<ReportService> log)
{
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
_log = log;
}
}
// Primary constructor (C# 12) — параметры доступны во всём классе
public class ReportService(IDataCollector collector, ILogger<ReportService> log)
{
public Task<byte[]> BuildAsync(Guid id) { log.LogInformation("Build {Id}", id); return collector.CollectAsync(id); }
}У primary constructor параметры — это захваченные переменные, а не readonly-поля: их можно переприсвоить внутри класса. Для гарантии иммутабельности присвойте в private readonly поле.
- Цепочки:
: this(...)— вызвать другой конструктор этого класса,: base(...)— конструктор базового. - Статический конструктор — выполняется один раз перед первым использованием типа, потокобезопасно. Исключение в нём делает тип непригодным навсегда (
TypeInitializationException).
Порядок инициализации (при наследовании)
- Инициализаторы полей производного класса.
- Инициализаторы полей базового класса.
- Конструктор базового класса.
- Конструктор производного класса. (Статические: static-поля и статический конструктор — до первого обращения к типу.)
Warning
Не вызывайте виртуальные методы в конструкторе: переопределение в наследнике выполнится, когда поля наследника ещё не проинициализированы его конструктором.
Индексаторы
public class ConceptMap
{
private readonly Dictionary<string, string> _map = new(StringComparer.OrdinalIgnoreCase);
public string? this[string sourceCode]
{
get => _map.GetValueOrDefault(sourceCode);
set => _map[sourceCode] = value ?? throw new ArgumentNullException(nameof(value));
}
}
var target = conceptMap["J06.9"];Нюансы и подводные камни
- Публичные изменяемые коллекции (
public List<T> Items { get; set; }) ломают инкапсуляцию: снаружи можно очистить список. ОтдавайтеIReadOnlyList<T>и меняйте через методы. - «Анемичная модель» — классы только из get/set без поведения. В CRUD-сервисах нормально, в сложном домене — антипаттерн.
- Тяжёлая логика в геттере свойства неожиданна для вызывающего (ожидается дешёвая операция). Используйте метод.
- Сущностям EF Core нужен конструктор, который EF может вызвать (можно private без параметров) и сеттеры (можно private).
Тестирование
[Fact]
public void Email_setter_validates_format()
{
var p = new Patient { LastName = "A" };
var act = () => p.Email = "bad";
act.Should().Throw<ArgumentException>();
}Вопросы с ответами
Чем поле отличается от свойства?
Поле — переменная для хранения данных. Свойство — методы доступа get и set с синтаксисом поля: можно валидировать, вычислять, ограничивать доступ и менять реализацию без изменения публичного API.
Чем init отличается от private set и readonly?
init разрешает присваивание только при создании объекта, включая инициализатор объекта снаружи. private set — изменение в любом месте внутри класса. readonly-поле — только в объявлении и конструкторе.
Каков порядок инициализации при наследовании?
Инициализаторы полей наследника, затем инициализаторы полей базового класса, конструктор базового класса и конструктор наследника.
Почему нельзя вызывать виртуальные методы в конструкторе?
Вызовется переопределение из наследника, хотя его конструктор ещё не выполнился, и поля наследника не инициализированы. Возможны NullReferenceException и некорректное состояние.
Что такое primary constructor и в чём его нюанс?
Параметры конструктора в объявлении класса, доступные во всём теле. Это захваченные параметры, а не readonly-поля: их можно изменить, и они не видны как свойства.
Связанные темы
- Следующая: Модификаторы доступа, static, sealed, partial
- Records, init, required: Records, init, with, required, primary constructors
- DI и конструкторы: DI и IoC: зачем и как работает контейнер
- DDD: сущности и агрегаты: DDD: тактический — entity, value object, aggregate, domain events
- Классы в JS: Классы