Инкапсуляция, модификаторы доступа: 3‑я часть гайда по ООП
Классы, методы и поля не всегда могут постоять за себя. Рассказываем, как быть защитником в объектно-ориентированном программировании.


vlada_maestro / shutterstock
Инкапсуляция в программировании может показаться довольно сложной темой. Тем не менее, её необходимо освоить для уверенной работы с парадигмой ООП. В этой статье мы познакомимся с концепцией инкапсуляции и рассмотрим её на примере уровней доступа.
Все статьи про ООП
- Что такое классы и объекты.
- Особенности работы с объектами.
- Модификаторы доступа, инкапсуляция.
- Полиморфизм и перегрузка методов.
- Полиморфизм.
- Наследование и ещё немного полиморфизма.
- Абстрактные классы и интерфейсы.
- Практикум.
Что такое инкапсуляция в ООП
Представьте автомобиль с автоматической коробкой передач (АКП). Во время езды водитель такой машины выполняет три действия: крутит руль на поворотах; нажимает педаль газа, чтобы ускориться, и педаль тормоза — чтобы остановиться. Он не задумывается, о том, как работает сцепление, и не может «перескочить» со второй ступени на четвертую. В его распоряжении есть лишь ограниченный набор органов управления, с помощью которых он воздействует на автомобиль.
А вот у водителя, скажем, ВАЗ 2105 с механической коробкой передач больше свободы действий, но с ней приходит и большая ответственность. Если игнорировать внутреннее устройство автомобиля, то можно «подпалить сцепление» или вывести из строя выжимной подшипник.
Хотя некоторые водители предпочитают «механику», среднестатистическому городскому жителю, куда удобнее и безопаснее ездить на «автомате». Ведь с АКП даже начинающий водитель, скорее всего, не нанесет вред автомобилю неумелыми действиями.
Описанный выше принцип и называется инкапсуляцией. Процессы, происходящие под капотом автомобиля, скрыты от водителя, а управление осуществляется через удобный и безопасный «интерфейс» — руль и педали. Такой же принцип лежит в основе ООП.
Инкапсуляция (от лат. in capsule — в оболочке) — это заключение данных и функциональности в оболочку. В объектно-ориентированном программировании в роли оболочки выступают классы: они не только собирают переменные и методы в одном месте, но и защищают их от вмешательства извне (сокрытие).
Методы позволяют контролировать обращение к данными и предотвратить их удаление или некорректное изменение. Например, можно запретить присваивать полю «возраст» объекта «Пользователь» число большее 130. Другими словами, это такая «защита от дурака» в программировании.
Модификатор доступа public
Первый уровень, с которым сталкиваются все разработчики, — публичный. Чтобы сказать компилятору, что что-то должно быть доступно для всех, используется ключевое слово public.
Рассмотрим на примере класса Item:
public class Item
{
public string name;
public int cost;
public Item(string name, int cost)
{
this.name = name;
this.cost = cost;
}
}
Объявив экземпляр этого класса, можно обращаться к любым его полям в любом месте программы, где доступен сам объект (речь о локальных и глобальных переменных).
Item sword = new Item("Sword of Destiny", 500000);
Console.WriteLine($"You've got {sword.name}! It costs {sword.cost} rubles.");
Так как поля публичные, в консоли они отобразятся без каких-либо проблем:

Это удобно, потому что можно в любой момент выполнить любое действие над объектом и его данными. Но в этом и кроется проблема: объект становится беззащитен перед любым вмешательством. Например, можно просто взять и изменить его цену:
sword.cost = 50;
Console.WriteLine($"You've got {sword.name}! It costs {sword.cost} rubles.");
Из-за того, что поле публичное, оно изменится:

Это плохо по нескольким причинам:
- Если поле можно изменить в любом месте программы, сложно отслеживать ошибки, потому что не всегда понятно, где что-то пошло не так.
- Если вдруг в программу во время работы попадёт чужой код, он сможет изменять и читать любые поля любых объектов.
Разумеется, это не лучшее, что может случиться с приложением.
Модификатор доступа private
Чтобы поля были защищены от вмешательства, используется ключевое слово private — оно делает члены класса доступными только внутри самого класса.
public class Item
{
private string name;
private int cost;
public Item(string name, int cost)
{
this.name = name;
this.cost = cost;
}
}
Теперь эти поля нельзя будет изменить нигде, кроме как в методах этого класса. Но и получить их значение извне тоже не получится, а попытка вывести приведёт к ошибке:

