ООП. Часть 2: особенности работы с объектами

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

Это вторая статья из цикла про ООП. Поэтому советуем сначала прочесть предыдущую часть.

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

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

Евгений Кучерявый

Пишет о разработке сайтов, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Работа со ссылочным типом данных

Данные, с которыми работает программа, хранятся в оперативной памяти. Для этого используются ячейки, у которых есть адрес (ключ) и значение.

Раньше программистам приходилось запоминать эти ключи или записывать их в отдельный блокнот. Но современные языки программирования позволяют создавать переменные (их называют примитивными или значимыми типами данных), у которых вместо адреса идентификатор:

int sum = 50; //sum — ключ, 50 — значение 
string name = "John";

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

Character hero = new Character();

Но в ней будет храниться не сам объект, а ссылка на него. Поэтому объекты относятся к ссылочным типам данных, и из-за этого работа с ними отличается от работы с примитивными типами.

Рассмотрим такой код:

Character hero = new Character("Player", 50, 20);

Character hero1 = new Character("Player", 50, 20);

if (hero == hero1)
{
	Console.WriteLine("true");
}
else
{
	Console.WriteLine("false");
}

Console.ReadKey();

Здесь сначала создаются, а потом сравниваются два идентичных объекта. Если они равны, то выводится true, а иначе — false. Вот что выведет программа:

Несмотря на то что все поля совпадают, программа выводит false. И это не баг. Дело в том, что сравниваются не значения полей объектов, а ссылки на них. А поскольку это два различных объекта, то и адреса у них разные.

Строка true будет выведена только в том случае, если в обеих переменных находится ссылка на один и тот же объект. Чтобы это проверить, в коде из примера выше нужно изменить значение переменной hero1:

Character hero1 = hero;

Теперь программа выведет true, потому что это один и тот же объект:

И теперь, если изменить hero1, hero тоже изменится:

Character hero = new Character("Player", 50, 20);

Character hero1 = hero;

hero1.Move("up");

Console.WriteLine(hero.Coordinates);

Console.ReadKey();

Тут меняются координаты объекта hero1 с помощью метода Move(), а затем выводятся координаты объекта hero. Они были [ 50 , 20 ], но теперь по оси Y значение будет 21:

Если нужно проверить, совпадают ли значения полей у объектов, то надо вручную проверять каждое поле:

if (hero.X == hero1.X && hero.Y == hero1.Y)
{
	Console.WriteLine("true");
}
else
{
	Console.WriteLine("false");
}

Однако это неудобно, если полей очень много. Чтобы упростить эту задачу, можно использовать метод Equals() — его мы рассмотрим в статье про полиморфизм. Если же надо сделать копию, то можно написать метод Clone() или Copy(). Он будет принимать объект, чтобы скопировать его данные:

public void Clone(Character obj)
{
	this.name = obj.name;
	this.x = obj.x;
	this.y = obj.y;
	this.health = obj.health;
}

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

Использование объекта в качестве аргумента

Сразу же стоит отметить, что у передачи объекта в качестве аргумента есть побочные эффекты. Рассмотрим такой код:

static void Main(string[] args)
{
	int num = 2;

	Multiply(num);

	Console.WriteLine(num);

	Console.ReadKey();
}

private static void Multiply(int num)
{
	num *= 2;
}

В методе Multiply() полученное число умножится на два. Но так как int — это значимый тип, то в метод будет передана не ссылка на это значение в памяти, а само число. То есть будет создана новая переменная (их называют локальными) с таким же значением, и уже она будет умножена на два. На переданное значение это никак не повлияет.

Вот что выведет программа:

Однако если использовать объект, то будет передана ссылка на него, а все операции будут проводиться с ним, а не с его значением:

static void Main(string[] args)
{
	Character hero = new Character(50, 20);

	Change(hero);

	Console.WriteLine(hero.Health);

	Console.ReadKey();
}

private static void Change(Character hero)
{
	hero.Health = 4;
}

Вот что выведет программа:

Такие особенности ссылочных типов нужно учитывать при разработке программ, чтобы не происходило ничего неожиданного. Например, когда значение меняется, хотя и не должно.

Домашнее задание

Создайте класс Game, в котором будут храниться все игровые объекты. Дайте персонажу возможность перемещаться по карте и взаимодействовать с объектами, координаты которых совпадают с его позицией.


Подсказка

Используйте коллекции и циклы.


Заключение

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

Изучению и закреплению работы в парадигме ООП посвящена большая часть курса «Профессия С#-разработчик». Студенты не только узнают теорию, но и практикуются на проектах, которые затем смогут добавить в портфолио.

Курс

Профессия С#-разработчик


130 часов — и вы научитесь писать программы на языке, созданном Microsoft. Мы создадим 5 проектов для портфолио, даже если до этого никогда не программировали. После прохождения обучения — гарантированное трудоустройство.

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox