Урок №115. Инкапсуляция, Геттеры и Сеттеры
На предыдущем уроке мы узнали, что переменные-члены класса по умолчанию являются закрытыми. Новички, которые изучают объектно-ориентированное программирование, очень часто не понимают, почему всё обстоит именно так.
Зачем делать переменные-члены класса закрытыми?
В качестве ответа, воспользуемся аналогией. В современной жизни мы имеем доступ ко многим электронным устройствам. К телевизору есть пульт дистанционного управления, с помощью которого можно включать/выключать телевизор. Управление автомобилем позволяет в разы быстрее передвигаться между двумя точками. С помощью фотоаппарата можно делать снимки.
Все эти 3 вещи используют общий шаблон: они предоставляют вам простой интерфейс (кнопка, руль и т.д.) для выполнения определенного действия. Однако, то, как эти устройства фактически работают, скрыто от вас (как от пользователей). Для нажатия кнопки на пульте дистанционного управления вам не нужно знать, что выполняется «под капотом» пульта для взаимодействия с телевизором. Когда вы нажимаете на педаль газа в своем автомобиле, вам не нужно знать о том, как двигатель внутреннего сгорания приводит в движение колеса. Когда вы делаете снимок, вам не нужно знать, как датчики собирают свет в пиксельное изображение.
Такое разделение интерфейса и реализации чрезвычайно полезно, поскольку оно позволяет использовать объекты, без необходимости понимания их реализации. Это значительно снижает сложность использования этих устройств и значительно увеличивает их количество (устройства с которыми можно взаимодействовать).
По аналогичным причинам разделение реализации и интерфейса полезно и в программировании.
Инкапсуляция
В объектно-ориентированном программировании инкапсуляция (или «сокрытие информации») — это процесс скрытого хранения деталей реализации объекта. Пользователи обращаются к объекту через открытый интерфейс.
В языке C++ инкапсуляция реализована через спецификаторы доступа. Как правило, все переменные-члены класса являются закрытыми (скрывая детали реализации), а большинство методов являются открытыми (с открытым интерфейсом для пользователя). Хотя требование к пользователям использовать публичный интерфейс может показаться более обременительным, нежели просто открыть доступ к переменным-членам, но на самом деле это предоставляет большое количество полезных преимуществ, которые улучшают возможность повторного использования кода и его поддержку.
Преимущество №1: Инкапсулированные классы проще в использовании и уменьшают сложность ваших программ.
С полностью инкапсулированным классом вам нужно знать только то, какие методы являются доступными для использования, какие аргументы они принимают и какие значения возвращают. Не нужно знать, как класс реализован изнутри. Например, класс, содержащий список имен, может быть реализован с использованием динамического массива, строк C-style, std::array, std::vector, std::map, std::list или любой другой структуры данных. Для использования этого класса, вам не нужно знать детали его реализации. Это значительно снижает сложность ваших программ, а также уменьшает количество возможных ошибок. Это является ключевым преимуществом инкапсуляции.
Все классы Стандартной библиотеки C++ инкапсулированы. Представьте, насколько сложнее был бы процесс изучения языка C++, если бы вам нужно было знать реализацию std::string, std::vector или std::cout (и других объектов) для того, чтобы их использовать!
Преимущество №2: Инкапсулированные классы помогают защитить ваши данные и предотвращают их неправильное использование.
Глобальные переменные опасны, так как нет строгого контроля над тем, кто имеет к ним доступ и как их используют. Классы с открытыми членами имеют ту же проблему, только в меньших масштабах. Например, допустим, что нам нужно написать строковый класс. Мы могли бы начать со следующего:
getter
Синтаксис get связывает свойство объекта с функцией, которая будет вызываться при обращении к этому свойству.
Интерактивный пример
Синтаксис
Параметры
Имя свойства для привязывания к заданной функции.
Начиная с ECMAScript 6, вы также можете использовать выражения для вычисляемого имени свойства для привязки к заданной функции.
Описание
Иногда желательно разрешить доступ к свойству, которое возвращает динамически вычисляемое значение, или вы можете захотеть отражать состояние внутренней переменной без необходимости использования явных вызовов методов. В JavaScript, это можно реализовать при помощи использования геттера.
Невозможно сделать так, чтобы геттер был привязан к свойству и одновременно чтобы это свойство действительно содержало значение, хотя можно использовать геттер и сеттер в сочетании, чтобы создать тип псевдо-свойство.
Учтите следующее при работе с синтаксисом get :
- Он может иметь идентификатор, который является либо числом, либо строкой;
- Он должен иметь ровно 0 параметров (смотрите Incompatible ES5 change: literal getter and setter functions must now have exactly zero or one arguments для доп. информации);
- Он не должен появляться в объектном литерале вместе с другим get или через ввод данных для того же свойства ( < get x() < >, get x() < >> и < x: . get x() < >> запрещены).
Геттер можно удалить при помощи оператора delete .
Примеры
Определение геттера на новом объекте в момент инициализации этого объекта
Ниже создаётся псевдо-свойство latest для объекта obj , который выведет последний элемент массива в консоль лог.
Обратите внимание, что попытка присвоить значение latest не изменит его.
Удаление геттера оператором delete
Если вы хотите удалить геттер, используйте delete :
Определение геттера на уже существующих объектах с помощью defineProperty
Для добавления геттера к существующему объекту в любое время используйте Object.defineProperty().
Использование вычисляемого именованного свойства
Примечание: Вычисляемые свойства являются экспериментальной технологией, частью предложений спецификации ECMAScript 6, и массовой поддержки браузерами пока нет. Код ниже вызовет синтаксическую ошибку в неподдерживаемых средах.
Умные / самостоятельно перезаписывающиеся/ ленивые геттеры
Геттеры дают нам возможность определять свойство объекта , но они не вычисляют значение этого свойства до тех пор, пока оно не станет доступно. Геттер откладывает стоимость вычисления значения до тех пор, пока это значение не станет нужно, и если оно никогда не понадобится, то вы никогда не заплатите.
Дополнительная техника оптимизации заключается в том, чтобы лениться или откладывать вычисление значения свойства и кешировать его для дальнейшего доступа. Так поступают умные или запоминающие геттеры. Значение вычисляется в первый раз при вызове геттера и затем сохраняется в кеше так, что последующие обращения будут возвращать кешированные значения без его пересчёта. Это полезно в следующих ситуациях:
- Если вычисление значения свойства дорого (занимает много оперативной памяти или процессорного времени, порождает рабочий поток, получает удалённый файл, и т. д.).
- Если сейчас это значение не нужно. Оно будет использоваться позже, или в некоторых случаях оно не используется вообще.
- Если оно используется, к нему будут обращаться несколько раз, и нет необходимости его пересчитывать, так как значение не будет изменено, или не должно пересчитываться.
Значит, вам не нужно использовать ленивый геттер для свойства, значение которого вы собираетесь менять потому, что геттер не будет пересчитывать значение.
В следующем примере у объекта есть геттер как собственное свойство. При получении свойства, свойство удаляется из объекта и вновь добавляется, но в этот раз неявно, как свойство с данными. В итоге значение возвращается.
Какое значение у переменной геттер
Инициализатор, геттер и сеттер свойства необязательны. Указывать тип свойства также необязательно, если он может быть выведен их значения инициализатора или из возвращаемого значения геттера.
Геттеры и сеттеры необязательно определять именно для свойств внутри класса, они могут также применяться к переменным верхнего уровня.
Сеттер
Сеттер определяет логику установки значения переменной. Он определяется с помощью слова set . Например, у нас есть переменная age , которая хранит возраст пользователя и представляет числовое значение.
Но теоретически мы можем установить любой возраст: 2, 6, -200, 100500. И не все эти значения будут корректными. Например, у человека не может быть отрицательного возраста. И для проверки входных значений можно использовать сеттер:
Блок set определяется сразу после свойства, к которому оно относится — в данном случае после свойства age . При этом блок set фактически представляет собой функцию, которая принимает один параметр — value, через этот параметр передается устанавливаемое значение. Например, в выражении age = 45 число 45 и будет представлять тот объект, который будет храниться в value.
В блоке set проверяем, входит ли устанавливаемое значение в диапазон допустимых значений. Если входит, то есть если значение корректно, то передаем его объекту field . Если значение некорректно, то свойство просто сохраняет свое предыдущее значение.
Идентификатор field представляет автоматически генерируемое поле, которое непосредственно хранит значение свойства. То есть свойства фактически представляют надстройку над полями, но напрямую в классе мы не можем определять поля, мы можем работать только со свойствами. Стоит отметить, что к полю через идентификатор field можно обратиться только в геттере или в сеттере, и в каждом конкретном свойстве можно обращаться только к своему полю.
В функции main при втором обращении к сеттеру ( age = -345 ) можно заметить, что значение свойства age не изменилось. Так как новое значение -345 не входит в диапазон от 0 до 110.
геттер
Геттер управляет получением значения свойства и определяется с помощью ключевого слова get :
Справа от выражения get() через знак равно указывается возвращаемое значение. В данном случае возвращается значения поля field , которое хранит значение свойства name. Хотя в таком геттер большого смысла нет, поскольку получить подобное значение мы можем и без геттера.
Если геттер должен содержать больше инструкций, то геттер можно оформить в блок с кодом внутри фигурных скобок:
Если геттер оформлен в блок кода, то для возвращения значения необходимо использовать оператор return . И, таким образом, каждый раз, когда мы будем получать значение переменной age (например, в случае с вызовом println(age) ), будет срабатывать геттер, когда возвращает значение. Например:
Консольный вывод программы:
Использование геттеров и сеттеров в классах
Хотя геттеры и сеттеры могут использоваться к глобальным переменным, как правило, они применяются для опосредования доступа к свойствам класса.
При втором обращении к сеттеру ( bob.age = -8 ) можно заметить, что значение свойства age не изменилось. Так как новое значение -8 не входит в диапазон от 0 до 110.
Вычисляемый геттер
Геттер может возвращать вычисляемые значения, которые могут задействовать несколько свойств:
Здесь свойство fullname определяет блок get, который возвращает полное имя пользователя, созданное на основе его свойств firstname и lastname. При этом значение самого свойства fullname напрямую мы изменить не можем — оно определено доступно только для чтения. Однако если изменятся значения составляющих его свойств — firstname и lastname, то также изменится значение, возвращаемое из fullname.
Использование полей для хранения значений
Выше уже рассматривалось, что с помощью специального поля field в сеттере и геттере можно обращаться к непосредственному значению свойства, которое хранится в специальном поле. Однако мы сами можем явным образом определить подобное поле. Нередко это приватное поле:
Можно использовать одновременно и геттер, и сеттер:
Здесь для свойства age добавлены геттер и сеттер, которые фактически являются надстройкой над полей _age , которое собственно хранит значение.
Пара мыслей о геттерах и сеттерах в C++

