🔷 C#

ООП: Классы и объекты в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты решил построить город из конструктора LEGO. У тебя есть коробка с деталями, но чтобы построить, например, дом, тебе нужен чертёж. По одному чертежу ты можешь собрать сотни одинаковых домов. А если захочешь добавить в дом окно или дверь — просто изменишь чертёж, и все новые дома будут уже с окнами.

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

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

Зачем это нужно? Представь, что ты пишешь игру. У тебя 100 врагов. Если не использовать классы, тебе придётся для каждого врага вручную прописывать его координаты, здоровье, скорость и действия. А с классами ты один раз описываешь класс Enemy, а потом создаёшь 100 объектов этого класса — быстро, удобно, и если нужно добавить всем врагам новое свойство, ты меняешь только класс.

Теория: как это работает изнутри

Когда программа запускается, компьютер выделяет память для данных. Класс — это просто описание, он не занимает места в памяти, пока ты не создашь объект. Когда ты пишешь new ClassName(), происходит вот что:

  1. Компьютер выделяет в памяти (в куче — heap) место под новый объект.
  2. Вызывается специальный метод — конструктор, который инициализирует свойства объекта начальными значениями.
  3. Ссылка на этот объект сохраняется в переменной, с которой ты работаешь.

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

Важно: Переменная, которая хранит объект, на самом деле хранит не сам объект, а ссылку на него. Если ты присвоишь одну переменную другой, они будут ссылаться на один и тот же объект. Изменения через одну переменную увидятся и через другую.

В C# есть два основных типа данных: значимые (int, double, bool, struct) и ссылочные (class, string, array). Классы относятся к ссылочным типам. Это значит, что при передаче объекта в метод, ты передаёшь ссылку, а не копию. Метод может изменить твой оригинальный объект.

Синтаксис в C# (.NET): все формы и варианты

Давай разберём, как объявлять классы и создавать объекты в C#. Начнём с самого простого и будем усложнять.

Объявление класса

// Самая простая форма объявления класса
class Car
{
    // Поля (поля данных) — переменные, которые хранят состояние объекта
    public string brand;
    public string model;
    public int year;
    
    // Метод — действие, которое может выполнять объект
    public void Honk()
    {
        Console.WriteLine("Beep beep!");
    }
}

Создание объекта (экземпляра класса)

// Создаём объект класса Car
Car myCar = new Car();

// Теперь можно обращаться к его полям и методам
myCar.brand = "Toyota";
myCar.model = "Camry";
myCar.year = 2020;
myCar.Honk();  // Выведет: Beep beep!

Конструкторы

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

class Car
{
    public string Brand { get; set; }  // Свойство (property) — современный способ
    public string Model { get; set; }
    public int Year { get; set; }
    
    // Конструктор без параметров (по умолчанию)
    public Car()
    {
        Brand = "Unknown";
        Model = "Unknown";
        Year = 2000;
    }
    
    // Конструктор с параметрами
    public Car(string brand, string model, int year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }
}

// Использование
Car car1 = new Car();                    // Без параметров
Car car2 = new Car("BMW", "X5", 2022);  // С параметрами

Свойства (Properties)

В C# принято использовать не поля, а свойства. Они выглядят как поля, но внутри могут содержать логику.

class Person
{
    // Автоматическое свойство — компилятор сам создаёт поле для хранения
    public string Name { get; set; }
    
    // Свойство с проверкой
    private int age;
    public int Age
    {
        get { return age; }
        set 
        { 
            if (value >= 0 && value <= 150)
                age = value;
            else
                Console.WriteLine("Некорректный возраст!");
        }
    }
    
    // Свойство только для чтения
    public string Greeting { get; } = "Hello!";
}

// Использование
Person p = new Person();
p.Name = "Анна";
p.Age = 25;  // Всё хорошо
p.Age = 200; // Выведет: Некорректный возраст!

Модификаторы доступа

Модификатор Описание
public Доступно отовсюду
private Доступно только внутри этого класса
protected Доступно внутри класса и в классах-наследниках
internal Доступно в пределах одной сборки (проекта)

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

