🔷 C#

Objektorientierte Programmierung in C#

ООП в C#: классы, интерфейсы и наследование с примерами

Объектно-ориентированное программирование (ООП) — это способ мышления о коде, при котором программа представляется в виде набора взаимодействующих объектов. Представь LEGO: каждый кубик — объект, у него есть форма (данные) и способность соединяться с другими кубиками (методы). Из кубиков строятся сложные конструкции.

Что такое класс

Класс — это чертёж, шаблон для создания объектов. Как чертёж дома: по одному чертежу можно построить много похожих домов, каждый со своим адресом и жителями.

// Определение класса
class Dog
{
    // Поля (fields) — данные
    public string Name;
    public string Breed;
    public int Age;
    
    // Методы — действия
    public void Bark()
    {
        Console.WriteLine($"{Name} говорит: Гав!");
    }
    
    public void Introduce()
    {
        Console.WriteLine($"Я {Name}, {Breed}, {Age} лет");
    }
}

// Создание объекта (экземпляра класса)
Dog dog1 = new Dog();
dog1.Name = "Шарик";
dog1.Breed = "Дворняга";
dog1.Age = 3;
dog1.Bark();       // Шарик говорит: Гав!
dog1.Introduce();  // Я Шарик, Дворняга, 3 лет

Dog dog2 = new Dog();
dog2.Name = "Рекс";
dog2.Breed = "Немецкая овчарка";
dog2.Age = 5;
dog2.Bark(); // Рекс говорит: Гав!

Свойства: умные поля

В настоящем C#-коде редко используют публичные поля напрямую. Вместо них используют свойства — это поля с контролем доступа.

class Person
{
    // Автоматическое свойство (auto-property)
    public string Name { get; set; }  // get — читать, set — записать
    public string Email { get; set; }
    
    // Свойство только для чтения
    public string FullInfo { get; } // set отсутствует — нельзя изменить снаружи
    
    // Свойство с валидацией
    private int _age;
    public int Age
    {
        get => _age; // Возвращаем _age
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentException("Некорректный возраст");
            _age = value; // value — это то, что присваивают
        }
    }
    
    // Вычисляемое свойство (только get)
    public bool IsAdult => _age >= 18;
}

var person = new Person();
person.Name = "Иван";
person.Age = 25;
Console.WriteLine(person.IsAdult); // True

// person.Age = -5; // ArgumentException!

Конструкторы: инициализация объекта

class Car
{
    public string Brand { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public double Price { get; private set; } // private set — снаружи не изменить
    
    // Конструктор — метод с именем класса, без возвращаемого типа
    public Car(string brand, string model, int year, double price)
    {
        Brand = brand;
        Model = model;
        Year = year;
        Price = price;
    }
    
    // Перегруженный конструктор
    public Car(string brand, string model) : this(brand, model, 2024, 0)
    {
        // : this(...) вызывает другой конструктор
    }
    
    public override string ToString()
    {
        return $"{Year} {Brand} {Model}";
    }
}

// Создание объектов
var car1 = new Car("Toyota", "Camry", 2022, 2500000);
var car2 = new Car("Lada", "Vesta");

Console.WriteLine(car1);         // 2022 Toyota Camry
Console.WriteLine(car2);         // 2024 Lada Vesta

// Инициализатор объекта (object initializer) — альтернатива
var car3 = new Car("BMW", "X5", 2023, 5000000)
{
    // Нельзя задать через инициализатор то, что уже задаёт конструктор,
    // но можно задать дополнительные свойства с public set
};

Record: неизменяемые объекты данных

В C# 9 появился тип record — идеален для объектов-данных, которые не должны изменяться.

// Record — компактная запись данных
record Point(double X, double Y);
record Person(string Name, int Age, string City = "Москва");

var p1 = new Point(3.0, 4.0);
var person = new Person("Анна", 25);

Console.WriteLine(p1);       // Point { X = 3, Y = 4 }
Console.WriteLine(person);   // Person { Name = Анна, Age = 25, City = Москва }

// Records поддерживают сравнение по значению!
var p2 = new Point(3.0, 4.0);
Console.WriteLine(p1 == p2); // True! (для class было бы False)

// Создание изменённой копии с 'with'
var p3 = p1 with { X = 10.0 };
Console.WriteLine(p3); // Point { X = 10, Y = 4 }

// Class с такими же полями — сравнение по ссылке!
class PointClass { public double X; public double Y; }
var c1 = new PointClass { X = 3, Y = 4 };
var c2 = new PointClass { X = 3, Y = 4 };
Console.WriteLine(c1 == c2); // False! (разные объекты в памяти)

Struct: тип-значение

// struct — хранится в стеке (быстро, но без наследования)
struct Size
{
    public int Width { get; }
    public int Height { get; }
    
    public Size(int width, int height)
    {
        Width = width;
        Height = height;
    }
    
