Стиль кодирования геттеров/сеттеров C++
-
09-09-2019 - |
Вопрос
Я некоторое время программировал на C# и теперь хочу освежить свои навыки C++.
Имея класс:
class Foo
{
const std::string& name_;
...
};
Каков был бы лучший подход (я хочу разрешить доступ только для чтения к полю name_):
- используйте метод получения:
inline const std::string& name() const { return name_; }
- сделайте поле общедоступным, поскольку оно является константой
Спасибо.
Решение
Делать неконстантные поля общедоступными — плохая идея, поскольку в этом случае становится сложно принудительно ввести ограничения проверки ошибок и/или добавить побочные эффекты для изменения значений в будущем.
В вашем случае у вас есть константное поле, поэтому вышеуказанные проблемы не являются проблемой.Основным недостатком публичного поля является то, что вы блокируете базовую реализацию.Например, если в будущем вы захотите изменить внутреннее представление на строку C, строку Unicode или что-то еще, вы сломаете весь клиентский код.С помощью геттера вы можете перейти к устаревшему представлению для существующих клиентов, одновременно предоставляя новые функции новым пользователям с помощью нового геттера.
Я бы все равно предложил использовать метод получения, подобный тому, который вы разместили выше.Это максимизирует вашу будущую гибкость.
Другие советы
Использование метода получения — лучший выбор для долгоживущего класса, поскольку оно позволяет в будущем заменить метод получения чем-то более сложным.Хотя для константного значения это вряд ли понадобится, затраты невелики, а возможные выгоды велики.
Кроме того, в C++ особенно хорошая идея предоставить как метод получения, так и метод установки для члена. то же имя, поскольку в будущем вы действительно сможете изменить пару методов:
class Foo {
public:
std::string const& name() const; // Getter
void name(std::string const& newName); // Setter
...
};
В одну общедоступную переменную-член, определяющую operator()()
для каждого:
// This class encapsulates a fancier type of name
class fancy_name {
public:
// Getter
std::string const& operator()() const {
return _compute_fancy_name(); // Does some internal work
}
// Setter
void operator()(std::string const& newName) {
_set_fancy_name(newName); // Does some internal work
}
...
};
class Foo {
public:
fancy_name name;
...
};
Клиентский код, конечно, потребуется перекомпилировать, но никаких изменений синтаксиса не требуется!Очевидно, что это преобразование работает так же хорошо для константных значений, для которых нужен только геттер.
Кстати, в C++ несколько странно иметь константный ссылочный элемент.Вы должны назначить его в списке конструкторов.Кому на самом деле принадлежит память этого объекта и какова ее продолжительность жизни?
Что касается стиля, я согласен с остальными, что не стоит раскрывать свои интимные места.:-) Мне нравится этот шаблон для сеттеров/геттеров.
class Foo
{
public:
const string& FirstName() const;
Foo& FirstName(const string& newFirstName);
const string& LastName() const;
Foo& LastName(const string& newLastName);
const string& Title() const;
Foo& Title(const string& newTitle);
};
Таким образом вы можете сделать что-то вроде:
Foo f;
f.FirstName("Jim").LastName("Bob").Title("Programmer");
Я думаю, что сейчас подход C++11 будет больше похож на этот.
#include <string>
#include <iostream>
#include <functional>
template<typename T>
class LambdaSetter {
public:
LambdaSetter() :
getter([&]() -> T { return m_value; }),
setter([&](T value) { m_value = value; }),
m_value()
{}
T operator()() { return getter(); }
void operator()(T value) { setter(value); }
LambdaSetter operator=(T rhs)
{
setter(rhs);
return *this;
}
T operator=(LambdaSetter rhs)
{
return rhs.getter();
}
operator T()
{
return getter();
}
void SetGetter(std::function<T()> func) { getter = func; }
void SetSetter(std::function<void(T)> func) { setter = func; }
T& GetRawData() { return m_value; }
private:
T m_value;
std::function<const T()> getter;
std::function<void(T)> setter;
template <typename TT>
friend std::ostream & operator<<(std::ostream &os, const LambdaSetter<TT>& p);
template <typename TT>
friend std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p);
};
template <typename T>
std::ostream & operator<<(std::ostream &os, const LambdaSetter<T>& p)
{
os << p.getter();
return os;
}
template <typename TT>
std::istream & operator>>(std::istream &is, const LambdaSetter<TT>& p)
{
TT value;
is >> value;
p.setter(value);
return is;
}
class foo {
public:
foo()
{
myString.SetGetter([&]() -> std::string {
myString.GetRawData() = "Hello";
return myString.GetRawData();
});
myString2.SetSetter([&](std::string value) -> void {
myString2.GetRawData() = (value + "!");
});
}
LambdaSetter<std::string> myString;
LambdaSetter<std::string> myString2;
};
int _tmain(int argc, _TCHAR* argv[])
{
foo f;
std::string hi = f.myString;
f.myString2 = "world";
std::cout << hi << " " << f.myString2 << std::endl;
std::cin >> f.myString2;
std::cout << hi << " " << f.myString2 << std::endl;
return 0;
}
Я тестировал это в Visual Studio 2013.К сожалению, чтобы использовать базовое хранилище внутри LambdaSetter, мне нужно было предоставить общедоступный метод доступа GetRawData, который может привести к нарушению инкапсуляции, но вы можете либо оставить его и предоставить свой собственный контейнер хранения для T, либо просто убедиться, что единственный раз вы используете «GetRawData», когда вы пишете собственный метод получения/установки.
Несмотря на то, что имя является неизменяемым, вы все равно можете захотеть иметь возможность вычислить его, а не сохранять в поле.(Я понимаю, что это маловероятно для «имени», но давайте ориентироваться на общий случай.) По этой причине даже константные поля лучше всего заключать в геттеры:
class Foo {
public:
const std::string& getName() const {return name_;}
private:
const std::string& name_;
};
Обратите внимание, что если бы вы изменили getName()
чтобы вернуть вычисленное значение, он не мог вернуть const ref.Это нормально, потому что это не потребует никаких изменений в вызывающих программах (перекомпиляция по модулю).
Избегайте общедоступных переменных, за исключением классов, которые по сути являются структурами в стиле C.Это просто не лучшая практика.
После того как вы определили интерфейс класса, вы, возможно, никогда не сможете изменить его (кроме дополнений), потому что люди будут опираться на него и полагаться на него.Публикация переменной означает, что вам необходимо иметь эту переменную и убедиться, что она содержит то, что нужно пользователю.
Теперь, если вы используете геттер, вы обещаете предоставить некоторую информацию, которая в настоящее время хранится в этой переменной.Если ситуация изменится и вы не хотите постоянно поддерживать эту переменную, вы можете изменить доступ.Если требования меняются (а я видел некоторые довольно странные изменения требований), и вам в основном нужно имя, которое находится в этой переменной, но иногда и имя в этой переменной, вы можете просто изменить метод получения.Если бы вы сделали переменную общедоступной, вы бы застряли с ней.
Это происходит не всегда, но мне гораздо проще просто написать быстрый метод получения, чем анализировать ситуацию, чтобы понять, не пожалею ли я о том, что сделал переменную общедоступной (и рискую ошибиться позже).
Делать переменные-члены приватными — хорошая привычка.Любой магазин, в котором есть стандарты кода, вероятно, запретит публиковать случайные переменные-члены, и любой магазин, где есть обзоры кода, вероятно, будет критиковать вас за это.
Всякий раз, когда удобство письма действительно не имеет значения, выработайте более безопасную привычку.
Собрал идеи из нескольких источников C++ и поместил их в красивый, но довольно простой пример геттеров/сеттеров в C++:
class Canvas { public:
void resize() {
cout << "resize to " << width << " " << height << endl;
}
Canvas(int w, int h) : width(*this), height(*this) {
cout << "new canvas " << w << " " << h << endl;
width.value = w;
height.value = h;
}
class Width { public:
Canvas& canvas;
int value;
Width(Canvas& canvas): canvas(canvas) {}
int & operator = (const int &i) {
value = i;
canvas.resize();
return value;
}
operator int () const {
return value;
}
} width;
class Height { public:
Canvas& canvas;
int value;
Height(Canvas& canvas): canvas(canvas) {}
int & operator = (const int &i) {
value = i;
canvas.resize();
return value;
}
operator int () const {
return value;
}
} height;
};
int main() {
Canvas canvas(256, 256);
canvas.width = 128;
canvas.height = 64;
}
Выход:
new canvas 256 256
resize to 128 256
resize to 128 64
Вы можете протестировать его онлайн здесь: http://codepad.org/zosxqjTX
ПС:ФО Иветта <3
Из теории шаблонов проектирования;«инкапсулировать то, что меняется».Определение «геттера» обеспечивает хорошее соблюдение вышеуказанного принципа.Таким образом, если представление реализации члена изменится в будущем, член можно будет «массировать» перед возвратом из «получателя»;подразумевая отсутствие рефакторинга кода на стороне клиента, где выполняется вызов метода получения.
С уважением,