5_Классы и структуры

Урок 5 : Классы и структуры

О чём этот урок

Классы и структуры - основа объектно-ориентированного программирования. Класс описывает шаблон объекта: его данные (поля) и поведение (методы).


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

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

Класс - шаблон для создания объектов. Описывает, какие данные хранит объект и что умеет делать.

// Определение класса
class Player
{
    // Поля - данные объекта
    public string name;
    public int health;
    public int level;

    // Метод - поведение объекта
    public void Attack()
    {
        Console.WriteLine($"{name} атакует!");
    }
}

// Создание объектов (экземпляров) класса
Player hero = new Player();
hero.name   = "Артём";
hero.health = 100;
hero.level  = 1;
hero.Attack(); // "Артём атакует!"

Player enemy = new Player(); // независимый объект
enemy.name   = "Гоблин";
Класс vs Объект

  • Класс - это чертёж. Он один.
  • Объект (экземпляр) - это конкретная вещь, построенная по чертежу. Их может быть много.

Класс Player - один. Объектов hero, enemy, boss - сколько угодно.

Класс является ссылочным типом и наследуется от System.Object (базового класса всех классов в C#).

NullReferenceException

Поскольку класс - ссылочный тип, переменная может хранить null - то есть не указывать ни на какой объект. Попытка вызвать метод или обратиться к полю через такую переменную вызывает NullReferenceException - одну из самых частых ошибок в C#.

Player hero = null; // переменная объявлена, но объект не создан

hero.Attack(); // NullReferenceException!
               // hero не указывает ни на какой объект

Когда возникает

Player p;           // p не инициализирована - тоже null
// p.health = 10;   // NullReferenceException

Player[] team = new Player[3]; // массив создан, но объекты в нём - null
// team[0].Attack();            // NullReferenceException

Проверка на null перед использованием

Простейшая защита - явная проверка if:

Player hero = GetPlayer(); // метод может вернуть null

if (hero != null)
{
    hero.Attack(); // безопасно
}

// Современная форма
if (hero is not null)
{
    hero.Attack();
}

Оператор ?. - безопасный вызов

?. вызывает метод или обращается к свойству только если объект не null. Если объект null - выражение возвращает null вместо исключения.

Player hero = null;

hero?.Attack();                    // ничего не произойдёт, без исключения

string name = hero?.name;         // name == null
int? hp     = hero?.health;       // hp == null

// Комбинация ?. и ?? - безопасное чтение со значением по умолчанию
string displayName = hero?.name ?? "Безымянный";
int    displayHp   = hero?.health ?? 0;

Console.WriteLine(displayName); // "Безымянный"
Console.WriteLine(displayHp);   // 0
NullReferenceException - сигнал о незаполненной ссылке

Ошибка говорит: «Я попытался обратиться к объекту, но там null». Чаще всего причина - забытый new при создании объекта или метод, который мог вернуть null.

Типичная ошибка в Unity

// Поле объявлено, но не назначено в инспекторе
public Rigidbody rb;

void Start()
{
    rb.AddForce(Vector3.up); // NullReferenceException, если rb не задан!
}

Решение: проверить if (rb != null) или использовать rb?.AddForce(Vector3.up).


2. Поля и свойства

Поле - переменная внутри класса, хранит данные объекта.

class Enemy
{
    // Поля (рекомендуется делать приватными)
    private string _name;
    private int _health;

    // Свойства - управляемый доступ к полям
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    public int Health
    {
        get { return _health; }
        set { _health = Math.Max(0, value); } // нельзя уйти в минус
    }
}

Автосвойства (сокращённая запись)

class Item
{
    // Компилятор сам создаёт скрытое поле
    public string Name { get; set; }
    public int Price { get; set; }
    public bool IsRare { get; private set; } // только чтение снаружи
}

Item sword = new Item();
sword.Name  = "Меч";
sword.Price = 150;
Соглашение об именовании полей

Приватные поля принято называть с нижнего подчёркивания: _health, _name. Это явно отличает поле от локальной переменной.


3. Конструктор

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

class Weapon
{
    public string Name { get; }
    public int Damage { get; }
    public float Weight { get; }

    // Конструктор
    public Weapon(string name, int damage, float weight)
    {
        Name   = name;
        Damage = damage;
        Weight = weight;
    }

    // Перегруженный конструктор с дефолтным весом
    public Weapon(string name, int damage) : this(name, damage, 1.0f)
    {
        // : this(...) - вызов другого конструктора этого же класса
    }
}

// Создание объектов через конструктор
Weapon sword = new Weapon("Меч", 50, 2.5f);
Weapon bow   = new Weapon("Лук", 30);  // weight = 1.0f по умолчанию
Конструктор по умолчанию

Если не объявить ни одного конструктора, компилятор создаёт пустой конструктор без параметров автоматически. Как только вы объявляете свой конструктор с параметрами - конструктор по умолчанию исчезает.


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

Контролируют, кто может видеть и использовать члены класса.

Модификатор Доступен из...
public Отовсюду
private Только внутри этого класса
protected Внутри класса и его потомков
internal Внутри того же проекта (сборки)
class BankAccount
{
    private decimal _balance;    // только внутри класса
    public string Owner { get; } // доступно всем

    public void Deposit(decimal amount)
    {
        if (amount > 0)
            _balance += amount;  // приватное поле доступно внутри класса
    }

    public decimal GetBalance() => _balance; // публичный метод чтения
}

BankAccount acc = new BankAccount();
acc.Deposit(1000);
// acc._balance = 999; // ОШИБКА: private недоступен снаружи

5. Ключевое слово this

this - ссылка на текущий объект (тот, для которого вызван метод).

class Character
{
    private string name;
    private int level;

    // this разрешает конфликт имён параметра и поля
    public Character(string name, int level)
    {
        this.name  = name;   // this.name - поле, name - параметр
        this.level = level;
    }

    // this в методе - ссылка на себя
    public Character LevelUp()
    {
        this.level++;
        return this;  // возврат самого объекта (для цепочки вызовов)
    }
}

6. Структуры (struct)

Структура - похожа на класс, но является значимым типом. Хранится в стеке, при присваивании копируется полностью.

struct Point
{
    public float X;
    public float Y;

    public Point(float x, float y)
    {
        X = x;
        Y = y;
    }

    public float DistanceTo(Point other)
    {
        float dx = X - other.X;
        float dy = Y - other.Y;
        return MathF.Sqrt(dx * dx + dy * dy);
    }
}

Point a = new Point(0, 0);
Point b = new Point(3, 4);
Console.WriteLine(b.DistanceTo(a)); // 5

// Копирование значения (не ссылки)
Point c = a;
c.X = 99;
Console.WriteLine(a.X); // 0 - a не изменился!

Класс vs Структура

Характеристика class struct
Тип Ссылочный Значимый
Хранится в Куча (heap) Стек (stack)
При присваивании Копируется ссылка Копируются данные
Наследование Да Нет
Значение по умолчанию null Нулевые поля
Подходит для Сложные объекты с логикой Маленькие данные (координаты, цвет)
Struct в Unity

В Unity Vector2, Vector3, Color, Quaternion - всё это структуры. Это объясняет, почему transform.position.x = 5 не работает: вы изменяете копию, а не оригинал. Нужно присваивать весь вектор: transform.position = new Vector3(5, 0, 0).


Для углублённого изучения

Модификатор static

static означает, что метод или поле принадлежат классу, а не конкретному объекту.

class GameManager
{
    // Статическое поле - одно на весь класс
    public static int TotalEnemiesKilled = 0;

    // Статический метод - вызывается без создания объекта
    public static void ResetGame()
    {
        TotalEnemiesKilled = 0;
        Console.WriteLine("Игра сброшена");
    }
}

// Вызов без создания объекта
GameManager.ResetGame();
GameManager.TotalEnemiesKilled++;

// Статический класс - нельзя создать его экземпляр
static class MathHelper
{
    public static int Clamp(int value, int min, int max)
    {
        return Math.Max(min, Math.Min(max, value));
    }
}

int hp = MathHelper.Clamp(150, 0, 100); // 100

Наследование

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

// Базовый класс
class Animal
{
    public string Name { get; set; }

    public void Breathe()
    {
        Console.WriteLine($"{Name} дышит");
    }

    // virtual - метод можно переопределить в потомке
    public virtual void MakeSound()
    {
        Console.WriteLine("...");
    }
}

// Потомок наследует Animal
class Dog : Animal
{
    // override - переопределяем виртуальный метод
    public override void MakeSound()
    {
        Console.WriteLine("Гав!");
    }

    // Новый метод, специфичный для собаки
    public void Fetch()
    {
        Console.WriteLine($"{Name} приносит мяч");
    }
}

Dog dog = new Dog();
dog.Name = "Рекс";
dog.Breathe();    // унаследован от Animal
dog.MakeSound();  // "Гав!" - переопределён
dog.Fetch();      // собственный метод

Ключевое слово base

base позволяет обратиться к методу родительского класса из переопределённого метода. Полезно, когда нужно расширить поведение, а не полностью заменить его.

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

    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} издаёт звук");
    }
}