Эта статья посвящена геттерам и сеттерам в C++. Приношу свои извинения, но речь пойдет не о корутинах. К слову, в ближайшее время появится вторая часть про пулы потоков.
TL;DR: геттеры и сеттеры не очень хорошо подходят для структуроподобных объектов.
Введение
В этой статье я лишь высказываю свое личное мнение, я не преследую цели кого-нибудь обидеть или задеть, я просто собираюсь объяснить, почему и когда стоит или не стоит, использовать геттеры и сеттеры. Буду очень рад любым дискуссиям в комментариях.
Следует сразу прояснить, что когда я говорю о геттере, я подразумеваю функцию, которая просто что-то возвращает, а когда я говорю о сеттере, я подразумеваю функцию, которая просто изменяет одно внутреннее значение, не выполняя никаких проверок или других дополнительных вычислений.
Производительность и геттеры
Допустим, у нас есть простая структура с обычными геттерами и сеттерами:
Сравним эту версию с версией без геттеров и сеттеров.
Она намного лаконичнее и надежнее. Здесь мы не можем, например, верну фамилию вместо имени.
Оба кода полностью функциональны. У нас есть класс Person с именем ( firstName ), фамилией ( lastName ) и возрастом ( age ). Однако предположим, что нам нужна функция, которая возвращает некоторую сводку по конкретному человеку.