Иногда нужно, чтобы свойство или метод принадлежали не объекту, а всему классу. Например, счётчик созданных объектов.

class Counter
{
    // Статическое поле — одно на все объекты
    public static int TotalCount { get; private set; }
    
    // Обычное поле — своё для каждого объекта
    public int InstanceNumber { get; }
    
    public Counter()
    {
        TotalCount++;
        InstanceNumber = TotalCount;
    }
    
    // Статический метод — вызывается через имя класса
    public static void ShowTotal()
    {
        Console.WriteLine($"Всего создано объектов: {TotalCount}");
    }
}

// Использование
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter.ShowTotal();  // Выведет: Всего создано объектов: 2
Console.WriteLine(c1.InstanceNumber);  // 1
Console.WriteLine(c2.InstanceNumber);  // 2

Примеры кода — от простого к сложному

Пример 1: Простейший класс «Студент»

using System;

class Student
{
    public string Name;
    public int Age;
    
    public void Introduce()
    {
        Console.WriteLine($"Привет! Я {Name}, мне {Age} лет.");
    }
}

class Program
{
    static void Main()
    {
        // Создаём объект
        Student student = new Student();
        
        // Заполняем поля
        student.Name = "Иван";
        student.Age = 15;
        
        // Вызываем метод
        student.Introduce();  // Выведет: Привет! Я Иван, мне 15 лет.
    }
}

Объяснение: Мы создали класс Student с двумя полями и одним методом. В Main создали объект, заполнили его данные и вызвали метод. Всё просто и понятно.

Пример 2: Класс с конструктором и свойствами

using System;

class Book
{
    // Автоматические свойства
    public string Title { get; set; }
    public string Author { get; set; }
    public int Pages { get; set; }
    
    // Конструктор
    public Book(string title, string author, int pages)
    {
        Title = title;
        Author = author;
        Pages = pages;
    }
    
    // Метод для вывода информации
    public void DisplayInfo()
    {
        Console.WriteLine($"Книга: "{Title}"");
        Console.WriteLine($"Автор: {Author}");
        Console.WriteLine($"Страниц: {Pages}");
    }
}

class Program
{
    static void Main()
    {
        // Создаём объект через конструктор с параметрами
        Book myBook = new Book("Война и мир", "Лев Толстой", 1225);
        myBook.DisplayInfo();
    }
}

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

Пример 3: Несколько объектов одного класса

using System;

class Robot
{
    public string Name { get; set; }
    public int BatteryLevel { get; set; }
    
    public Robot(string name)
    {
        Name = name;
        BatteryLevel = 100;
    }
    
    public void Work()
    {
        if (BatteryLevel >= 10)
        {
            Console.WriteLine($"{Name} работает...");
            BatteryLevel -= 10;
        }
        else
        {
            Console.WriteLine($"{Name} разряжен! Нужна зарядка.");
        }
    }
    
    public void Charge()
    {
        BatteryLevel = 100;
        Console.WriteLine($"{Name} заряжен до 100%.");
    }
}

class Program
{
    static void Main()
    {
        // Создаём несколько роботов
        Robot robot1 = new Robot("R2-D2");
        Robot robot2 = new Robot("C-3PO");
        
        // Работаем с первым роботом
        robot1.Work();  // R2-D2 работает...
        robot1.Work();  // R2-D2 работает...
        
        // Второй робот пока не работал, у него полный заряд
        Console.WriteLine($"Заряд {robot2.Name}: {robot2.BatteryLevel}%");  // 100%
        
        // Заряжаем первого
        robot1.Charge();
    }
}

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

Пример 4: Статические члены и счётчик объектов

using System;

class User
{
    // Статическое поле — общее для всех объектов
    private static int totalUsers = 0;
    
    // Обычное поле — своё для каждого
    public int Id { get; }
    public string Name { get; set; }
    
    public User(string name)
    {
        totalUsers++;
        Id = totalUsers;
        Name = name;
    }
    
    public static void ShowTotalUsers()
    {
        Console.WriteLine($"Всего пользователей: {totalUsers}");
    }
    