    public int Area => Width * Height;
}

var s1 = new Size(100, 50);
Console.WriteLine($"Размер: {s1.Width}x{s1.Height}, площадь: {s1.Area}");

// struct копируется по значению:
var s2 = s1; // Создаётся копия!

// Когда использовать struct:
// - Маленькие данные (2-4 поля)
// - Не должны быть null
// - Сравнение по значению важно

Наследование: дочерние классы

Наследование — как генеалогическое дерево. Дочерний класс наследует всё от родительского и может добавлять своё.

// Базовый (родительский) класс
class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }
    
    public virtual void MakeSound() // virtual — можно переопределить
    {
        Console.WriteLine($"{Name} издаёт звук");
    }
    
    public void Sleep()
    {
        Console.WriteLine($"{Name} спит");
    }
}

// Дочерний класс
class Dog : Animal  // : Animal — наследуем от Animal
{
    public string Breed { get; set; }
    
    public Dog(string name, int age, string breed) 
        : base(name, age)  // base() — вызов конструктора родителя
    {
        Breed = breed;
    }
    
    public override void MakeSound() // override — переопределяем
    {
        Console.WriteLine($"{Name} ({Breed}): Гав-гав!");
    }
    
    public void Fetch()
    {
        Console.WriteLine($"{Name} приносит мячик");
    }
}

class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) {}
    
    public override void MakeSound()
    {
        Console.WriteLine($"{Name}: Мяу-мяу!");
    }
}

var dog = new Dog("Бобик", 3, "Хаски");
var cat = new Cat("Мурка", 2);

dog.MakeSound(); // Бобик (Хаски): Гав-гав!
dog.Sleep();     // Бобик спит (унаследовано от Animal)
dog.Fetch();     // Бобик приносит мячик (собственный метод)

cat.MakeSound(); // Мурка: Мяу-мяу!

Полиморфизм: один интерфейс — разное поведение

// Полиморфизм — разные классы, одна переменная
Animal[] animals = { 
    new Dog("Рекс", 4, "Овчарка"), 
    new Cat("Барсик", 1),
    new Dog("Лайка", 2, "Хаски")
};

// Один цикл — разное поведение для каждого животного
foreach (Animal animal in animals)
{
    animal.MakeSound(); // Вызывается нужная реализация!
}
// Рекс (Овчарка): Гав-гав!
// Барсик: Мяу-мяу!
// Лайка (Хаски): Гав-гав!

Абстрактные классы

// abstract — нельзя создать экземпляр, только наследовать
abstract class Shape
{
    public string Color { get; set; }
    
    // abstract метод — ОБЯЗАН быть переопределён в дочернем классе
    public abstract double Area();
    public abstract double Perimeter();
    
    // Обычный метод — можно использовать как есть
    public void Describe()
    {
        Console.WriteLine($"{GetType().Name}: площадь={Area():F2}, периметр={Perimeter():F2}");
    }
}

class Circle : Shape
{
    public double Radius { get; }
    public Circle(double radius) { Radius = radius; }
    
    public override double Area() => Math.PI * Radius * Radius;
    public override double Perimeter() => 2 * Math.PI * Radius;
}

class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }
    public Rectangle(double w, double h) { Width = w; Height = h; }
    
    public override double Area() => Width * Height;
    public override double Perimeter() => 2 * (Width + Height);
}

var shapes = new Shape[] { new Circle(5), new Rectangle(4, 6) };
foreach (var shape in shapes)
{
    shape.Describe();
}
// Circle: площадь=78.54, периметр=31.42
// Rectangle: площадь=24.00, периметр=20.00

Интерфейсы: контракт поведения

Интерфейс — это договор: «если ты реализуешь этот интерфейс, у тебя будут вот такие методы». Как розетка: любое устройство с вилкой подключится к розетке, неважно что это за устройство.

// Определение интерфейса (по соглашению начинается с I)
interface ISwimmable
{
    void Swim(); // Метод без реализации (только сигнатура)
    int SwimmingSpeed { get; } // Свойство тоже можно
}

interface IFlyable
{
    void Fly();
    int FlyingAltitude { get; }
}

// Класс может реализовывать НЕСКОЛЬКО интерфейсов
class Duck : Animal, ISwimmable, IFlyable
{
    public Duck(string name) : base(name, 2) {}
    
    public override void MakeSound() => Console.WriteLine($"{Name}: Кря!");
    
    // Реализация ISwimmable
    public void Swim() => Console.WriteLine($"{Name} плывёт");
    public int SwimmingSpeed => 5;
    
    // Реализация IFlyable
    public void Fly() => Console.WriteLine($"{Name} летит");
    public int FlyingAltitude => 100;
}

var duck = new Duck("Кряква");
duck.MakeSound(); // Кряква: Кря!
duck.Swim();      // Кряква плывёт
duck.Fly();       // Кряква летит

// Работа через интерфейс
ISwimmable swimmer = duck; // duck как пловец
swimmer.Swim();

