16

使用不可变对象已经变得越来越普遍,即使手头的程序永远不会并行运行。然而我们仍然使用 getter,每个字段需要 3 行样板代码,每次访问需要 5 个额外字符(使用您最喜欢的主流 OO 语言)。尽管这看起来微不足道,而且许多编辑器无论如何都减轻了程序员的大部分负担,但这似乎仍然是不必要的努力。

继续使用访问器而不是直接访问不可变对象的原因是什么?具体来说,强制用户使用访问器(对于客户端或库编写器)有什么好处,如果是,它们是什么?


请注意,我指的是不可变对象,与这个问题不同,它通常指的是对象。需要明确的是,不可变对象没有设置器。

4

8 回答 8

5

我会说这实际上取决于语言。如果你不介意的话,我会稍微谈谈 C#,因为我认为它有助于回答这个问题。

我不确定你是否熟悉 C#,但它的设计、工具等非常直观且对程序员友好。
C#(也存在于 Python、D 等)的一个特性就是属性;属性基本上是一对方法(getter 和/或 setter),它们在外部看起来就像一个实例字段:您可以分配给它,并且可以像实例变量一样从中读取。
当然,在内部,它是一种方法,它可以做任何事情。

但是 C# 数据类型有时也有 GetXYZ() 和 SetXYZ() 方法,有时它们甚至直接公开它们的字段......这就引出了一个问题:你如何选择什么时候做?

Microsoft对 C# 属性以及何时使用 getter/setter有很好的指导:

属性应该表现得好像它们是字段一样;如果方法不能,则不应将其更改为属性。在以下情况下,方法优于属性:

  • 该方法执行耗时的操作。该方法明显比设置或获取字段值所需的时间慢。
  • 该方法执行转换。访问字段不会返回其存储的数据的转换版本。
  • Get方法具有可观察到的副作用。检索字段的值不会产生任何副作用。
  • 执行顺序很重要。设置字段的值不依赖于其他操作的发生。
  • 连续两次调用该方法会产生不同的结果。
  • 该方法是静态的,但返回一个可由调用者更改的对象。检索字段的值不允许调用者更改该字段存储的数据。
  • 该方法返回一个数组。

请注意,这些指南的全部目标是使所有属性在外部看起来像字段。

因此,使用属性而不是字段的唯一真正原因是:

  1. 你想要封装,yada yada。
  2. 您需要验证输入。
  3. 您需要从其他地方检索数据(或将数据发送到)。
  4. 您需要前向二进制 (ABI) 兼容性。我是什么意思?如果您在某个时候决定需要添加某种验证(例如),那么将字段更改为属性并重新编译您的库将破坏任何其他依赖它的二进制文件。但是,在源代码级别,什么都不会改变(除非您正在获取地址/引用,否则您可能不应该这样做)。

现在让我们回到 Java/C++ 和不可变数据类型。

其中哪一点适用于我们的场景?

  1. 有时它不适用,因为不可变数据结构的全部意义在于存储数据,而不是具有(多态)行为(例如,字符串数据类型)。
    如果您要隐藏数据并且什么都不做,那么存储数据有什么意义呢?
    但有时它确实适用(例如,假设您有一个不可变的树)——您可能不想公开元数据。
    但是在那种情况下,您显然会隐藏您不想公开的数据,而且您一开始就不会问这个问题!:)
  2. 不适用;没有需要验证的输入,因为没有任何变化。
  3. 不适用,否则不能使用字段!
  4. 可能适用,也可能不适用。

现在 Java 和 C++ 没有属性,但是方法取代了它们——所以上面的建议仍然适用,没有属性的语言的规则变成:

如果 (1) 您不需要 ABI 兼容性,并且(2) 您的 getter 的行为就像一个字段(即它满足上面 MSDN 文档中的要求),那么您应该使用字段而不是 getter。

要意识到的重要一点是,这些都不是哲学的。所有这些指南都是基于程序员的期望。显然,最终的目标是 (1) 完成工作,以及 (2) 保持代码的可读性/可维护性。已经发现上面的指南有助于实现这一点——你的目标应该是做任何适合你的事情来实现这一点。

于 2013-01-11T07:41:01.427 回答
4

封装有几个有用的目的,但最重要的一个是信息隐藏。通过将字段隐藏为实现细节,您可以保护对象的客户端不依赖于实际存在的字段。例如,您的对象的未来版本可能想要延迟计算或获取值,并且只有在您可以拦截读取字段的请求时才能做到这一点。

也就是说,没有理由让 getter 变得特别冗长。特别是在 Java 世界中,即使“get”前缀非常根深蒂固,您仍然会发现以值本身命名的 getter 方法(即方法foo()而不是getFoo()),这是节省一些人物。在许多其他 OO 语言中,您可以定义一个 getter 并仍然使用看起来像字段访问的语法,因此根本没有额外的冗长。

于 2013-01-11T05:54:11.160 回答
3

不可变对象应该使用直接字段访问来保持一致性,因为它允许设计对象完全按照客户期望的方式执行。

考虑一个系统,其中每个可变字段都隐藏在访问器后面,而每个不可变字段都没有。现在考虑以下代码片段:

class Node {
    private final List<Node> children;

    Node(List<Node> children) {
        this.children = new LinkedList<>(children);
    }

    public List<Node> getChildren() {
        return /* Something here */;
    }
}

