C++ Getter/Setter(替代方案?)

9

好的,几乎在我所阅读的每个地方都提到了 getters/setters 是“邪恶”的。作为一个经常在 PHP / C# 中使用 getters/setters 的程序员,我不明白它们为什么会被认为是“邪恶”的。我已经读过它们破坏封装等等的问题,但是这里有个简单的例子。

class Armor{

  int armorValue;
public:
  Armor();
  Armor(int); //int here represents armor value
  int GetArmorValue();
  void SetArmorValue(int);
};

现在,假设获取器和设置器是“邪恶的”。 那么在初始化后如何更改成员变量呢?
例如:
Armor arm=Armor(128); //armor with 128 armor value

//for some reason I would like to change this armor value
arm.SetArmorValue(55); //if i do not use getters / setters how is this possible?

假设以上内容不可行,出于某种原因。 如果我的游戏将防具值限制在1到500之间。(没有任何一件防具的防御值可以高于500或低于1)。
现在我的实现变成了:
void Armor::SetArmor(int tArmValue){
      if (tArmValue>=1 && tArmValue<=500)
         armorValue=tArmValue;
      else
         armorValue=1;
}

那么,如果不使用getter/setter,我还能怎样施加这种限制呢? 如果不使用getter/setter,我还能怎样修改属性呢? 在情况1中,armorValue是否只应该是公共成员变量,在情况2中使用getter/setter?

好奇。谢谢各位。


1
如果你相信不少程序员的话,很多东西都是邪恶的。大多数时候,“邪恶”可以替换为“适度使用有益”。当然,你不一定非得同意他们的看法 :) - Skurmedel
8个回答

17
您误解了一些内容。 不使用getter/setter破坏了封装性并公开了实现细节,可以根据某种定义将其视为“邪恶”的行为。
我想它们可以被认为是“邪恶的”,因为在没有适当的IDE/编辑器支持的情况下,它们有些乏味,很难用C++编写...
C++的一个陷阱是创建非常量引用getter,这也允许修改。这与返回指向内部数据的指针相同,并将锁定内部实现的该部分,并且与将字段公开没有什么不同
编辑:根据评论和其他答案,您听到的可能是始终为每个字段创建非私有getter和setter。但是,我也不会称其为邪恶,只是愚蠢而已;-)

1
没错:getter和setter比直接访问公共方法要“不那么”邪恶,因为它们定义了一个接口,使得你的类可以在不破坏其依赖关系的情况下进行更改。只是不要使用它们来允许外部世界访问真正不应该被访问的成员,这样你就做到了。 - Matt
在我的例子中,armorValue是可以被外部访问的,对吧?因为我需要它被修改/读取?所以整个getter/setter都是邪恶的说法是指编写用于修改真正私有变量的getter/setter?但在这种情况下,似乎是SETTER是邪恶的。我的意思是,即使一些私有变量也需要只读访问。这正确吗? - Scott Moniz
1
根据下面的答案,我现在明白了。如果一个变量是私有的,只有当它需要从外部访问时才应编写getter/setter。如果一个变量是私有的且仅在内部使用,则不应编写getter/setter,因为通过使用类 - 基于您的getter/setter - 人们可以推断出一些关于类的内部信息。 - Scott Moniz
2
当然,从某种意义上说,拥有没有getter/setter的私有字段是“最好的”。而且很多时候只需要getter,不需要setter。要问的问题是,如果您更改了内部实现,并且该字段消失了,那么getter或setter是否仍然有意义?如果没有,那么它就不应该成为公共API的一部分。此外,根据getter的功能而不是初始实现中获取的字段来命名getter。 - hyde
非常抱歉我对C++非常陌生,但是如果需要更改私有变量,使用setter是否会邪恶?例如,如果我有一个名为Person的类,并且该类中有变量int hp;,那么如何在不使用某种公共setter函数的情况下更改Person对象的hp? - JaredCubilla
1
@JaredCubilla 我认为“邪恶”的想法是setter修改私有变量。Setter修改属性。通常,修改该属性与修改保存属性值的私有变量完全相同,但理想情况下,开发人员应该将这两个事物分开考虑。有公共接口(属性的setter),还有隐藏的实现(可能直接设置私有成员变量,但这是实现细节)。像这样的setter没有任何问题。 - hyde

12

有点不同寻常的观点:是的,getter和setter(也称为访问器和修改器)大多数情况下是有害的。

这里的问题并不在于“破坏封装”,而在于仅仅将一个变量定义为一种类型(例如int),而实际上它根本不是那种类型。看看你的例子,你把Armor称为一个int,但它实际上不是。虽然它无疑是一个整数,但它肯定不是intint(除其他事项外)定义了一个范围。虽然你的类型是一个整数,但它从来没有打算支持与int相同的范围。如果你想让Armor成为1到500的整数类型,请直接定义一个表示该类型的类型,并将Armor定义为该类型的一个实例。在这种情况下,由于你想要强制执行的不变式是作为类型本身的一部分定义的,所以你不需要添加一个setter来尝试强制执行它。

template <class T, class less=std::less<T> >
class bounded {
    const T lower_, upper_;
    T val_;

    bool check(T const &value) {
        return less()(value, lower_) || less()(upper_, value);
    }

    void assign(T const &value) {
        if (check(value))
            throw std::domain_error("Out of Range");
        val_ = value;
    }

public:
    bounded(T const &lower, T const &upper) 
        : lower_(lower), upper_(upper) {}

    bounded(bounded const &init) 
        : lower_(init.lower), upper_(init.upper), val_(init.val_)
    { }

    bounded &operator=(T const &v) { assign(v);  return *this; }

    operator T() const { return val_; }

    friend std::istream &operator>>(std::istream &is, bounded &b) {
        T temp;
        is >> temp;

        if (b.check(temp))
            is.setstate(std::ios::failbit);
        else
            b.val_ = temp;
        return is;
    }
};

有了这个功能,定义一些范围在1..500之间的装甲变得非常简单:

bounded<int> armor(1, 500);

根据情况,您可能更喜欢定义(例如)一个saturating类型,在这种情况下,尝试分配超出范围的值是可以的,但实际分配的值将仅为在范围内最接近的值。

saturating<int> armor(1, 500);

armor = 1000;

std::cout << armor;  // prints "500"

当然,上面的内容还有点简略。对于你的装甲类型,支持使用-=(可能还有+=)会更方便,这样攻击就会变成类似于x.armor -= 10;
总之,getter和setter的一个(或至少是“一个”)主要问题在于,它们通常指向你定义一个变量为某一类型,而实际上你想要的是其他某种类型,只是这种类型在某些方面有点相似而已。
现在,确实有些语言(例如Java)没有为程序员提供编写此类代码所需的工具。我相信你使用C++标签表示你真的想编写C++代码。C++确实为你提供了必要的工具,并且(至少在我看来),如果你充分利用它提供的工具,你的代码将更好地满足所需的语义约束,同时仍使用干净、自然、易读的语法。

明白了。创建一个 Bounded 类型似乎有些过度了。不过,我理解你的想法。我猜这取决于你想深入多少,也就是说,什么时候会停止呢?无论如何,谢谢! - Scott Moniz
1
如果每次使用都必须重新创建它,我会同意@ScottMoniz的观点。当只需要加入“#include”语句并使用它时,没有理由不使用它。 - Jerry Coffin

5
简而言之:它们并不邪恶。
只要它们不泄漏内部表示,它们就没有问题。我在这里看不到任何问题。

啊,好的,你的意思是如果你为每个私有变量编写getter/setter(即使有些确实需要在类外部访问),那么你基本上正在泄漏你的内部表示...这很有道理。看来有选择地使用getter/setter确实不是邪恶的。 - Scott Moniz
另一个例子是泄露成员变量的引用。 - Karoly Horvath

4
get/set 函数常受到批评,因为客户端代码可能会滥用它们来执行应该封装在类中的操作。例如,假设客户端想要“抛光”他们的盔甲,并决定将“价值”增加20,于是他们进行了get和set操作并感到很高兴。然后,其他客户端代码的某个地方决定生锈的盔甲应该减少30的价值,他们也这样做了。与此同时,客户端代码中的其他十几个地方也允许对盔甲进行打磨和生锈效果——以及实现直接的“加固”和“破裂”。这没有集中控制...盔甲类的维护者无法执行以下操作:
  • 使锈蚀、抛光、加固和破裂效果每件盔甲最多只应用一次

  • 针对特定逻辑效果调整添加或减去的数字

  • 决定新的“皮革”盔甲类型不能生锈,并忽略客户端试图使其生锈的尝试

另一方面,如果第一个想让盔甲生锈的客户端无法通过接口这样做,他们会去找盔甲类的维护者,并说“嘿,给我一个函数来做这个”,然后其他人就可以开始使用逻辑级别的“生锈”操作了。如果以后有必要执行我上面描述的那些操作,它们可以很容易地在盔甲类中集中实现(例如,通过有一个单独的布尔变量来表示盔甲是否生锈,或者一个单独的变量记录生锈效果)。
因此,get/set函数的问题在于它们阻碍了逻辑功能API的自然演化,而是将逻辑分散到客户端代码中,最终导致难以维护的混乱。

1
你的getter/setter看起来不错。
getter/setter的替代方案是将成员变量公开。更准确地说,将变量分组为没有成员函数的结构。并在类内操作这个结构。

1

给成员访问权限会降低封装性,但有时是必要的。而最好的方法是通过getter和setter来实现。有些人即使不需要这样的访问权限也会实现它们,只是因为他们可以并且这是一种习惯。


1

当以下情况发生时,Getter 方法是有害的:

  1. 它们直接访问类的数据成员
  2. 每次向类中添加数据时都需要添加新的 Getter 方法
  3. 每个 Getter 方法的数据行为都不同

因此,好的 Getter 方法应该执行以下操作:

  1. 它们将请求转发到其他对象或从多个位置收集数据
  2. 您可以使用一个 Getter 方法获取大量数据
  3. 所有获取的数据都以相同的方式处理

另一方面,Setter 方法总是有害的。


0

如果不使用getter/setter,我还能以什么方式实施此限制?如果不使用getter/setter,我还能如何修改属性?

您可以检查从变量中读取的内容,如果其值超出范围,则使用预定义的值(如果可能)。

您还可以采用一些肮脏的技巧,例如保护变量下面的内存不被写入,捕获写入尝试并禁止/忽略具有无效值的尝试。这将是繁琐的实现和昂贵的执行。虽然这可能对调试有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接