Есть два способа сделать поле доступным только для чтения. Первый — использовать ключевое слово readonly, но оно запрещает менять значение вообще.
Второй способ заключается в том, чтобы передавать значения приватного члена класса через публичный. Например, с помощью методов:
public string GetName()
{
return this.name;
}
public int GetCost()
{
return this.cost;
}
К такой практике прибегают Java-разработчики, но в C# есть более элегантный способ — свойства.
public string Name
{ //Объявление свойства. Свойства принято называть так же, как и поля, но начинаются они с заглавной буквы
get //Конструкция get (также она называется геттером) позволяет определить логику получения значения
{
return this.name;
}
set //Конструкция set (сеттер) позволяет определить логику изменения значения
{
this.name = value; //value — это значение, которое передаётся свойству
}
}
Теперь, чтобы получить данные, нужно обратиться к свойству, а не к полю:
sword.Name = "Sword of Protection"; //Значения для свойства передаются так же, как и для обычной переменной
Console.WriteLine($"You've got {sword.Name}!");
Преимущество этого в том, что можно разрешить получать данные, но запретить их менять. То есть прописать только геттер:
public int Cost
{
get;
}
Обратите внимание, что можно просто написать set; или get; если не требуется дополнительная логика. Это сработает, если у поля и свойства одинаковые имена и если это примитивный тип (int, float, char, double и другие). Со ссылочными типами (объекты и строки) это не работает.
Также можно менять логику работы со значением:
public int Cost
{
get
{
return this.cost;
}
set
{
if(value > 0)
{
this.cost = value;
}
}
}
Здесь поле будет изменено только в том случае, если ему пытаются указать значение, которое выше нуля.
То есть если запустить вот такой код:
sword.Cost = 50;
sword.Cost = -1;
Console.WriteLine($"Sword's cost: {sword.Cost}!");
То выведено будет 50, а не -1:

Также можно создавать свойства без поля:
public bool IsExpensive
{
get
{
if(this.cost > 5000)
{
return true;
}
else
{
return false;
}
}
}
Это свойство вернёт true, если цена выше 5000, и false, если ниже.
Ключевое слово private можно также применять и к методам. Это делает их доступными только внутри класса.
private void Wipe()
{
this.name = "";
this.cost = 0;
}
Также приватным можно сделать сам класс, если он находится внутри другого класса:
public class Char
{
private Collider collider = new Collider();
private class Collider
{
public int x = 5;
public int y = 10;
public int width = 15;
public int height = 25;
}
}
Модификатор доступа internal
Иногда нужно сделать компонент доступным только внутри одного файла — например, в Program.cs, Item.cs или любом другом. Для этого используется ключевое слово internal.
class Program
{
static void Main(string[] args)
{
Backpack b = new Backpack();
Console.WriteLine($"{b.itemsCount} items in the backpack.");
}
}
internal class Backpack
{
public int itemsCount = 10;
}
Класс Backpack можно будет использовать только внутри файла Program.cs, и попытка объявить его внутри другого файла приведёт к ошибке.
Ключевое слово static
Статичность относится не совсем к уровням доступа, но тоже помогает заключить реализацию функционала в оболочку класса. Статичность позволяет обращаться к методам или полям, не создавая объект.
Например:
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"5 + 6 = {Calc.Sum(5, 6)}");
}
}
class Calc
{
public static int Sum(int a, int b) //Создание статичного метода
{
return a + b;
}
}
Метод Sum () используется в классе Program, хотя экземпляр класса Calc не создавался. При этом можно сделать статичным как отдельный метод или свойство, так и весь класс. В этом случае все поля и методы тоже должны быть статичными.
Это может быть нужно, чтобы создать набор инструментов, который будет использоваться в других частях программы. Хороший пример — класс Console, который тоже является статичным.
Другой пример — класс Math. Его можно использовать, чтобы выполнять различные математические операции (получение квадратного корня, модуляция, получение синуса, косинуса и так далее). У него много методов, а также он хранит различные константы вроде числа пи.
Подробно о том, что такое классы и объекты, читайте в первой статье цикла об объектно-ориентированном программировании.
Домашнее задание
Напишите класс GameObject, в котором будут храниться координаты объекта. Координаты должны быть доступны для чтения, а их изменение должно происходить в методе Move ().
Заключение
Есть и другие ключевые слова:
- abstract;
- protected;
- private protected;
- protected internal;
- sealed и другие.
Они будут рассмотрены в статье о наследовании.