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 = "Гоблин";
- Класс - это чертёж. Он один.
- Объект (экземпляр) - это конкретная вещь, построенная по чертежу. Их может быть много.
Класс 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
Ошибка говорит: «Я попытался обратиться к объекту, но там null». Чаще всего причина - забытый new при создании объекта или метод, который мог вернуть null.
// Поле объявлено, но не назначено в инспекторе
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 |
Нулевые поля |
| Подходит для | Сложные объекты с логикой | Маленькие данные (координаты, цвет) |
В 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 не даёт полиморфизма!
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. Инкапсуляция гарантирует, что объект всегда находится в корректном состоянии - его нельзя «сломать» снаружи.
Итог урока
- Класс - шаблон объекта (ссылочный тип, наследует
object) - NullReferenceException - обращение к
null-объекту; проверяй!= nullили используй?.для безопасного вызова - Структура - лёгкий значимый тип для простых данных
- Поле - данные объекта; свойство - управляемый доступ к полю
- Конструктор - метод инициализации, вызывается при
new - Модификаторы доступа:
public- всем,private- только внутри,protected- потомкам this- ссылка на текущий объектstatic- принадлежит классу, а не экземпляру- Наследование - потомок расширяет возможности родителя
base- обращение к методу или конструктору родителя;override- переопределение с полиморфизмом;new- сокрытие без полиморфизма (использовать осторожно)- Инкапсуляция - скрывай данные, открывай только безопасный интерфейс
📋 Перейти к практическим заданиям урока 5
Материалы для дополнительного ознакомления :
Классы и объекты
Конструкторы, инициализаторы, деструкторы
Свойства
Точка входа Main
Структуры
Наследование
Модификаторы доступа
Модификатор static