    public void ShowInfo()
    {
        Console.WriteLine($"ID: {Id}, Имя: {Name}");
    }
}

class Program
{
    static void Main()
    {
        User user1 = new User("Алиса");
        User user2 = new User("Боб");
        User user3 = new User("Чарли");
        
        user1.ShowInfo();  // ID: 1, Имя: Алиса
        user2.ShowInfo();  // ID: 2, Имя: Боб
        user3.ShowInfo();  // ID: 3, Имя: Чарли
        
        // Статический метод вызывается через имя класса
        User.ShowTotalUsers();  // Всего пользователей: 3
    }
}

Объяснение: Статическое поле totalUsers одно на всех. Каждый новый пользователь увеличивает его на 1. ID присваивается автоматически.

Пример 5: Композиция классов (класс внутри класса)

using System;

class Engine
{
    public int HorsePower { get; set; }
    public string Type { get; set; }
    
    public Engine(int hp, string type)
    {
        HorsePower = hp;
        Type = type;
    }
    
    public void Start()
    {
        Console.WriteLine($"Двигатель {Type} ({HorsePower} л.с.) запущен.");
    }
}

class Car
{
    public string Brand { get; set; }
    public string Model { get; set; }
    
    // Композиция: машина содержит двигатель
    public Engine Engine { get; set; }
    
    public Car(string brand, string model, Engine engine)
    {
        Brand = brand;
        Model = model;
        Engine = engine;
    }
    
    public void StartCar()
    {
        Console.WriteLine($"Запуск {Brand} {Model}...");
        Engine.Start();  // Двигатель запускается внутри машины
    }
}

class Program
{
    static void Main()
    {
        // Сначала создаём двигатель
        Engine v8 = new Engine(400, "V8");
        
        // Потом создаём машину с этим двигателем
        Car myCar = new Car("Ford", "Mustang", v8);
        
        // Запускаем машину — запускается и двигатель
        myCar.StartCar();
    }
}

Объяснение: Один класс может содержать другой класс как свойство. Это называется композицией. Машина «состоит из» двигателя. Если уничтожить машину, двигатель тоже исчезнет (в данном случае).

Пример 6: Наследование классов

using System;

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

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

class Program
{
    static void Main()
    {
        Animal animal = new Animal("Просто животное");
        animal.MakeSound();  // Животное издаёт звук.
        
        Dog dog = new Dog("Шарик", "Овчарка");
        dog.MakeSound();  // Шарик (порода Овчарка) говорит: Гав-гав!
        
        // Полиморфизм: можем хранить собаку в переменной типа Animal
        Animal myPet = new Dog("Бобик", "Такса");
        myPet.MakeSound();  // Вызовется метод Dog, а не Animal!
    }
}

Объяснение: Наследование позволяет создать новый класс на основе существующего. Dog наследует все свойства и методы Animal, но может добавлять свои и переопределять родительские. Полиморфизм позволяет работать с разными типами через единый интерфейс.

Пример 7: Интерфейсы

using System;

// Интерфейс — контракт, который обязуются выполнять классы
interface IPlayable
{
    void Play();
    void Stop();
}

class Guitar : IPlayable
{
    public void Play()
    {
        Console.WriteLine("Гитара играет: дрынь-дрынь!");
    }
    
    public void Stop()
    {
        Console.WriteLine("Гитара замолчала.");
    }
}

class Piano : IPlayable
{
    public void Play()
    {
        Console.WriteLine("Пианино играет: тинь-тинь-тинь!");
    }
    
    public void Stop()
    {
        Console.WriteLine("Пианино замолчало.");
    }
}

class Program
{
    static void Main()
    {
        // Массив объектов, реализующих интерфейс IPlayable
        IPlayable[] instruments = new IPlayable[]
        {
            new Guitar(),
            new Piano()
        };
        
        foreach (var instrument in instruments)
        {
            instrument.Play();
            instrument.Stop();
        }
    }
}

Объяснение: Интерфейс определяет, что должен уметь делать класс, но не говорит, как именно. Разные классы могут реализовывать один интерфейс по-своему. Это основа гибкости и расширяемости кода.