Версия без геттеров выполняет эту задачу на 30% быстрее, чем версия с геттерами. Почему? Из-за возврата по значению в геттере. При возврате по значению создается копия, что снижает производительность. Давайте сравним производительность person.getFirstName() ; и person.firstName .

Как видите, прямой доступ к полю имени без геттера эквивалентен noop.
Геттер по константной ссылке
Однако можно использовать возврат не по значению, а по ссылке. Таким образом мы получим такую же производительность, как и без использования геттеров. Обновленный код будет выглядеть так:
Так как мы получаем ту же производительность, что и в лаконичной версии, мы можем на этом успокоиться, не так ли? Прежде чем отвечать на этот вопрос, попробуйте выполнить этот код.
Вы можете заметить некоторые странные символы, выведенные в консоли. Но почему? Что произошло, когда мы сделали make().getLastName() ?
Вы создаете экземпляр Person.
Вы получаете ссылку на фамилию.
Вы удаляете экземпляр Person.
И вот у нас есть висячая ссылка! Это может привести к крашам (в лучшем случае) или чему-то еще более худшему, чему-то, что можно найти только в фильмах ужасов.
Чтобы предупредить это, мы должны ввести ref-qualified функции.
Вот новое решение, которое будет работать везде. Вам нужно два геттера. Один для lvalue и один для rvalue (как xvalue , так и для prvalue ).
Проблемы с сеттерами
Тут особо нечего сказать. Если вы хотите добиться максимальной производительности, вы должны написать один сеттер, который принимает lvalue , и один, который принимает rvalue . Однако, как правило, достаточно иметь всего один сеттер, который принимает перемещаемое значение. Тем не менее, вам придется расплатиться за это дополнительным move. Однако таким образом у вас не получится производить небольшие изменения в переменных. Вы должны заменять всю переменную целиком. Если вы просто хотите заменить одну букву A в имени на D, то вы не сможете сделать это с помощью сеттеров. Однако с помощью прямого доступа так делать можно.
А как насчет иммутабельных переменных?
Кто-то может посоветовать вам просто сделать атрибут члена const. Однако меня это решение не устраивает. Создание константы предотвратит move-семантику и приведет к ненужному копированию.
У меня нет волшебного решения, которое я мог бы предложить вам прямо сейчас. Тем не менее, мы можем написать обертку, которую мы можем назвать immutable <T>. Эта обертка должна быть:
Так как она immutable , она не должна быть assignable
Она может быть copy constructible или move constructible
Она должна быть конвертируемой в const T &, будучи lvalue
Она должна быть конвертируемой в T , будучи rvalue
Она должна использоваться, как и другие оболочки, с помощью оператора * или оператора -> .
Получить адрес базового объекта должно быть легко.
Вот небольшая реализация:
Таким образом, для иммутабельного объекта Person вы можете просто написать:
Заключение
Я бы не сказал, что геттеры и сеттеры — это зло. Однако, когда вам не нужно делать что-либо еще в геттере и сеттере, достижение максимальной производительности, безопасности и гибкости подводит вас к написанию:
3-х геттеров (или даже 4-х): const lvalue , rvalue , const rvalue и, по вашему усмотрению, для неконстантного lvalue (даже если это уже просто очень странно звучит, так как проще использовать прямой доступ)
1 сеттер (или 2, если вы хотите выжать максимальную производительность).
Это по большому счету шаблон, который подходит практически для всего.
Некоторые люди могут вам сказать, что геттеры и сеттеры обеспечивают инкапсуляцию, но это не так. Инкапсуляция — это не просто делать атрибуты приватными. Речь идет о сокрытии внутренностей от пользователей, а в структуроподобных объектах вы редко хотите что-либо скрывать.
Мой совет: когда у перед вами структуроподобный объект, просто не используйте геттеры и сеттеры, а используйте публичный/прямой доступ. Проще говоря, если вам не нужен сеттер для поддержания инвариантности, вам не нужен приватный атрибут.
PS: Для людей, которые используют библиотеки с поверхностным копированием, влияние на производительность менее важно. Однако вам все равно нужно написать 2 функции вместо 0. Не забывайте, что чем меньше кода вы напишете, тем меньше будет ошибок, проще поддерживать и легче читать этот самый код.
Ну а что думаете вы? Используете ли вы геттеры и сеттеры? И почему?
Перевод материала подготовлен в рамках курса «C++ Developer. Basic». Всех желающих приглашаем на двухдневный онлайн-интенсив «HTTPS и треды в С++. От простого к прекрасному». В первый день интенсива мы настроим свой http-сервер и разберем его что называется «от и до». Во второй день произведем все необходимые замеры и сделаем наш сервер супер быстрым, что поможет нам понять на примере, чем же все-таки язык С++ лучше других. Регистрация здесь