为什么继续使用具有不可变对象的 getter?

2022-09-03 07:01:00

使用不可变对象变得越来越普遍,即使手头的程序从来就不应该并行运行。然而,我们仍然使用 getters,它要求每个字段使用 3 行样板,每次访问需要 5 个额外的字符(以您最喜欢的主流 OO 语言)。虽然这似乎微不足道,而且许多编辑无论如何都消除了程序员的大部分负担,但这似乎仍然是不必要的努力。

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


请注意,我指的是不可变对象,这与这个问题不同,它指的是一般的对象。需要明确的是,在不可变对象上没有 setter。


答案 1

我会说这实际上是依赖于语言的。如果你原谅我,我会稍微谈谈C#,因为我认为它将有助于回答这个问题。

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

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

Microsoft 对 C# 属性以及何时使用 getters/setters 有很好的指导原则

属性的行为应像字段一样;如果方法不能,则不应将其更改为属性。在以下情况下,方法优于属性:

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

请注意,这些准则的整个目标是使所有属性在外部看起来像字段。

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

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

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

其中哪一点适用于我们的方案?

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

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

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

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


答案 2

封装有几个有用的目的,但最重要的一个是信息隐藏。通过将字段隐藏为实现详细信息,可以保护对象的客户端免受依赖于那里实际存在的字段的影响。例如,对象的未来版本可能希望延迟计算或提取值,而这只有在您可以截获读取字段的请求时才能完成。

也就是说,没有理由让 getters 特别冗长。特别是在Java世界中,即使“get”前缀非常根深蒂固,您仍然可以找到以值本身命名的getter方法(即,代替方法),这是保存几个字符的好方法。在许多其他 OO 语言中,您可以定义一个 getter,并且仍然使用看起来像字段访问的语法,因此根本没有额外的冗长。foo()getFoo()