В прошлом уроке мы увидели две проблемы процедурного кода: воин мог кастовать заклинания мага, а на каждого нового персонажа приходилось плодить кучу переменных. Сегодня мы это исправим — напишем первый класс и увидим, как ООП решает обе проблемы разом.
Весь наш мир состоит из объектов, которые содержат какую-то информацию и умеют что-то делать. Примеры из реальной жизни помогут нам лучше понять эту концепцию:
- Машина: имеет цвет, количество дверей, марку и модель. Она умеет заводиться, ездить, сигналить и так далее.
- Человек: имеет цвет глаз, рост, возраст и имя. Он умеет спать, ходить, бегать, говорить и так далее.
Этот принцип перенесли и в программирование. Благодаря объектно-
ориентированному подходу наша программа становится набором объектов, каждый из которых объединяет в себе как данные (свойства объекта, например, цвет и технические характеристики машины), так и методы (начать движение, переключить передачу, остановить машину и т.д.). Такой подход позволяет моделировать реальные сущности и их взаимодействие, делая код более структурированным и понятным.
Проще говоря, объект — это «супер-переменная», содержащая не только данные, но и функции для их обработки.
Таким образом, мы можем моделировать различные объекты, объединяя десятки мелких переменных в единую структуру. Будь то сотрудник, телефон или автомобиль — каждый объект будет представлять собой целостную «карточку», где собрана вся необходимая информация. И более того, данная «карточка» будет содержать варианты поведения, которыми обладает объект. Но давайте уже перейдём к созданию объекта и посмотрим, как это выглядит. Представьте, что в реальной жизни вас просят создать объект,
например: «Создай мне, пожалуйста, робота!» Сможете ли вы сразу это сделать? Наверное, у вас возникнут вопросы: «Какой именно робот? Какие у него характеристики? Что он должен уметь делать?». Ведь только на основании ответов на эти вопросы вы сможете выбрать тот чертёж робота, который подходит вам больше всего.
В программировании всё устроено аналогично: нельзя просто создать объект, не зная, какие характеристики он будет иметь и каким поведением он будет обладать — нам нужен шаблон, описывающий эти данные. Такой шаблон называется классом.
На основе классов вы сможете создавать множество однотипных объектов, не прописывая их заново каждый раз. Вместо разрозненных переменных мы получаем удобную, логичную и структурированную модель, с которой легко работать и которую можно расширять по мере необходимости.
Давайте вспомним задачу, которую мы решали в рамках прошлого урока с помощью процедурного подхода, и подойдём к её решению с помощью ООП. В первую очередь создадим класс Mage, который будет служить чертежом для создания магов.
Сразу возникает вопрос: а где же создавать этот класс в рамках кода?
До этого момента мы работали только внутри метода Main, и вы могли представить его как всю вашу программу от начала до конца. Внутри Main происходило выполнение логики наших программ: арифметические операции, операции сравнения, вывод информации и так далее. Но класс, который мы сейчас хотим создать, сам по себе не является переменной или операцией — он является лишь чертежом для создания объектов, поэтому он не создаётся внутри Main, а создаётся в отдельном файле.
А уже в Main мы будем создавать объекты на основе этого класса-чертежа и работать с ними.
Когда вы откроете созданный файл, вы увидите примерно следующее:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MagesAndWarriorsGame
{
internal class Mage
{
}
}
Пока давайте удалим всё содержимое и вместо него добавим следующий код для класса, описывающего магов:
public class Mage
{
public string Name; //Поле "Имя"
public int Mana; //Поле "Мана"
// Метод для атаки магией с проверкой и расходом маны
public void CastSpell()
{
if (Mana >= 5)
{
Mana -= 5;
Console.WriteLine($"{Name} атаковал магией. (Осталось маны: {Mana})");
}
else
{
Console.WriteLine($"Маг {Name} не может атаковать, не хватает маны. (Осталось маны: {Mana})");
}
}
}
Для создания класса используется стандартная конструкция class { }, внутри которой и будут описываться все его данные. Ключевое слово public перед class делает этот класс доступным для использования в других файлах вашего проекта. Это означает, что вы можете обращаться к этому классу из любой части программы.
Внутри класса мы можем объявлять переменные и функции.
public string Name; //Поле "Имя"
public int Mana; //Поле "Мана"
// Метод для атаки магией с проверкой и расходом маны
public void CastSpell()
{
if (Mana >= 5)
{
Mana -= 5;
Console.WriteLine($"{Name} атаковал магией. (Осталось маны: {Mana})");
}
else
{
Console.WriteLine($"Маг {Name} не может атаковать, не хватает маны. (Осталось маны: {Mana})");
}
}
Давайте разберёмся, что означает ключевое слово public и сразу обсудим одну из его альтернатив — private.
Суть проста: со временем команда разработчиков растёт, и классы, которые вы пишете, могут использоваться другими программистами. Ключевое слово public определяет, сколько свободы действий вы предоставляете другим разработчикам при работе с объектами этого класса.
- Если ваш класс объявлен как
public, то на его основе можно создавать объекты во всех частях программы. На практике классы обычно остаютсяpublic, поскольку их суть — быть доступными для использования другими разработчиками. - Объявление класса как
publicозначает, что сам класс в целом доступен из любой точки программы. Но при этом нам не обязательно делать такими же доступными все поля. Например, поля маны и имени сейчас помечены какpublic— это означает, что другие разработчики могут напрямую изменять значение этих параметров. Если же вы хотите запретить напрямую записывать и считывать значения полей, необходимо использоватьprivate. В этом случае редактировать поле можно будет только из самого класса, например, через его методы. - То же самое касается и методов. Если метод помечен как
public, его можно вызывать не только внутри самого класса, но и из любых других классов. Если же вы хотите, чтобы определённый метод вызывался только внутри класса, его следует пометить какprivate.
Сейчас, пока вы являетесь единственным разработчиком, можно оставить все элементы публичными, а в следующих уроках мы проработаем вопрос защиты данных через модификатор private.
Давайте посмотрим, как будет выглядеть решение нашей задачи с использованием объектно-ориентированного подхода. Все разрозненные переменные уходят в прошлое — на их место приходят объекты.
У нас уже есть класс Mage (маг). Создадим по аналогии класс воина:
public class Warrior
{
public string Name;
public int Stamina;
// Метод для атаки с проверкой и расходом выносливости
public void Attack()
{
if (Stamina >= 5)
{
Stamina -= 5;
Console.WriteLine($"{Name} наносит сильный удар. (Выносливость уменьшена на 5)");
}
else
{
Console.WriteLine($"Воин {Name} устал и не может атаковать");
}
}
}
Отлично, классы созданы. Теперь мы можем вернуться в наш класс Program и заняться созданием объектов. Вспоминаем аналогию: класс — это чертёж, а объект — это экземпляр, созданный по этому чертежу. Процесс создания объектов выглядит следующим образом:
Mage mage = new Mage();
Warrior warrior = new Warrior();
Вспомните инициализацию переменной:
int count = 0;
Мы указывали тип (int), имя переменной (count) и значение (0). При создании объекта мы делаем почти то же самое: указываем тип — имя класса, например Mage, даём имя объекту — mage, и используем слово new, чтобы сказать компьютеру: «Создай мне мага по этому чертежу!». Ключевое слово new сообщает компьютеру, что нужно выделить память для нового объекта.
Чтобы заполнить поля объектов значениями, мы обращаемся к ним с помощью оператора точки (.). Например:
internal class Program
{
static void Main(string[] args)
{
// Создаём магов
Mage mageAlbert = new Mage();
mageAlbert.Name = "Альберт";
mageAlbert.Mana = 20;
Mage mageBella = new Mage();
mageBella.Name = "Белла";
mageBella.Mana = 30;
// Создаём воинов
Warrior warriorEdward = new Warrior();
warriorEdward.Name = "Эдвард";
warriorEdward.Stamina = 100;
// Вызовы для магов
mageAlbert.CastSpell();
mageBella.CastSpell();
warriorEdward.Attack();
}
}
Объединив все свойства и методы мага в одном классе, мы получаем единое «хранилище» его данных —
точно так же, как если бы в жизни мы всю информацию о конкретном маге поместили на одной карточке.
Чтобы узнать любую информацию об объекте — имя или уровень магической энергии — достаточно обратиться к объекту-магу и прочитать нужное поле. И аналогичным образом через объект можно запустить любое его действие. При этом мы можем создавать сколько угодно экземпляров — магов, воинов и других персонажей — и каждый из них будет хранить свои собственные характеристики и методы.
Вы можете посмотреть на наш код и подумать, что объём кода не сократился, а, наоборот, увеличился. Хоть это и так, мы всё же достигли существенных улучшений:
Сокращение дублирования данных. Раньше нам приходилось создавать десятки отдельных переменных, например
mageName1,mageName2,mageName3для каждого мага. Теперь все данные объединены в одном классе, и каждый объект наследует этот набор свойств.Лёгкость расширения. Если мы захотим добавить новую характеристику для мага, в процедурном стиле нам пришлось бы создавать новые переменные вроде
magicLevel1,magicLevel2,magicLevel3. В ООП достаточно один раз отредактировать класс, добавив новое поле, и оно станет доступно для всех объектов.Удобство чтения. Код стал чище и понятнее. Когда мы читаем
mageAlbert.CastSpell(), мы сразу понимаем, что маг Альберт должен применить заклинание. Методы — это, по сути, обычные функции, которые находятся внутри класса. Однако есть несколько нюансов, которые стоит отметить.Модификаторы доступа. Перед методами (как и перед полями) мы можем использовать ключевые слова
publicилиprivate.Вызов методов через объект. В процедурном программировании функции можно вызывать просто по их имени. Но в ООП, чтобы вызвать метод, сначала нужно создать объект класса, а затем обратиться к методу через этот объект с помощью оператора точки (
.).Похожи на функции. В остальном методы ничем не отличаются от обычных функций: они могут принимать аргументы и возвращать значения.
Давайте расширим класс Mage, добавив метод RestoreMana, который восстанавливает магическую энергию. На вход этот метод будет принимать число — магическая энергия, которая будет восстановлена. Если кто-то передаст отрицательное значение, метод выводит предупреждение и не изменяет значение маны. Также, если суммарное значение маны превышает 100, оно устанавливается равным 100:
public class Mage
{
public string Name;
public int Mana;
public void CastSpell()
{
if (Mana >= 5)
{
Mana -= 5;
Console.WriteLine($"{Name} использует заклинание. (Мана уменьшена на 5)");
}
else
{
Console.WriteLine($"Маг {Name} не может атаковать, не хватает маны");
}
}
public void RestoreMana(int amount)
{
if (amount <= 0)
{
Console.WriteLine("Нельзя восстановить отрицательное количество маны.");
}
else
{
Mana += amount;
if (Mana > 100)
{
Mana = 100;
}
}
}
}
И для проверки создадим объект и вызовем этот метод:
static void Main(string[] args)
{
Mage gandalf = new Mage();
gandalf.Name = "Гэндальф Серый";
gandalf.Mana = 50;
Console.WriteLine($"Магическая энергия у мага {gandalf.Name}: {gandalf.Mana}");
// Восполняем ману на 30 единиц
gandalf.RestoreMana(30);
Console.WriteLine($"Значение магической энергии после восполнения {gandalf.Name}: {gandalf.Mana}");
}
Когда вы пишете метод внутри класса, нужно быть осторожным с именами параметров. Если имя аргумента метода совпадает с именем поля класса, возникает конфликт имён. В этом случае внутри метода при обращении к этому имени будет использоваться аргумент, а не поле класса.
Пример неправильного кода:
public void RestoreMana(int Mana)
{
// Остальной код...
Mana += Mana; // Не работает так, как ожидается!
// Остальной код...
}
Что здесь происходит:
- У вас есть поле
Manaв классе. - В метод
RestoreManaприходит параметр с тем же именем —Mana. - В строке
Mana += Mana;обаManaссылаются на параметр метода, а не на поле класса. - В результате компилятор не может корректно обновить значение поля.
Как это исправить? Есть два способа:
- Использовать
thisдля явного указания на поле класса:
public void RestoreMana(int Mana)
{
//Остальной код...
this.Mana = 100; // Теперь понятно, что мы обновляем поле класса
//Остальной код...
}
- Просто выбрать другое имя для аргумента, как мы и сделали выше —
amount:
public void RestoreMana(int amount)
{
Mana += amount; // Конфликта имён нет
}
Синтаксически класс выглядит следующим образом:
public class MyClass
{
public string Field; // Поле класса
public void MyMethod() // Метод класса
{
// Реализация метода
}
}
А вот так выглядит создание объекта:
MyClass myObject = new MyClass();
Для обращения к полям и для вызова методов используется оператор точки:
myObject.Field = "NewValue"; // Присваивание нового значения полю
myObject.MyMethod(); // Вызов метода объекта
Таким образом, используя классы и объекты, мы объединяем данные и поведение в одном логическом блоке, что делает код более структурированным, легко поддерживаемым и расширяемым.
Класс — это шаблон (чертёж), определяющий, какие поля и методы будут у объектов, созданных на его основе.
Объект (экземпляр класса) — это конкретная сущность, созданная по шаблону класса. Объект содержит данные и обладает поведением.
Поле — это переменная, объявленная внутри класса. Поля хранят данные объекта.
Метод — это функция, объявленная внутри класса. Методы определяют поведение объекта.
Модификатор доступа — ключевое слово, определяющее видимость элемента класса. public — доступно отовсюду, private — доступно только внутри класса.
Оператор точки (.) используется для доступа к полям и методам объекта: mage.Name, mage.CastSpell().
Для создания объекта используется ключевое слово new:
Mage mage = new Mage();
Преимущества ООП: сокращение дублирования кода, лёгкость расширения, удобство чтения. Вместо десятков отдельных переменных мы получаем объекты, каждый из которых хранит свои данные и методы.