在不知道 的确切实现的情况下Node,正如您在按合同设计时必须做的那样,在您看到的任何地方root.getChildren(),您只能假设正在发生以下三件事之一:

  • 没有。该字段children按原样返回,您不能修改列表,因为您会破坏 Node.js 的不变性。为了修改List你必须复制它,一个 O(n) 操作。
  • 它被复制,例如:return new LinkedList<>(children);. 这是一个 O(n) 操作。您可以修改此列表。
  • 返回一个不可修改的版本,例如:return new UnmodifiableList<>(children);. 这是一个 O(1) 操作。同样,为了修改List它,你必须复制它,一个 O(n) 操作。

在所有情况下,修改返回的列表都需要 O(n) 操作来复制它,而只读访问需要 O(1) 或 O(n) 之间的任何地方。这里要注意的重要一点是,按照合同设计,您无法知道库编写者选择了哪种实现,因此必须假设最坏的情况,O(n)。因此,O(n) 访问和 O(n) 创建您自己的可修改副本。

现在考虑以下几点:

class Node {
    public final UnmodifiableList<Node> children;

    Node(List<Node> children) {
        this.children = new UnmodifiableList<>(children);
    }
}

现在,无论您在哪里看到root.children,都存在一种可能性,即它是一个UnmodifiableList,因此您可以假设 O(1) 访问和 O(n) 用于创建本地可变副本。

显然,在后一种情况下,可以得出关于访问该字段的性能特征的结论,而在前一种情况下唯一可以得出的结论是,在最坏的情况下,也就是我们必须假设的情况下,性能远远不够比直接字段访问更糟糕。提醒一下,这意味着程序员必须在每次访问时考虑 O(n) 复杂度函数。


总而言之,在这种类型的系统中,无论何时看到一个 getter,客户端都会自动知道该 getter 对应于一个可变字段,或者该 getter 执行某种操作,无论是耗时 O(n) 的防御性复制操作、延迟初始化、转换或其他方式。每当客户看到直接字段访问时,他们立即知道访问该字段的性能特征。

通过遵循这种风格,程序员可以推断出更多关于他/她正在与之交互的对象提供的合约的信息。这种风格也促进了统一的不变性,因为只要您将上述代码片段更改UnmodifiableList为 interface List,直接字段访问就允许对象发生变异,从而迫使您的对象层次结构被仔细设计为从上到下不可变。

好消息是,您不仅获得了不变性的所有好处,还能够推断出无论在何处访问字段的性能特征,而无需查看实现并确信它永远不会改变。

于 2013-01-11T10:54:39.843 回答
2

Joshua Bloch,在Effective Java(第 2 版) “第 14 条:在公共类中,使用访问器方法,而不是公共字段”,对公开不可变字段有以下说法:

虽然公共类直接公开字段从来都不是一个好主意,但如果字段是不可变的,则危害较小。如果不更改其 API,则无法更改此类的表示形式,并且在读取字段时无法执行辅助操作,但可以强制执行不变量。

并将本章总结为:

总之,公共类不应该公开可变字段。公共类公开不可变字段的危害较小,尽管仍然值得怀疑。

于 2013-01-13T06:35:26.307 回答
1

您可以拥有公共的最终字段(以模仿某种不变性),但这并不意味着被引用的对象不能改变它们的状态。在某些情况下,我们仍然需要防御副本。

 public class Temp {
    public final List<Integer> list;

    public Temp() {
        this.list = new ArrayList<Integer>();
        this.list.add(42);
    }

   public static void foo() {
      Temp temp = new Temp();
      temp.list = null; // not valid
      temp.list.clear(); //perferctly fine, reference didn't change. 
    }
 }
于 2013-01-11T05:54:58.513 回答
1

继续使用访问器而不是直接访问不可变对象的原因是什么?具体来说,强制用户使用访问器(对于客户端或库编写器)有什么好处,如果是,它们是什么?

您听起来像一个程序程序员,在问为什么您不能直接访问字段,而必须创建访问器。主要问题是,即使你提出问题的方式也是错误的。这不是 OO 设计的工作方式——您通过它的方法设计对象行为并将其公开。然后,如有必要,您可以创建需要实现该行为的内部字段。所以这样说:“我正在创建这些字段,然后通过 getter 公开每个字段,这很冗长”是不正确的 OO 设计的明显标志。

于 2016-12-09T14:54:43.607 回答
0

封装字段然后仅通过 getters 方法将其公开是一种 OOP 实践。如果您直接公开字段,这意味着您必须将其公开。将字段公开并不是一个好主意,因为它会暴露对象的内部状态。

因此,公开您的字段/数据成员不是一个好习惯,它违反了 OOP 的封装原则。我还要说它不是特定于不可变对象的;对于非不可变对象也是如此。

编辑 正如@Thilo 指出的那样;另一个原因:也许您不需要公开字段的存储方式。

谢谢@Thilo。

于 2013-01-11T05:46:37.463 回答
0

在 Java 程序中继续生成(我希望现在没有人手工编写)getter 的一个非常实际的原因,即使是对于不可变的“值”对象,在我看来,这是不必要的开销:

许多库和工具依赖于旧的 JavaBeans 约定(或者至少是其中的 getter 和 setter 部分)。

这些使用反射或其他动态技术通过 getter 访问字段值的工具无法处理访问简单的公共字段。JSP 是我想到的一个例子。

现代 IDE 也使得一次为一个或多个字段生成 getter 以及在字段名称更改时更改 getter 名称变得微不足道。

所以我们只是继续为不可变对象编写 getter。

于 2016-12-09T14:40:56.867 回答