Объектно-ориентированное программирование в C#
ООП в продакшене
ООП в C# хорошо работает, когда классы отражают реальные роли системы:
- сущности предметной области;
- сервисы с чёткой единственной ответственностью;
- интерфейсы как контракт между слоями.
Если класс совмещает несвязанные обязанности, это сигнал к декомпозиции — см. композицию вместо наследования.
Практические принципы для продакшена
- Предпочитайте композицию наследованию, если нет строгого отношения "is-a".
- Держите инварианты в одном месте (конструктор, фабрика, валидирующие методы).
- Публичные свойства должны отражать осмысленный контракт, а не служить прямым доступом к внутреннему состоянию.
- Используйте
recordдля DTO и immutable-моделей обмена.
Типичные ошибки
- Глубокая иерархия наследования ради переиспользования пары методов.
- Массовые
publicполя вместо инкапсуляции. - "Божественные" классы на сотни строк с разными обязанностями.
Смежные статьи
- Пространства имён в C#
- Обработка значения null и nullable-типы
- Обобщения (generics)
- Разработка на Unity — практика
MonoBehaviourв редакторе - Unity C# — скрипты для новичков — каркас, движение, триггеры, UI с построчным разбором
- Частые паттерны GoF — обзор; реализации на C# — Стратегия, Наблюдатель, Команда, Фабрика
Объектно-ориентированное программирование в C#
Разработчику АрхитекторуЕсли ООП для вас новое или вы учите C# с нуля, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделе — зачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.
Ниже — как это устроено в C#.
Теория и синтаксис C#
| Понятие ООП | Как выражено в C# |
|---|---|
| АДТ, класс | class, struct, record; ссылочные и значимые типы |
| Инкапсуляция и сокрытие | модификаторы доступа, свойства get/set |
| Наследование | : BaseClass (один базовый класс) |
| Полиморфизм подтипов | virtual / override, abstract, интерфейсы |
| Ad hoc-полиморфизм | перегрузка методов |
| Параметрический полиморфизм | обобщения T |
| Сообщения | вызовы методов, делегаты и event |
Определения без привязки к языку — раздел 4-08-oop.
Кратко для новичка:
- Класс — ссылочный тип;
newсоздаёт объект в куче. - Struct — значимый тип; при копировании дублируются поля (подробнее, типы).
- Свойства (
get/set) — контролируемый доступ к полям (инкапсуляция). - Наследование —
: BaseClass;virtual/override— полиморфные методы. - Интерфейс — контракт; класс или struct реализует несколько интерфейсов.
record— неизменяемая запись для DTO;withсоздаёт копию с изменением поля.
--- — MonoBehaviour и компоненты
В Unity скрипт — класс C#, наследующий MonoBehaviour. Движок прикрепляет его к GameObject; в Inspector видны сериализованные поля.
| Идея C# | Как это в Unity |
|---|---|
| Класс | Файл PlayerMovement.cs → public class PlayerMovement : MonoBehaviour |
| Поля объекта | [SerializeField] private float speed — видно в Inspector, снаружи класса недоступно |
| Методы экземпляра | void Update(), void FixedUpdate() — движок вызывает по расписанию |
| Композиция | Один GameObject — несколько компонентов (Rigidbody, Collider, ваш скрипт) |
| Доступ к соседнему компоненту | GetComponent<Rigidbody>(), TryGetComponent<T>(out T c) |
| Класс без сцены | public static class LevelRestart — утилиты, не наследуют MonoBehaviour |
Код ITЗагрузка примера кода…
Правила, которые экономят часы отладки:
- Имя файла
.csдолжно совпадать с именем класса (Unity так связывает asset и тип). - Не вызывайте
GetComponentкаждый кадр вUpdate— кешируйте вAwake. - Обычный C#-класс (
public class Inventory) можно использовать внутри проекта, но он не появится в меню Add Component, пока не обёрнут или не вызван изMonoBehaviour. - Наследование
MonoBehaviour— композиция предпочтительнее глубоких иерархий скриптов (см. разработку на Unity).
Lifecycle (Awake, Start, Update, FixedUpdate, LateUpdate) — таблица в Справочнике по Unity. Те же методы с разбором каждой строки — каркас MonoBehaviour в Lab.
ООП в C#
C# во многом похож на Java, но есть важные отличия (полная таблица — Сравнение C# и Java):
- Свойства (properties) — полноценная языковая конструкция вместо пары геттер/сеттер.
- Структуры (
struct) — значимые типы; в ООП — отдельный раздел. - Автосвойства — меньше шаблонного кода при инкапсуляции.
- Именованные и опциональные параметры — меньше перегрузок конструкторов.
- Оператор
nameof— безопасные имена членов для рефакторинга.
Интерактивная схема — класс и объект (псевдокод, подходит для любого ООП-языка). Полный разбор принципов: ООП в разделе "Код и разработка".
КЛАСС Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ
объект barsik := новый Кот(имя="Барсик", возраст=3)
barsik.мяукнуть()
Разбор:
- Псевдокод отделяет описание типа (
КЛАСС Кот) от создания конкретного экземпляра (barsik). полязадают состояние объекта, аметод мяукнуть()- его поведение.- Конструкция
новый Кот(...)эквивалентна созданию экземпляра через конструктор. - Вызов
barsik.мяукнуть()показывает обращение к методу уже созданного объекта.
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Код ITЗагрузка примера кода…
Разбор:
- Класс демонстрирует инкапсуляцию: внутреннее поле
_nameскрыто, а доступ к имени идёт через свойствоName. - В
setсвойстваNameвстроена валидация (string.IsNullOrEmpty) и выбросArgumentExceptionпри ошибке. public static int Count => _count;и_countпоказывают общее состояние, разделяемое всеми экземплярами.Damage- вычисляемое свойство: значение урона рассчитывается на лету из текущих характеристик.- Конструктор использует опциональный параметр
level = 1и увеличивает счётчик созданных объектов. override ToString()настраивает человекочитаемое строковое представление объекта.
Наследование как в Java - одиночное:
Код ITЗагрузка примера кода…
Разбор:
: Warriorзадаёт наследование:Knightрасширяет базовый классWarrior.: base(name, level)вызывает конструктор родителя и переиспользует его инициализацию.HorseNameдобавляет состояние, специфичное для наследника.override ToString()переопределяет поведение и дополняет вывод базового класса черезbase.ToString().- Это пример "добавить специализацию, не дублируя общую логику".
Структур нет в Java:
Код ITЗагрузка примера кода…
Разбор:
structобъявляет значимый тип: копирование происходит по значению, а не по ссылке.- Конструктор
Point(int x, int y)инициализирует свойства координат. var p2 = p1;создаёт независимую копию точки.- Изменение
p2.Xне затрагиваетp1.X, что наглядно показывает семантику value type.
class и struct в ООП
В объектно-ориентированном программировании центральная идея — класс как шаблон и объект как экземпляр с полями и методами. C# добавляет struct: тот же синтаксис членов (инкапсуляция, методы, свойства), но другая модель хранения — значение, которое копируется целиком, как int или DateTime.
Полная таблица отличий, примеры и типы BCL — Типы данных в C#, раздел struct. Ниже — как это выглядит в терминах ООП.
Class и struct в терминах ООП
| Понятие | class | struct |
|---|---|---|
| Шаблон типа | Да | Да |
Экземпляр через new | Объект в куче | Значение (часто на стеке) |
| Инкапсуляция | Поля, свойства, методы | То же |
| Наследование реализации | : BaseClass | Недоступно (тип неявно sealed) |
| Полиморфизм | virtual / override, интерфейсы | Только через интерфейсы |
| Идентичность | Два new — два разных объекта | Два экземпляра с одинаковыми полями — равные значения (при корректном Equals) |
null | Допустим для ссылочного типа | Только через int? / Nullable<T> |
Класс описывает сущность с жизненным циклом — создали, изменили, передали по ссылке, освободили через GC. Подходит для сервисов, моделей с поведением, иерархий наследования.
Структура описывает объект-значение (value object) — набор полей, который сравнивают по содержимому. Примеры: координата на карте, сумма с валютой, диапазон дат. Отдельная "личность" экземпляра обычно не важна — важны числа внутри.
Память, копирование и наследование
Память
- У
classв переменной лежит ссылка; сам объект — в куче. - У
structданные лежат внутри переменной. Локальная переменная обычно размещается на стеке. - Поле
structвнутриclassхранится в куче вместе с объектом-владельцем.
Копирование
var b = aдляclass— две переменные, один объект.- То же для
struct— две копии всех полей. Изменение копии не затрагивает оригинал.
Наследование
classстроит цепочки (Warrior→Knight) — см. наследование в C#.structне наследует другие типы. Общий контракт задаёт интерфейс —struct Circle : IShape. Это соглашение о членах, без копирования реализации родителя.
Конструктор без параметров
- У
classявныйpublic MyClass()разрешён всегда. - У
structдо C# 10 компилятор сам добавлял конструктор, обнуляющий поля; явный parameterless был запрещён. С C# 10+ свой parameterless допустим, если все поля инициализированы (версии C#).
Три вопроса при выборе типа
| Вопрос | Если ответ "да" / подходит | Тип |
|---|---|---|
| Данных мало (порядка 16 байт)? | Координаты, ключ, флаг | struct |
| Поля не меняются после создания? | Безопасное копирование | readonly struct, record struct |
Нужна иерархия Базовый → Производный? | Полиморфизм подтипов | class |
Неизменяемый struct снижает риск типичной ошибки: метод получает копию, меняет поле внутри себя, а вызывающий код этого не видит. Вместо изменения полей возвращайте новый экземпляр или используйте ref.
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) =>
(Amount, Currency) = (amount, currency);
public Money Add(decimal delta) => new(Amount + delta, Currency);
}
Для денег в коде чаще берут decimal, а не double — см. типы данных.
Типы в стандартной библиотеке
Краткий ориентир (развёрнутая таблица — в статье о типах):
- struct —
int,DateTime,Guid,TimeSpan,Nullable<T>,ValueTuple. - class —
string,List<T>,Exception, делегаты, доменные сервисы,MonoBehaviourв Unity.
Struct в объектном дизайне
Подходящие случаи:
- компактные данные без собственной идентичности (точка, цвет, интервал);
- малые DTO в участках кода, где важна частота аллокаций;
- ключи
Dictionary/HashSetчерезrecord structс автоматическимEquals/GetHashCode.
Лучше выбрать class, если тип:
- внедряет зависимости (DI);
- маппится ORM (EF Core);
- участвует в глубокой иерархии наследования;
- должен часто быть
null.
record (C# 9+) — сокращённая запись для неизменяемых типов. record class — ссылочный DTO; record struct — компактное значение. Детали — справочник C#.
Типичные ошибки
| Что происходит | Почему | Что сделать |
|---|---|---|
Большой struct (много полей) | Каждый вызов копирует все байты | class или передача через in / ref readonly |
Изменяемый struct в методе | Меняется копия параметра | readonly struct, ref, возврат нового значения |
list[i].Field = x для struct | Индексатор отдаёт копию | with, промежуточная переменная |
Иерархия через struct | Язык не поддерживает | class или композиция + интерфейсы |
IShape s = new Circle() для struct | Boxing при присвоении интерфейсу | Generics where T : struct или классы |
Модификаторы доступа:
privateтолько внутри классаprotectedвнутри класса + наследникиinternalвнутри сборки (.dll/.exe) — аналогpackage-privatepublicвезде.
Статические члены здесь совсем другое.
Код ITЗагрузка примера кода…
Разбор:
static classпредназначен для утилитарной логики и не допускает создание экземпляров.const PI- константа уровня типа, доступная без объекта.public static int Square(int x)вызывается через имя класса и не зависит от состояния экземпляра.- Статический конструктор
static MathUtils()выполняется один раз при первом обращении к типу. - Такой шаблон удобен для чистых вспомогательных функций.
Вместо геттеров-сеттеров здесь свойства:
Код ITЗагрузка примера кода…
Разбор:
- В примере объединены разные формы свойств — автоматические, только для чтения, вычисляемые и с ручной логикой.
Name { get; set; }- стандартное auto-property для простого хранения значения.Id { get; }задаёт read-only свойство после инициализации.Greeting => ...- expression-bodied свойство, вычисляемое при обращении.- Свойство
Ageвалидирует входные данные и защищает состояние через приватное поле_age. Tags = new List<string>()сразу убирает рискnullдля коллекции.
Пример класса в C#
Код ITЗагрузка примера кода…
Разбор:
- Класс
Unitмоделирует сущность с характеристиками (интеллект, ловкость, сила, здоровье и т.д.). - Свойство
Damageвычисляет итоговый урон из текущих статов и уровня, а не хранит отдельное поле. - Метод
Attack(Unit target)принимает другой объект того же типа и уменьшает егоHealth. Console.WriteLineс интерполяцией ($"...") формирует понятные сообщения о ходе боя.Mainвыступает точкой входа: создаёт объекты, настраивает их состояние и запускает взаимодействие.- Пример показывает базовую ООП-связку: состояние в полях + поведение в методах.
Ключевое слово class определяет новый класс. Модификатор доступа public делает класс доступным из других сборок. Имя класса начинается с заглавной буквы по соглашению об именовании в C#.
Директива using System; подключает базовое пространство имён платформы .NET. Эта директива размещается в начале файла и предоставляет доступ к классам ввода-вывода, таким как Console.
Поля объявляются внутри тела класса с указанием типа и имени. Модификатор public предоставляет прямой доступ к полям извне класса. Тип string хранит текстовые значения, тип int хранит целые числа.
Свойство Damage определено с использованием блока get. При каждом обращении к свойству выполняется вычисление значения на основе текущих характеристик объекта. Такой подход обеспечивает динамическое обновление урона без необходимости вызова отдельного метода.
Метод Attack принимает параметр типа Unit. Внутри метода используется интерполяция строк через символ $ для формирования сообщений. Метод изменяет состояние целевого объекта через прямое обращение к его полю Health.
Статический метод Main служит точкой входа для консольного приложения. Модификатор static позволяет вызывать метод без создания экземпляра содержащего его класса. Параметр string[] args принимает аргументы командной строки.
Оператор new выделяет память для нового объекта и вызывает конструктор класса. Конструктор по умолчанию инициализирует все поля значениями, указанными при их объявлении. После создания объекта значения полей изменяются через точечную нотацию.
Класс Console предоставляет метод WriteLine для вывода текста в консоль. Интерполяция строк позволяет встраивать значения переменных непосредственно в текст сообщения без дополнительной конкатенации.
Основы классов
Класс
Как мы помним из основ ООП, класс - это шаблон (чертёж), описывающий структуру (какие данные может хранить), поведение (какие действия может выполнять) и способ создания (как инициализируется). Класс не занимает память сам по себе - он всего лишь описание. В C# синтаксис класса очень похож на Java:
public class Car
{
public string Model;
public int Year;
public void StartEngine()
{
Console.WriteLine("Двигатель запущен!");
}
}
Разбор:
class Carзадаёт пользовательский ссылочный тип для предметной сущности "машина".- Поля
ModelиYearхранят состояние конкретного экземпляра. - Метод
StartEngine()описывает действие объекта. - Такой шаблон иллюстрирует принцип "данные + поведение" в одном типе. Класс - это тип ссылочного вида, наследуемый от System.Object.
System.Object — корень всей иерархии типов. Все типы в .NET (включая string, массивы, классы, record, enum) неявно наследуют от System.Object.
И соответственно у System.Object есть важные методы:
ToString()- возвращает строковое представление, например,obj.ToString().Equals(object obj)проверяет равенство (мы его упоминали ранее когда говорили о сравнениях). К примеруobj1.Equals(obj2).GetHashCode()возвращает хеш-код для словарей. К примеру,dict[obj].GetType()возвращает тип объекта, к примеру,obj.GetType().Name.
object obj = "Hello";
Console.WriteLine(obj.GetType()); // System.String
Console.WriteLine(obj.ToString()); // Hello
Разбор:
- Переменная
object objможет ссылаться на любой объект .NET, здесь - на строку. GetType()показывает фактический runtime-тип (System.String).ToString()возвращает строковое представление объекта (для строки - её же содержимое).- Фрагмент демонстрирует универсальные методы базового типа
System.Object.
Переопределяйте ToString(), Equals() и GetHashCode() в своих классах, если нужно осмысленное поведение.
Объект
Объект (или экземпляр) — это конкретный представитель класса, созданный в памяти с помощью оператора new, по принципу Класс имя = new Конструктор() :
Car myCar = new Car();
Разбор:
new Car()создаёт экземпляр класса в памяти и вызывает конструктор.myCarхранит ссылку на этот объект.- После этого через
myCarможно менять состояние и вызывать методы. - Это базовая операция инстанцирования в C#.
Здесь:
- myCar — переменная, ссылающаяся на объект.
- new Car() — вызов конструктора, создающего экземпляр в куче (heap).
myCar.Model = "Tesla";
myCar.Year = 2023;
myCar.StartEngine(); // Двигатель запущен!
Разбор:
- Точечная нотация (
myCar.Model) используется для доступа к членам экземпляра. - Присваивания обновляют внутреннее состояние объекта.
StartEngine()вызывает поведение, описанное в классе.- Фрагмент показывает жизненный цикл: создать объект -> заполнить данные -> вызвать действие.
Класс - тип, а объект - экземпляр данного типа.
Конструктор – это специальный метод, который вызывается при создании экземпляра класса new, он используется для инициализации объекта.
Пример:
class Car {
public string Brand { get; set; }
public string Model { get; set; }
// Конструктор
public Car(string brand, string model) {
Brand = brand;
Model = model;
}
}
// Использование
Car car1 = new Car("BMW", "X5");
Разбор:
- Конструктор
Car(string brand, string model)задаёт обязательные параметры создания объекта. - Внутри конструктора параметры сохраняются в свойствах
BrandиModel. - Вызов
new Car("BMW", "X5")создаёт сразу инициализированный объект. - Подход помогает не оставлять экземпляры в "полупустом" состоянии. Можно иметь несколько конструкторов – это называется перегрузкой конструкторов.
Пример простого класса:
Код ITЗагрузка примера кода…
Атрибуты
Атрибуты – это свойства и поля, которые описывают состояние объекта.
string brand; // поле
int year;
public string Model { get; set; } // свойство
Поле
Поле — это переменная, объявленная внутри класса. Хранит состояние объекта.
public class Person
{
public string Name; // открытое поле
private int _age; // приватное поле (по соглашению с подчёркиванием)
}
Свойство
Свойство — это контролируемый доступ к полю. Позволяет добавлять логику при чтении/записи.
Auto-Property (автоматическое свойство) подразумевает, что C# автоматически создаёт скрытое резервное поле:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Использование:
var person = new Person();
person.Name = "Alice";
Console.WriteLine(person.Name);
Полное свойство (full property) подразумевает добавление валидации, логирования и прочего:
private int _age;
public int Age
{
get { return _age; }
set
{
if (value < 0)
throw new ArgumentException("Возраст не может быть отрицательным.");
_age = value;
}
}
Можно устанавливать свойства только для чтения:
public string FullName => $"{FirstName} {LastName}";
или
public DateTime CreatedAt { get; } = DateTime.Now;
Метод
Метод — это функция, принадлежащая классу. Описывает, что объект умеет делать. Методы в C# используются для выполнения задач и могут принимать параметры и возвращать значения.
public void Drive()
{
Console.WriteLine("Машина едет...");
}
public int CalculateAge(int birthYear)
{
return DateTime.Now.Year - birthYear;
}
Методы могут принимать параметры. Специальный модификатор params позволяет передавать произвольное количество аргументов одного типа:
public void PrintNames(params string[] names)
{
foreach (string name in names)
Console.WriteLine(name);
}
// Вызов:
PrintNames("Alice", "Bob", "Charlie"); // OK
PrintNames(); // OK — пустой массив
И поскольку класс является типом, то указывая тип возвращаемого значения метода, можно указывать соответствующий класс.
this
Ключевое слово this — это ссылка на текущий объект. То есть, текущий экземпляр.
Код ITЗагрузка примера кода…
this помогает избежать неоднозначности и явно указать, что вы работаете с текущим экземпляром.
Цепочка конструкторов: this() используется для того, чтобы избежать дублирования кода:
public Person() : this("Unknown", 0) { }
public Person(string name) : this(name, 0) { }
public Person(string name, int age)
{
Name = name;
Age = age;
}
Деструктор
Деструктор (финализатор) — метод, вызываемый перед удалением объекта сборщиком мусора. Используется редко — только для освобождения неуправляемых ресурсов (например, файловых дескрипторов).
~Person()
{
// Очистка неуправляемых ресурсов
Console.WriteLine("Объект Person уничтожен.");
}
Частичные классы и методы
partial class позволяет разделить определение класса на несколько файлов. Компилятор объединяет их в один тип.
// File1.cs
partial class Calculator
{
public void Add(int a, int b) => Console.WriteLine(a + b);
}
// File2.cs
partial class Calculator
{
public void Multiply(int a, int b) => Console.WriteLine(a * b);
}
Это используется для генерируемого кода, разделения логики, или удобного редактирования больших классов.
partial method, объявленный в одном файле, может быть реализован в другом (или нет).
// В генерируемом коде
partial void OnNameChanged();
// В пользовательском коде
partial void OnNameChanged()
{
Console.WriteLine("Имя изменилось!");
}
Если реализация не предоставлена — компилятор удаляет вызов, не оставляя накладных расходов.
enum
В C# есть перечисления (enum).
enum — это тип, представляющий набор именованных констант.
public enum DayOfWeek
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
По умолчанию — int, начиная с 0.
Соответственно, задавать значения можно явно:
public enum HttpStatus
{
Ok = 200,
NotFound = 404,
ServerError = 500
}
Флаги — комбинация значений через побитовые операции (|, &, HasFlag). Атрибут [Flags] и степени двойки:
[Flags]
public enum FileAccess
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
var rw = FileAccess.Read | FileAccess.Write;
bool canRead = rw.HasFlag(FileAccess.Read);
Без [Flags] ToString() для комбинации может выглядеть менее читаемо; с атрибутом — перечисляет имена флагов.
record
record — легковесные классы для данных, это специальный тип класса, предназначенный для хранения данных и сравнения по значению, а не по ссылке.
public record Person(string Name, int Age);
По умолчанию record неизменяемые, сравниваются по значению и автоматически генерируют ToString(), Equals(), GetHashCode().
Порядок объявления
В C# не важно, в каком порядке вы объявляете поля, свойства, методы, конструкторы и другие члены класса. Вы можете вызывать метод, который объявлен ниже по коду, использовать свойство, которое идёт после метода, где оно используется, обращаться к полю, которое объявлено в конце класса, помещать public члены перед private, или наоборот - как удобно. Это гарантировано стандартом языка и работает во всех стандартных реализациях. Потому что C# — это язык с однофазной семантической проверкой. Компилятор C# не обрабатывает файл "сверху вниз" построчно, как это делают некоторые интерпретируемые языки. Вместо этого он сначала парсит весь исходный код и строит абстрактное семантическое дерево (AST), анализирует весь класс целиком, собирает информацию обо всех его членах, и только потом проверяет, правильно ли они используются (например, вызываются методы, ссылаются поля и т.д.). Это называется отложенной семантической проверкой.
Код ITЗагрузка примера кода…
Хотя порядок не важен для компилятора, важно удобство чтения и поддержки кода. Вот популярные подходы:
Распространённый порядок (от Microsoft и большинства стилей):
- Поля (приватные, затем публичные)
- Константы
- Свойства
- События
- Конструкторы
- Финализаторы / Деструкторы
- Методы
- Вложенные типы
Альтернатива — по видимости - сначала public, затем protected, internal, private
Или по логике (рекомендуется в сложных классах), в таком случае группируйте связанные методы и свойства вместе. Например — блок "инициализация", "валидация", "сохранение" и т.д.
Если вы используете partial классы (например, в WPF или при генерации кода), члены могут быть разделены по разным файлам. И это ещё раз подчёркивает: C# не зависит от порядка — компилятор объединяет все части и анализирует их как единое целое.
Модификаторы доступа
После знакомства с основами классов, объектов, свойств и методов, пришло время понять, как управлять доступом к этим элементам и как делиться состоянием между всеми экземплярами. На помощь приходят модификаторы доступа и статические члены.
Инкапсуляция
Интерактивная схема — инкапсуляция (псевдокод). Подробнее: Инкапсуляция.
Play ITЗагрузка интерактивного демо…
★ Инкапсуляция – принцип, согласно которому данные и методы, работающие с ними, объединяются в одном классе, а прямой доступ к внутреннему состоянию ограничивается. Нужна для защиты данных от некорректного изменения, упрощения взаимодействия с объектом и сокрытия сложной реализации.
Без инкапсуляции код выглядит вот так:
public class BankAccount
{
public decimal Balance; // Прямо доступно!
}
...
var account = new BankAccount();
account.Balance = -1000; // Ошибка! Баланс не может быть отрицательным
Разбор:
public decimal Balanceоткрывает поле для прямой записи из любого места кода.- Внешний код может установить невалидное значение (
-1000), обходя бизнес-правила. - Такой подход нарушает инварианты доменной модели счёта.
- Пример показывает, почему критичные данные не стоит хранить в публичных полях.
А вот так считается безопаснее благодаря инкапсуляции:
Код ITЗагрузка примера кода…
Разбор:
_balanceскрыт какprivate, поэтому изменять его напрямую снаружи нельзя.- Свойство
Balanceдаёт контролируемый доступ:getпубличный,setзакрыт (private set). - В
setвстроена валидация (value < 0) и выбрасываетсяInvalidOperationExceptionпри нарушении правила. - Метод
Deposit(decimal amount)задаёт безопасный путь изменения состояния и проверяет вход черезArgumentException. - Такая конструкция реализует инкапсуляцию: состояние защищено, поведение управляемо.
Поведение контролируется, данные защищены благодаря как раз-таки инкапсуляции - комбинации приватных полей и публичных свойств/методов.
Модификаторы доступа определяют, какие части кода могут обращаться к полям, свойствам, методам, классам и другим членам.
| Модификатор | Доступность |
|---|---|
private | Только внутри того же класса |
public | Повсеместный доступ без ограничений |
protected | Внутри класса и его наследников |
internal | В пределах одной сборки (проекта) |
protected internal | Внутри сборки или в любом наследнике, даже вне сборки |
private protected | Внутри той же сборки и только в её наследниках |
В C# поля и свойства имеют различия друг от друга.
Поля и свойства
Поле — это член класса или объекта, предназначенный для хранения данных, в то время как свойство — это член класса, который предоставляет методы для чтения, записи и вычисления значения соответствующего поля.
По умолчанию члены класса private, а классы internal.
private
private — только внутри класса. Наиболее строгий уровень. Доступен только в пределах класса. Используйте по умолчанию и открывайте доступ только при необходимости:
Код ITЗагрузка примера кода…
public
public — полный доступ. Доступен всем, кто может видеть класс.
public class Calculator
{
public int Add(int a, int b) => a + b; // любой может использовать
}
protected
protected — доступ у наследников. Доступен в текущем классе и всех его наследниках, даже если они в другом проекте. Как раз подойдёт базовым классам, где нужно передавать логику наследникам.
Код ITЗагрузка примера кода…
internal
internal — только внутри сборки. Доступен внутри одного проекта (сборки). Вне проекта — как private. Используется для вспомогательных классов, которые не должны быть видны извне, но нужны внутри проекта.
internal class DatabaseHelper
{
internal string ConnectionString = "server=localhost;";
}
Согласованность
C# следит за согласованностью уровней доступа. Компилятор запретит:
Пример 1: публичный метод возвращает приватный тип
private class SecretData { }
public class Service
{
public SecretData GetData() // ОШИБКА!
{
return new SecretData();
}
}
Пример 2: более открытый доступ у метода, чем у класса
internal class Utility
{
public void DoWork() // ОШИБКА: public метод в internal классе
{
}
}
Правила согласованности: член не может быть доступнее, чем содержащий его тип. Уровень доступа члена не может быть выше, чем у содержащего его типа. Возвращаемый тип и параметры метода должны быть не менее доступны, чем сам метод.
static
А для общего состояния и поведения используется механизм статических членов.
Ключевое слово static означает, что член принадлежит типу (классу), а не отдельному экземпляру. Представьте 100 объектов Car: если поле static, оно одно на всех.
Статические поля
Статические поля хранят общее состояние для всех экземпляров.
Код ITЗагрузка примера кода…
Используется это для счётчиков, кэшей, конфигураций.
Статические методы
Статические методы не требуют создания экземпляра и вызываются через имя типа.
public class MathUtils
{
public static double Square(double x) => x * x;
public static double Max(double a, double b) => a > b ? a : b;
}
double result = MathUtils.Square(5); // 25
Это можно использовать для утилит, не зависящих от состояния объекта.
Статические свойства
Статические свойства:
public class AppSettings
{
public static string ApiUrl { get; set; } = "https://api.example.com";
public static int Timeout { get; set; } = 30;
}
Статический конструктор
Статический конструктор выполняется один раз при первом обращении к классу (или при создании экземпляра). Используется для инициализации статических полей, особенно с логикой:
Код ITЗагрузка примера кода…
Статический класс
Класс, помеченный static, не может иметь экземпляров и может содержать только статические члены. Используется для утилитарных классов, которые не должны создавать объекты.
Код ITЗагрузка примера кода…
Как можно понять, в принципе static использовать можно для утилит, глобальных настроек, счётчиков, статистики, кэшей, констант. static помогает делиться состоянием и поведением между всеми экземплярами — но используйте его с умом.
Статические члены предоставляют механизм глобального доступа к данным и поведению без привязки к экземплярам типа. Такой подход удобен для утилит, конфигураций и счётчиков, однако несёт определённые риски, особенно при работе с изменяемым состоянием.
Риски, связанные с изменяемыми статическими полями
Статические поля инициализируются один раз при первом обращении к типу и сохраняют своё значение на протяжении всего жизненного цикла приложения. Это означает, что любое изменение такого поля влияет на все части программы, использующие его. Последствия:
- Сложность локализации изменений — так как статическое состояние доступно глобально, отследить, какое именно место кода его изменило, может быть затруднительно.
- Проблемы с тестируемостью: статическое состояние не поддаётся изоляции в юнит-тестах, что усложняет написание независимых и воспроизводимых проверок.
- Сложности многопоточности: при отсутствии синхронизации доступ к общему изменяемому статическому полю из нескольких потоков может привести к гонкам данных.
Принципы безопасного использования
-
Избегайте изменяемых статических полей
Статические поля должны быть неизменяемыми (readonly) или константами (const). Это гарантирует, что состояние остаётся предсказуемым на протяжении исполнения программы. -
Статические методы должны быть чистыми функциями
Они не должны изменять внешнее состояние, полагаться на скрытые зависимости или иметь побочные эффекты. Пример — методы расширений в LINQ: они принимают входные данные и возвращают новые, не затрагивая исходные. -
Выносите общую логику в статические утилитарные классы
Если метод не использует состояние экземпляра, его логика может быть вынесена в отдельный статический класс. Это повышает читаемость и облегчает повторное использование. -
Не используйте статические поля для временных или сессионных данных
Статическое поле, ссылающееся на изменяемую коллекцию, например список, подвержено неожиданным изменениям из любого участка кода. В таких случаях предпочтительнее применять внедрение зависимостей (dependency injection) или явное управление временем жизни объекта.
Допустимые сценарии
- Утилитарные классы без состояния, такие как
Math,Path, или пользовательские аналоги (StringUtils,JsonHelper). - Методы расширений, реализующие функциональные преобразования.
- Инициализация глобальной конфигурации, если она неизменяема после загрузки.
- Специализированные случаи многопоточности, например поле с атрибутом
[ThreadStatic], когда каждому потоку требуется собственная копия данных.
Наследование и base
Интерактивная схема — наследование (псевдокод). Подробнее: Наследование.
Play ITЗагрузка интерактивного демо…
Наследование — это механизм, при котором один класс (производный) может наследовать данные и поведение другого класса (базового), расширяя или изменяя их. Это позволяет строить иерархии типов, избегать дублирования кода и реализовывать гибкое поведение через полиморфизм.
Сразу заметим, что C# не поддерживает множественное наследование классов.
При помощи наследования можно создавать дочерний класс от родительского, автоматически получая поля, свойства, методы, и косвенно даже конструкторы. При этом производный класс может расширять функциональность (добавлять новые члены), изменять поведение (переопределять виртуальные методы), специализировать поведение.
Наследование реализует принцип "IS-A":
Dog is a Animal
Button is a Control
То есть, базовый класс (base class) это тот, от которого наследуют, а производный класс (derived class) это тот, который наследует от базового.
Синтаксис через двоеточие:
class DerivedClass : BaseClass
{
// Наследует всё, что доступно из BaseClass
}
Пример:
Код ITЗагрузка примера кода…
base
Ключевое слово base — доступ к базовому классу. base — это ссылка на базовый класс изнутри производного. Позволяет вызывать конструктор базового класса, а также его методы и свойства.
Если в базовом классе есть важная логика (например, валидация), её нужно сохранить. И чтобы не затереть базовую логику переопределением, нужно добавлять base и вызывать базовый метод:
public override void Start()
{
base.Start(); // сначала базовая логика
Console.WriteLine("Дополнительная инициализация...");
}
Производный класс не наследует конструкторы, но может вызвать конструктор базового класса с помощью base():
Код ITЗагрузка примера кода…
Это цепочка конструкторов: сначала вызывается конструктор базового класса, потом — производного.
Если метод переопределён (override), можно вызвать оригинальную реализацию из базового класса:
Код ITЗагрузка примера кода…
Используется, когда нужно дополнить, а не полностью заменить поведение.
Говоря о переопределении, важно знать о virtual, override и sealed.
virtual
virtual — разрешить переопределение. Метод, помеченный virtual, может быть переопределён в производном классе.
public virtual void Start()
{
Console.WriteLine("Двигатель запущен");
}
По умолчанию методы не виртуальные — их нельзя переопределить. Поэтому если планируете переопределение - добавляйте virtual.
override
override — переопределить виртуальный метод. Производный класс заменяет реализацию виртуального метода.
public override void MakeSound()
{
Console.WriteLine("Гав!");
}
Если метод не virtual, abstract или override, компилятор запретит использовать override.
sealed
sealed override — запретить дальнейшее переопределение. Можно заблокировать возможность дальнейшего переопределения в цепочке наследования.
public class Wolf : Animal
{
public sealed override void MakeSound()
{
Console.WriteLine("Аууу!");
}
}
public class CyberWolf : Wolf
{
// public override void MakeSound() // ОШИБКА! Заблокировано
}
Полезно, когда логика критична и не должна изменяться дальше.
Повторное использование кода — одна из главных целей наследования. Вместо дублирования:
class Dog { public string Name; public void Eat() { ... } }
class Cat { public string Name; public void Eat() { ... } } // дубль!
Можно вынести общее в базовый класс:
class Animal
{
public string Name;
public void Eat() { Console.WriteLine("Ест..."); }
}
class Dog : Animal { }
class Cat : Animal { }
Теперь Dog и Cat автоматически имеют Name и Eat().
Порой нужно запретить наследование. sealed — запрет наследования. Если класс помечен как sealed, от него нельзя наследовать.
sealed class StringHelper
{
public static string Reverse(string s) => new(s.Reverse().ToArray());
}
Используется, когда класс завершён и не предназначен для расширения, важна производительность (компилятор может оптимизировать вызовы) или нужно предотвратить подмену поведения (например, в безопасности).
sealed в реальных проектах
Подход "sealed by default" означает: по умолчанию закрывать класс для наследования и открывать его только при явной архитектурной необходимости. Это помогает избежать случайных расширений и лучше защищает инварианты доменной логики.
public sealed class PaymentConfiguration
{
public string ApiKey { get; set; } = string.Empty;
public int TimeoutInSeconds { get; set; } = 30;
}
На горячих участках кода sealed иногда даёт выигрыш производительности: JIT проще де-виртуализировать вызовы и встраивать небольшие методы. Но это не "магический ускоритель" для любого кода, поэтому ориентируются на профиль и измерения.
Когда sealed может мешать
Некоторые инструменты строят наследников в рантайме. В этих сценариях "закрытый" тип создаёт ограничения:
- ORM-прокси (например, для lazy loading в EF) могут требовать наследуемые сущности.
- Библиотеки мокирования часто не умеют мокать
sealed-класс напрямую.
Если тип должен участвовать в таких механизмах, обычно делают одну из стратегий:
- Оставляют конкретный класс открытым для наследования.
- Выносят контракт в интерфейс и мокают интерфейс.
- Используют композицию вместо наследования.
Класс, объявленный как static, автоматически становится abstract (нельзя создавать экземпляры) и sealed (нельзя наследовать).
static class MathHelper
{
public static double Square(double x) => x * x;
}
Такие классы содержат только статические члены и используются как утилиты.
Если базовый класс содержит abstract методы, производный класс обязан их реализовать.
public abstract class Animal
{
public abstract void MakeSound(); // НЕТ реализации
}
public class Dog : Animal
{
public override void MakeSound() // ОБЯЗАТЕЛЬНО
{
Console.WriteLine("Гав!");
}
}
Если не реализовать — ошибка компиляции.
Абстракция
Интерактивная схема — абстракция (псевдокод). Подробнее: Абстракция.
Play ITЗагрузка интерактивного демо…
★ Абстракция – выделение ключевых характеристик объекта и игнорирование деталей реализации. Нужна для упрощения работы с объектами, скрытия сложности и формирования чёткого интерфейса.
Интерфейсы и абстрактные классы используются для определения методов, которые должны быть реализованы в производных классах.
Все интерфейсы начинаются с I — это стандарт .NET: I = Interface.
Пример абстракции:
abstract class Shape {
public abstract double Area();
}
class Circle : Shape {
private double radius;
public Circle(double r) => radius = r;
public override double Area() => Math.PI * radius * radius;
}
Пример интерфейса:
interface IDrawable {
void Draw();
}
class Rectangle : IDrawable {
public void Draw() {
Console.WriteLine("Рисую прямоугольник");
}
}
Интерфейс задаёт контракт: какие члены должен реализовать класс. Класс может реализовать несколько интерфейсов, но наследовать только один класс. Начиная с C# 8 интерфейс может содержать реализацию по умолчанию и статические члены; поля экземпляра по-прежнему запрещены.
Интерфейсы
Интерфейсы (Interfaces) – это контракты, которые определяют, какие методы и свойства должен реализовать класс. Пример:
interface IDrivable {
void Start();
void Stop();
}
Реализация интерфейса:
class Car : IDrivable {
public void Start() {
Console.WriteLine("Запуск двигателя");
}
public void Stop() {
Console.WriteLine("Остановка двигателя");
}
}
Класс может реализовывать несколько интерфейсов, но наследуется от одного класса.
Абстрактный класс и интерфейс
Теория с таблицей и примером умного дома — Абстракция в ООП. В C# это выражается так:
| Критерий | interface | abstract class |
|---|---|---|
| Роль в модели | Способность или контракт (IDrawable, IWiFiConnectable) | Общий предок семейства (Shape, HouseholdAppliance) |
| Наследование | Несколько интерфейсов через : | Один базовый класс |
| Состояние | Поля экземпляра запрещены | Свойства, поля, конструктор |
| Готовый код | С C# 8 — методы и свойства по умолчанию, static члены | virtual, abstract, обычные методы |
| Именование | Префикс I — соглашение .NET | Имя предметной области; каркас часто Abstract… |
abstract class HouseholdAppliance {
protected string SerialNumber { get; }
public HouseholdAppliance(string serial) => SerialNumber = serial;
}
interface IWiFiConnectable {
void ConnectToNetwork();
}
class WashingMachine : HouseholdAppliance, IWiFiConnectable {
public WashingMachine(string serial) : base(serial) { }
public void ConnectToNetwork() => Console.WriteLine("Wi-Fi подключён");
}
class SmartBulb : IWiFiConnectable {
public void ConnectToNetwork() => Console.WriteLine("Лампочка в сети");
}
WashingMachineнаследуетHouseholdAppliance— получает серийный номер и конструктор.WashingMachineреализуетIWiFiConnectable— берёт на себя контракт подключения к сети.SmartBulbреализует только интерфейс: лампочка не бытовая техника, но умеет подключаться к Wi‑Fi.
Когда создавать новый тип в C#:
- класс — новая сущность с полями и методами;
- методы — поведение объекта;
- поля и свойства — состояние;
- интерфейс — общий контракт для разных классов (полиморфизм);
- конструктор — инициализация при
new.
Полиморфизм
Интерактивная схема — полиморфизм (псевдокод). Подробнее: Полиморфизм.
Play ITЗагрузка интерактивного демо…
★ Полиморфизм позволяет объектам разных классов обрабатываться как объекты одного типа, при этом выполняя "свою" версию метода. Нужен для обеспечения единого интерфейса для разных типов, гибкости и расширяемости кода, а также поддержки принципа замены Барбары Лисков (классы-наследники не должны противоречить базовому классу).
Пример:
Animal a = new Animal();
Animal d = new Dog();
a.MakeSound(); // Звук...
d.MakeSound(); // Гав!
Полиморфизм в C# бывает следующих типов:
- статический (раннее связывание) – перегрузка методов;
- динамический (позднее связывание) – переопределение методов (virtual / override).
override
★ Переопределение метода (override) это механизм, при котором производный класс изменяет реализацию метода, унаследованного от базового класса. То есть, дочерний класс может изменить поведение, унаследованное от родителя. В базовом классе метод помечается как virtual, а в производном переопределяется при помощи override:
class Animal {
public virtual void MakeSound() {
Console.WriteLine("Звук...");
}
}
class Dog : Animal {
public override void MakeSound() {
Console.WriteLine("Гав!");
}
}
overloading
★ Перегрузка метода (overloading) – это возможность создания нескольких методов с одним и тем же именем, но с разными параметрами. Методы считаются перегруженными, если они отличаются количеством параметров, типом параметров, порядком параметров.
Пример:
class Calculator {
public int Add(int a, int b) {
return a + b;
}
public double Add(double a, double b) {
return a + b;
}
public int Add(int a, int b, int c) {
return a + b + c;
}
}
Использование:
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(2, 3)); // 5 (int)
Console.WriteLine(calc.Add(2.5, 3.5)); // 6.0 (double)
Console.WriteLine(calc.Add(1, 2, 3)); // 6 (три параметра)
Пример полиморфизма:
Код ITЗагрузка примера кода…
Конструкторы должны быть перегружены, чтобы определить несколько конструкторов для любого заданного класса. Для этого должны быть определены параметризованные конструкторы, которые могут принимать внешние параметры. Статические конструкторы не могут быть вызваны напрямую, а только через CLR, которая не может передать параметр параметризованному конструктору.
В C# реализованы различные виды полиморфизма:
- Ad-hoc полифорфизм (или специфический полиморфизм) — это возможность использования одного и того же имени функции или оператора для выполнения разных действий в зависимости от контекста вызова. Пример - перегрузка методов (несколько методов с одинаковым именем, но разными параметрами) и неявное приведение типов (можно передать int в метод, который ожидает double - так происходит неявное преобразование).
- Параметрический полиморфизм (обобщённое программирование) достигается через обобщённые типы и методы (generics). Он позволяет создавать классы, интерфейсы и методы, которые могут работать с любыми типами данных без потери безопасности типов. Об этом мы поговорим в разделе коллекций.
- Полиморфизм включения (подтиповое наследование) основан на механизме наследования и позволяет использовать объект подкласса там, где ожидается объект базового класса или интерфейса. Можно вызывать переопределённые методы динамически во время выполнения (динамический полиморфизм). К полиморфизму включения относятся интерфейсы, абстрактные классы, виртуальные методы (virtual / override) и методы с явным переопределением (new).
Паттерны проектирования
Интерфейсы и полиморфизм в C# — база для паттернов GoF: Strategy и Observer почти всегда строятся на interface; Factory — на статических фабриках или DI; Command — на делегатах и ICommand в UI.
| Материал | Ссылка |
|---|---|
| Шпаргалка по десяти частым паттернам | Частые паттерны GoF в реальных проектах |
| Стратегия, Наблюдатель, Команда, Фабрика на C# | Паттерн "Стратегия" в C# — когда нужен, а когда достаточно делегата, Паттерн "Наблюдатель" в C# — события, IObservable и утечки, Паттерн "Команда" в C# — объекты действий, а не методы, Паттерн "Фабрика" в C# — когда хватает DI-контейнера |
| ООП без привязки к языку | 4-08-oop |
Справочник по ООП в C#
Консолидированный блок для быстрой навигации по терминам, созданию объектов, модификаторам доступа и сравнению с другими языками. Дополняет развёрнутые разделы выше. Общая теория — ООП в разделе "Код"; параллель — ООП в Java, ООП в C++. Unity-компоненты — раздел выше.
Глоссарий — термин C# и понятие ООП
| Термин C# | Понятие ООП | Кратко |
|---|---|---|
class | Класс (АДТ) | Ссылочный тип; шаблон объектов |
struct | Значимый тип / объект-значение | Копирование по значению; struct и class, типы |
record | Объект-значение (value object) | Неизменяемые DTO, сравнение по содержимому |
interface | Контракт / абстракция поведения | Множественная реализация, без состояния экземпляра |
abstract class | Абстрактный базовый класс | Общий код + обязательные abstract-методы |
property (get/set) | Инкапсулированный атрибут | Контролируемый доступ к состоянию |
field | Поле / состояние | Переменная внутри типа |
method | Метод / поведение | Функция, принадлежащая типу |
constructor | Конструктор | Инициализация при new |
static | Состояние/поведение типа | Общее для всех экземпляров или утилиты |
virtual / override | Полиморфизм подтипов | Позднее связывание переопределений |
this | Ссылка на текущий экземпляр | Разрешение имён, цепочка конструкторов |
base | Ссылка на базовый класс | Вызов родительского конструктора/метода |
event / delegate | Сообщения, наблюдатель | Подписка на уведомления |
enum | Именованные константы | Ограниченный набор значений |
partial class | Разделение определения типа | Несколько файлов — один класс |
Создание экземпляра и запись в переменные
В C# переменная ссылочного типа (class, record class) хранит адрес объекта в управляемой куче (heap). Значимые типы (struct, примитивы) копируются по значению — см. struct и class.
// Ссылочный тип: переменная → объект в куче
Car myCar = new Car("Tesla", 2023);
Car alias = myCar; // две переменные, один объект
alias.Year = 2024; // myCar.Year тоже 2024
// Значимый тип: независимые копии
Point p1 = new Point(1, 2);
Point p2 = p1; // копия по значению
p2.X = 10; // p1.X остаётся 1
// Вывод типа и target-typed new (C# 9+)
var warrior = new Warrior("Артур");
Warrior gawain = new("Гавейн");
// Nullable-ссылки и значимые типы
Warrior? maybe = null; // допустимо при включённом nullable context
int? level = null; // Nullable<int>
Порядок при new Car(...):
- Выделяется память под объект (GC heap для
class). - Поля инициализируются значениями по умолчанию (
0,null,false). - Выполняется цепочка конструкторов (
this()/base()). - Возвращается ссылка, которая записывается в переменную.
Полиморфные ссылки: переменная базового типа может указывать на наследника — вызов virtual-метода разрешается по фактическому типу объекта:
Animal pet = new Dog();
pet.MakeSound(); // Гав! — вызывается реализация Dog, не Animal
Запись в коллекции и поля: объекты передаются по ссылке; в List<Car> хранятся ссылки, а не копии. Для value types в коллекции — копии значений.
record и with: неизменяемые записи создают новый экземпляр при обновлении полей:
Person p1 = new("Алиса", 30);
Person p2 = p1 with { Age = 31 }; // новый объект, p1 не меняется
Доступность методов и полей (сводная таблица)
По умолчанию: члены класса — private, сам класс — internal.
| Модификатор | Класс | Поле / свойство / метод | Наследник (другая сборка) | Другая сборка |
|---|---|---|---|---|
private | — | только этот класс | нет | нет |
private protected | — | класс + наследники в той же сборке | да (та же сборка) | нет |
protected | — | класс + все наследники | да | нет |
internal | вся сборка | вся сборка | да (та же сборка) | нет |
protected internal | — | сборка или любой наследник | да | частично |
public | везде | везде (если тип доступен) | да | да |
Дополнительные правила:
| Ситуация | Поведение |
|---|---|
public метод возвращает private тип | Ошибка компиляции |
public метод в internal классе | Ошибка: член не может быть доступнее типа |
static метод | Вызывается через имя типа; не видит нестатические поля без экземпляра |
Свойство private set | Чтение снаружи, запись только внутри класса |
init (C# 9+) | Запись только при инициализации объекта |
Типичные ошибки в C#-ООП
| Ошибка | Почему плохо | Что делать |
|---|---|---|
| Публичные поля вместо свойств | Нет валидации, ломается инкапсуляция | private поле + свойство с логикой |
Забыли virtual у базового метода | override в наследнике не сработает как полиморфизм | Помечать намеренно переопределяемые методы virtual |
new вместо override без понимания | Скрытие метода, не полиморфизм | override + virtual/abstract в базе |
Мутабельный static | Глобальное состояние, гонки в многопоточности | readonly, const или DI |
Глубокая иерархия MonoBehaviour | Хрупкие зависимости в Unity | Композиция компонентов |
Сравнение классов без Equals/GetHashCode | Сбои в Dictionary, HashSet | Переопределить оба или использовать record |
struct с изменяемыми полями в коллекции | Неочевидные мутации копий | readonly struct, иммутабельность |
Игнорирование nullable reference types | NullReferenceException в рантайме | Включить NRT, проверять null |
Мини-сравнение с другими языками
| Аспект | C# | Java | Python | C++ |
|---|---|---|---|---|
| Корень иерархии | System.Object (неявно) | java.lang.Object | object | нет единого корня |
| Единственное наследование классов | да | да | нет (MRO) | нет |
| Интерфейсы | interface, множественно | interface | abc.ABC (опционально) | чисто виртуальный класс |
| Свойства | встроенный синтаксис | геттеры/сеттеры | @property | нет (методы) |
| Значимые типы | struct, record struct | только примитивы + обёртки | нет (всё объекты) | стек, struct |
| Память | GC | GC | GC (refcount + cycle) | ручная / RAII |
| Полиморфизм | virtual/override | неявно virtual | duck typing | virtual + vtable |
| Модификатор по умолчанию (класс) | internal | package-private | public | private (в class) |
Учебные примеры ООП
Небольшие самодостаточные программы, которые показывают классы, объекты, инкапсуляцию, наследование и взаимодействие нескольких типов на одной предметной области.
Класс и объект
Чертёж класса Figure и конкретные объекты — круг и квадрат.
Код ITЗагрузка примера кода…
Банковский счёт
Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.
Код ITЗагрузка примера кода…
Наследование
Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().
Код ITЗагрузка примера кода…
Смартфон
Состояние объекта: заряд батареи, звонки и подзарядка.
Код ITЗагрузка примера кода…
Студент
Список оценок, средний балл и проходной порог.
Код ITЗагрузка примера кода…
Корзина покупок
Взаимодействие Product, Cart и Order при оформлении заказа.
Код ITЗагрузка примера кода…
Автомобиль
Пробег, расход топлива и напоминание о техобслуживании.
Код ITЗагрузка примера кода…
Пользователь
Скрытый пароль, вход в систему и публикация сообщений.
Код ITЗагрузка примера кода…