Сравнение с Python: чем похоже и чем отличается

Характеристика C# Python
Объявление класса class MyClass { } class MyClass:
Конструктор public MyClass() { } def __init__(self):
Свойства Есть встроенные { get; set; } Через @property декоратор
Модификаторы доступа public, private, protected По соглашению: _private, __private
Статические члены static ключевое слово Обычные методы класса, декоратор @staticmethod
Наследование Только однонаследование классов Множественное наследование
Интерфейсы Есть, через interface Через абстрактные классы (ABC)
Типизация Строгая статическая Динамическая (с необязательными аннотациями)
Ссылочные типы Классы — ссылочные Всё — объекты, но неизменяемые типы ведут себя как значимые

Главное отличие: C# — строго типизированный язык. Ты должен явно указывать тип переменной, и компилятор проверяет, что ты не пытаешься сделать что-то недопустимое. Python — динамический, типы проверяются во время выполнения. Это делает C# более безопасным, но требует больше кода.

Сходство: Оба языка поддерживают основные принципы ООП: инкапсуляцию (скрытие данных), наследование и полиморфизм. Если ты поймёшь концепции на C#, перейти на Python будет легко — нужно только выучить синтаксис.

Типичные ошибки и подводные камни

1. Забыл создать объект через new

Car myCar;  // Объявили переменную, но объект не создали!
myCar.brand = "Toyota";  // Ошибка! myCar == null

Решение: Всегда создавай объект через new перед использованием.

2. Путаница между полями и свойствами

class BadExample
{
    public int age;  // Поле — так делать не рекомендуется
}

class GoodExample
{
    public int Age { get; set; }  // Свойство — правильно
}

Решение: В C# принято использовать свойства. Они дают больше контроля и совместимы с некоторыми технологиями (например, WPF).

3. Изменение объекта через ссылку

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

void ChangeName(Person p)
{
    p.Name = "Новое имя";  // Изменяет оригинальный объект!
}

// Использование
Person person = new Person { Name = "Старое имя" };
ChangeName(person);
Console.WriteLine(person.Name);  // Выведет: Новое имя

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

4. Забыл про модификаторы доступа

class Secret
{
    private string password = "12345";  // Приватное поле
}

// В другом классе
Secret s = new Secret();
// Console.WriteLine(s.password);  // Ошибка! Недоступно

Решение: Продумывай, какие данные должны быть доступны снаружи, а какие — нет. Используй private для внутренних данных и public только для того, что должно быть доступно.

5. Путаница со статическими и обычными членами

class MathHelper
{
    public static int Add(int a, int b) => a + b;
    public int Multiply(int a, int b) => a * b;
}

// Правильно:
MathHelper.Add(5, 3);  // Статический метод — через имя класса

// Ошибка:
MathHelper.Multiply(5, 3);  // Нестатический метод — нужен объект!

// Правильно:
MathHelper helper = new MathHelper();
helper.Multiply(5, 3);

Решение: Статические члены вызываются через имя класса, нестатические — через объект.

6. Бесконечная рекурсия в свойствах

class BadProperty
{
    private int value;
    
    public int Value
    {
        get { return Value; }  // Ошибка! Вызывает сам себя бесконечно
        set { Value = value; }  // И здесь та же ошибка
    }
}

Решение: Внутри свойства обращайся к полю (с маленькой буквы), а не к самому свойству.

Лучшие практики и советы профессионалов

  1. Используй свойства вместо полей. Это даёт гибкость: ты можешь добавить проверку, логирование или вычисление без изменения кода, который использует класс.
  2. Давай классам и методам осмысленные имена. Класс Car — хорошо, класс DataProcessorManagerHelper — плохо. Имя должно отражать суть.
  3. Принцип единственной ответственности. Один класс — одна задача. Если класс «Студент» умеет и учиться, и печатать отчёты, и отправлять email — это плохо. Раздели на несколько классов.
  4. Инкапсулируй данные. Делай поля приватными, а доступ к ним — через публичные свойства или методы. Так ты контролируешь, как данные изменяются.
  5. Используй конструкторы для обязательной инициализации. Если объект не может существовать без каких-то данных, передавай их через конструктор.
  6. Не бойся создавать маленькие классы. Лучше 10 маленьких классов, чем один огромный на 1000 строк. Маленькие классы легче тестировать и поддерживать.
  7. Изучи принципы SOLID. Это пять принципов хорошего ООП-дизайна. Они помогут писать код, который легко расширять и изменять.
  8. Используй интерфейсы для слабой связанности. Зависимость от интерфейса, а не от конкретного класса, делает код гибким. Ты можешь заменить одну реализацию на другую без изменения остального кода.