// Проверка реализации интерфейса
if (duck is IFlyable flyer)
{
    Console.WriteLine($"Летит на высоте {flyer.FlyingAltitude}м");
}

Итого

  • ✅ class — ссылочный тип, передаётся по ссылке, поддерживает наследование
  • ✅ struct — тип-значение, хранится в стеке, копируется при присваивании
  • ✅ record — неизменяемые данные, сравнение по значению
  • ✅ Свойства — умные поля с get/set и возможностью валидации
  • ✅ Конструктор — метод инициализации объекта
  • ✅ Наследование — дочерний класс получает всё от родительского
  • ✅ virtual/override — переопределение поведения
  • ✅ abstract — абстрактный класс/метод, нельзя создать экземпляр
  • ✅ interface — контракт поведения, класс может реализовывать несколько
  • ✅ Полиморфизм — один код работает с разными типами

Дженерики (Generics): универсальные классы

// Обобщённый (generic) класс работает с любым типом T
class Stack
{
    private readonly List _items = new();
    
    public void Push(T item) => _items.Add(item);
    
    public T Pop()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Стек пуст!");
        var item = _items[^1]; // Индекс с конца (C# 8+)
        _items.RemoveAt(_items.Count - 1);
        return item;
    }
    
    public T Peek() => _items.Count > 0 ? _items[^1] 
        : throw new InvalidOperationException("Стек пуст!");
    
    public int Count => _items.Count;
    public bool IsEmpty => _items.Count == 0;
    
    public override string ToString() => $"Stack[{string.Join(", ", _items)}]";
}

// Один класс — разные типы!
var intStack = new Stack();
intStack.Push(1); intStack.Push(2); intStack.Push(3);
Console.WriteLine(intStack);       // Stack[1, 2, 3]
Console.WriteLine(intStack.Pop()); // 3
Console.WriteLine(intStack.Peek()); // 2

var strStack = new Stack();
strStack.Push("Алиса"); strStack.Push("Борис");
Console.WriteLine(strStack.Pop()); // Борис

Паттерны проектирования: Singleton и Factory

// Singleton — гарантирует единственный экземпляр
class Database
{
    private static Database? _instance;
    private static readonly object _lock = new();
    
    private string ConnectionString { get; }
    
    private Database()  // Приватный конструктор!
    {
        ConnectionString = "Server=localhost;Database=mydb";
        Console.WriteLine("Подключение к БД установлено");
    }
    
    public static Database Instance
    {
        get
        {
            if (_instance is null)
            {
                lock (_lock)
                {
                    _instance ??= new Database();
                }
            }
            return _instance;
        }
    }
    
    public void Query(string sql)
        => Console.WriteLine($"Выполняем: {sql}");
}

// Factory — создаёт объекты по условию
abstract class Notification
{
    public abstract void Send(string message);
}

class EmailNotification : Notification
{
    private string _to;
    public EmailNotification(string to) => _to = to;
    public override void Send(string message)
        => Console.WriteLine($"Email → {_to}: {message}");
}

class SmsNotification : Notification
{
    private string _phone;
    public SmsNotification(string phone) => _phone = phone;
    public override void Send(string message)
        => Console.WriteLine($"SMS → {_phone}: {message}");
}

static class NotificationFactory
{
    public static Notification Create(string type, string contact)
        => type.ToLower() switch
        {
            "email" => new EmailNotification(contact),
            "sms"   => new SmsNotification(contact),
            _       => throw new ArgumentException($"Неизвестный тип: {type}")
        };
}

// Использование
var db1 = Database.Instance;
var db2 = Database.Instance;
Console.WriteLine(ReferenceEquals(db1, db2));  // True — один объект!
db1.Query("SELECT * FROM users");

var notif = NotificationFactory.Create("email", "user@example.com");
notif.Send("Добро пожаловать!");

Event: события и подписки

// Events — сигналы между объектами
class Button
{
    public string Text { get; set; }
    // Событие — список подписчиков
    public event EventHandler? Clicked;
    public event EventHandler? ClickedWithCount;
    
    private int _clickCount = 0;
    
    public Button(string text) => Text = text;
    
    public void Click()
    {
        _clickCount++;
        Console.WriteLine($"Кнопка '{Text}' нажата (#{_clickCount})");
        Clicked?.Invoke(this, EventArgs.Empty);
        ClickedWithCount?.Invoke(this, _clickCount);
    }
}

var btn = new Button("OK");

// Подписываемся на событие (lambda)
btn.Clicked += (sender, e) => Console.WriteLine("  → Обработчик 1: кнопка нажата!");
btn.Clicked += (sender, e) => Console.WriteLine("  → Обработчик 2: записываем в лог");
btn.ClickedWithCount += (sender, count) =>
{
    if (count >= 3) Console.WriteLine($"  → Внимание: нажата {count} раза подряд!");
};

btn.Click();
btn.Click();
btn.Click();