class Dog : Animal
{
    public override void MakeSound()
    {
        base.MakeSound();       // сначала вызываем родительский метод
        Console.WriteLine("Гав!");
    }
}

Dog dog = new Dog { Name = "Рекс" };
dog.MakeSound();
// Рекс издаёт звук
// Гав!

base также используется для вызова конструктора родителя:

class Vehicle
{
    public string Brand { get; }
    public Vehicle(string brand) { Brand = brand; }
}

class Car : Vehicle
{
    public int Doors { get; }

    public Car(string brand, int doors) : base(brand) // вызов конструктора Vehicle
    {
        Doors = doors;
    }
}

override vs new - переопределение vs сокрытие

override - настоящее переопределение. При вызове через переменную базового типа сработает метод потомка (полиморфизм работает).

new - сокрытие метода родителя. При вызове через переменную базового типа сработает метод родителя (полиморфизм не работает).

class Base
{
    public virtual void Greet() => Console.WriteLine("Base.Greet");
    public void Hello()         => Console.WriteLine("Base.Hello");
}

class Child : Base
{
    public override void Greet() => Console.WriteLine("Child.Greet"); // переопределение
    public new void Hello()      => Console.WriteLine("Child.Hello");  // сокрытие
}

Base obj = new Child();
obj.Greet(); // Child.Greet - полиморфизм работает
obj.Hello(); // Base.Hello  - сокрытие через new не даёт полиморфизма!
Предпочитайте override вместо new