Золотое правило: Пиши код так, как будто его будут читать другие разработчики (или ты сам через полгода). Понятный код — хороший код.

Вопросы для самопроверки

  1. Вопрос: Что такое класс и что такое объект? Приведи аналогию.

    Ответ: Класс — это чертёж (шаблон), а объект — конкретная деталь, созданная по этому чертежу. Например, класс «Дом» описывает, что у дома есть стены, крыша, окна. А объект — это конкретный дом с красной крышей и синими стенами.

  2. Вопрос: Для чего нужен конструктор?

    Ответ: Конструктор — это метод, который вызывается при создании объекта. Он нужен, чтобы задать начальные значения свойств объекта. Если не написать свой конструктор, C# создаст конструктор по умолчанию без параметров.

  3. Вопрос: Чем отличается поле от свойства в C#?

    Ответ: Поле — это просто переменная внутри класса. Свойство — это специальная конструкция с блоками get и set, которая может содержать логику при чтении или записи значения. В C# принято использовать свойства.

  4. Вопрос: Что такое статический член класса? Как к нему обращаться?

    Ответ: Статический член принадлежит не объекту, а всему классу. Он один на все объекты. Обращаться к нему нужно через имя класса, а не через объект. Например: Math.PI или MyClass.StaticMethod().

  5. Вопрос: Что такое наследование? Приведи пример.

    Ответ: Наследование — это механизм, позволяющий создать новый класс на основе существующего. Новый класс (наследник) получает все свойства и методы родителя и может добавлять свои. Пример: класс Dog наследует от Animal. У собаки есть всё, что есть у животного, плюс свои особенности.

  6. Вопрос: Что такое полиморфизм?

    Ответ: Полиморфизм — это способность объектов разных классов отвечать на одни и те же вызовы методов по-разному. Например, у классов Dog и Cat есть метод MakeSound(), но собака говорит «Гав», а кошка — «Мяу». При этом мы можем работать с ними через общий тип Animal.

  7. Вопрос: Для чего нужны модификаторы доступа?

    Ответ: Модификаторы доступа управляют видимостью членов класса. public — видно всем, private — только внутри класса, protected — внутри класса и его наследников. Это помогает скрывать внутреннюю реализацию (инкапсуляция).

  8. Вопрос: Что произойдёт, если присвоить одну переменную-объект другой?

    Ответ: Обе переменные будут ссылаться на один и тот же объект. Изменения через одну переменную отразятся на другой. Это называется «ссылочное присваивание».

  9. Вопрос: Чем отличается интерфейс от класса?

    Ответ: Интерфейс определяет только сигнатуры методов (что нужно сделать), но не содержит реализации (как сделать). Класс, реализующий интерфейс, обязан предоставить реализацию всех его методов. Класс может реализовать несколько интерфейсов, но наследовать только один класс.

  10. Вопрос: Какие три основных принципа ООП?

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

Итог и что изучить дальше

Мы прошли большой путь. Ты узнал:

  • Что такое классы и объекты и зачем они нужны
  • Как объявлять классы, создавать объекты, использовать конструкторы и свойства
  • Что такое модификаторы доступа, статические члены, наследование и интерфейсы
  • Как избегать типичных ошибок
  • Какие практики помогут писать чистый и поддерживаемый код

Теперь ты готов двигаться дальше. Вот что стоит изучить следующим:

  1. Принципы SOLID — пять правил хорошего ООП-дизайна. Они помогут тебе писать код, который легко расширя