new отключает полиморфизм и редко является правильным выбором. Если хотите изменить поведение метода в потомке - делайте метод virtual в родителе и используйте override.


Инкапсуляция

Инкапсуляция - принцип ООП: скрывать внутреннее устройство объекта и предоставлять только безопасный публичный интерфейс.

class Character
{
    private int _health;       // скрытое состояние
    private int _maxHealth;

    public int Health => _health; // только чтение снаружи

    public Character(int maxHealth)
    {
        _maxHealth = maxHealth;
        _health    = maxHealth;
    }

    // Публичный метод - единственный способ изменить здоровье
    public void TakeDamage(int damage)
    {
        if (damage < 0) return;             // валидация
        _health = Math.Max(0, _health - damage);
        if (_health == 0) Die();
    }

    public void Heal(int amount)
    {
        _health = Math.Min(_maxHealth, _health + amount);
    }

    private void Die()
    {
        Console.WriteLine("Персонаж умер");  // приватная логика
    }
}
Зачем инкапсуляция?

Без неё любой код мог бы написать character._health = -9999. Инкапсуляция гарантирует, что объект всегда находится в корректном состоянии - его нельзя «сломать» снаружи.


Итог урока


📋 Перейти к практическим заданиям урока 5

Материалы для дополнительного ознакомления :

Классы и объекты
Конструкторы, инициализаторы, деструкторы
Свойства
Точка входа Main
Структуры
Наследование
Модификаторы доступа
Модификатор static