IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

关于 getter 和 setter

云风的 BLOG 2009-11-03 15:05:01 累计浏览 1,755 次
本机暂存

网友 "sjinny" 在上篇评论里写:

云风对那种所有成员数据都写setter/getter的做法有什么看法吗……这两天试图精简三个太庞大的类,但是单单setter/getter就让接口数目变得非常多了……

我谈谈我的看法吧。

首先,几乎任何设计问题都没有标准答案。如果有,就不需要人来做这件事了。无论多复杂的事情,只要你能定义出精确的解决方案,总可以用机器帮你实现。

下面谈谈我的大体设计原则。记住、一切皆有例外,但这里少谈例外。因为这涉及更复杂的衡量标准。

KISS 当然是首要原则。但有许多诠释角度,每个设计师眼中都有自己的 KISS 原则。

今天的我认为,我们应该尽量少提供新的概念。所以,如果你用 C 就尽量不要用函数指针数组去模拟虚表;如果你用 C++ 就别想着用模板之类的东西弄出个“属性”的概念出来…… 这些语言原本不提供的东西,对于用户(可能是你的队友、可能是今后的你自己,可能是你未来的继任者)就是新的东西。

大部分情况下,设计一个所谓框架,也是新东西。限制用户以一定的规范来编写程序,最合适的是在语言级、而且是大家都熟知的并成熟的语言特性。

我们应该坚信:简洁优良的设计一定是和语言工具无关的。优雅的接口设计,总可以以简单的方式表达出来。

第一件事情,就是寻找你选择的开发语言的惯例。因为,如果一个语言足够成熟,抽象化的需求一定有无数人遇到过,好的方案会经过时间的洗练留下来;不用我们重新发明。setter/getter 这种需求莫如是。

最近几年,我用的比较多的是 C 语言。C 语言的惯例是什么?C 语言因为 Unix 而生,并是 Unix 的原生开发语言。我们从 Unix 的接口中寻找答案。

举个大家都熟悉的例子:getsockopt / setsockopt 。几乎是一样的需求:向一个对象读取或设置某一属性值。

传统上,C 语言构建造的系统中较少为每个属性值分别留下两个接口(读/写)。对一个对象的内部状态的修改,一般会用统一的一对 API 去操控。

少即是多。


第二要点是效率。

不考虑效率的程序员不是好程序员。这是我的个人观点。可能有些老程序员不会同意,他们会苦口婆心的教导新人:性能并不总是那么重要,为了性能,你会失去很多东西,当你剩下了“性能”后,最后,还是会失去它。

在我学会编程的头十年里,我疯狂的追求速度。读了大量的书、写了大量的代码。小心翼翼的优化每处我觉得值得优化的部分,重写再重写。

慢慢的,我学会接受一些东西:

比如相信编译器。

比如别耍小聪明。

比如不要牺牲代码清晰性。

比如防御式编程。

比如先把代码做的可靠。

比如采用时间复杂度更高,但简洁的算法。

……

对于一个性能偏执狂来说,这些浅显的道理接受起来是多么的不容易。

我这里要写的,并不是重复证明这些道理多么的有价值;而是想反过来说,每次采用和性能相违背的方案时,我的内心都会抗拒和怀疑。我依旧认为应该考虑例外情况,从而破坏这些规则。如何判定什么时候该遵循、什么时候该违背。以我目前的水平,无法精确总结。只能靠大量的实践磨练出得感觉了。

同上,如果有精确的准则,我们应该让机器去选择,而不是人。

我敢肯定,无条件相信教条的程序员,不会成长。

相信 C++ 可以取得比 C 更高性能的程序员认为:C++ 语言设施会带来更高的效率。他们最喜欢举的例子是 algorithm::sort() 和 qsort() 的比较。

模板会内嵌比较函数,去掉函数调用之消耗。从而在性能测试中完全击败 qsort() (后者需要为每次比较做一次 C 函数调用)

前两周在有道难题 的决赛颁奖仪式后,我和参赛同学的交流中,我谈到了这个问题。当时,我先讲了另一段:如果你的程序要处理一组数据,是从前往后处理性能高、还是从后往前、还是间隔着处理…… 这其实取决于很多和你的算法关系不大的东西:比如内存控制器的工作方式、CPU Cache 的管理、OS 的虚拟内存调度,等等。

有时候,我们需要关心这些、有时候我们不关心这些。如果想把整个系统做的高效,何时关系,何时不关心,这个决策比如何优化更难,更要功力。对于性能偏执狂来说,影响他决策的才不是哪些重要,哪些不重要;不是把有限的精力投入到关键点的优化中;因为对于他来说,反正重要不重要的都会去优化的,他会无视旁人的嘲笑,做他觉得有兴趣的事情。做的久了,不存在事后优化,因为对他来说,第一次编写时就考虑了种种,而且随着经验的增加,代码可以在保证高效的同时清晰可靠。

但是,我们有时就不去关心那些底层的性能差异。这同样出于性能考虑。因为追究到细节,全体和局部很可能得到相反的结论。因为代码本身也是系统的一部分。就连代码的规模也会影响到机器的运作效率。

当你可以从更高层次来看问题,你就会对性能有更多的理解。我们编写和设计软件,最大的敌人是复杂度。性能的敌人同样也是它。控制软件的每个层次上处理对象的粒度就是减小复杂度的武器。

回到 sort 的问题。函数调用真的是不可饶恕的开销吗?如果你对一个整数数组排序,那么性能考虑就很巨大。对于整数比较操作而言,函数调用,寄存器压栈出栈这些,会有成倍的开销。

但是,除了教科书和考试题中,我们有多少机会对一个整数数组排序?

排序是为了重新组织一组对象(往往这还不是最终目的。比如说,排序只是为了更有效的检索,有效检索才是目的),在一个特定层次上,对象的数量不宜很多,对象的粒度应该保持相当的规模。其结果就是,对对象的比较的开销会大于函数调用。因为涉及对象细节的操作是跨层次的。正如大多数情况下,你不会考虑访问一个内存字节的操作,对于机器意味着什么,OS 怎样调用虚拟内存、CPU 怎样管理 Cache 、内存管理器怎么收发控制信号。跨越层次的数据访问,函数调用是应该被忽略的。

C++ 的 sort 想获得更高的性能,代价是破坏了层次间的封闭性,往往是得不偿失的。

关于函数调用的开销这个问题,和不把性能做为第一位的程序员讲是很容易的;但对于看中性能的程序员来说,其实是很纠结的一件事。

《Unix 编程艺术》在 Part 2 开篇(模块性:保持清晰,保持简洁)就提到了这点。中文版 P84 引用了一段话:

Dennis Ritchie 告诉所有人 C 中的函数调用开销真的很小很小,极力倡导模块化。于是人人都开始编写小函数,搞模块化。然而几年后,我们发现在 PDP-11 中函数调用开销仍然昂贵,而 VAX 代码往往在 “CLASS" 指令上花费掉 50% 的运行时间。 Dennis 对我们撒了谎!但为时已晚,我们已经欲罢不能……

我想说的是,我们得承认一些损失。承认它们最终是为了更好的性能。而不是在同一层次上用语言技巧去抹平它。过度依赖 template inline 这些,并不仅仅是浪费你的时间去等待编译。

舍就是得。


上面写了这么多,只想引出下面这个话题:

有时候,对于宏定义式的属性管理方式,并不满足我们的需求。

C 语言里其实还有另一种惯例:我们可以考察 FILE 的接口。fopen 在打开文件的时候,可以传入多个选项。它是用字符串传入的。每个字符表示了一个属性。这使得使用它们更加友好,并极具灵活性。

还可以看 XLib 的接口设计。虽然不算太好,以至于后来又有人制作 XCB 。但我个人觉得,已经比 Windows 的对应部分设计的好太多了。

Xlib 里,使用联合 + 位域的方式管理对象的内部结构。也是相当不错的。

谈及 GUI ,我推荐大家读读 IUP 的代码,或许你会喜欢上它。在折腾 GUI 的东东时,在 QT/GTK/WsWidgets 等等之外,又可以多一个选择。它的接口设计采用了一些原则,使得足够简洁。而我在经历了太多次的重构 GUI 模块后,才领悟了点点东西。之后,发现了 IUP ,看到了许多我最终认同的东西,感慨颇多。

btw, 真的,boost::python 或是 LuaBind 这样的,利用一大套代码,让机器转换繁杂的接口,从一个语言的接口转换另一个语言的方式,最终都是权益之计。把接口设计简洁方是正道。

这里引出另一种需求,我们需要保留属性的名字信息。这样在分割明确的模块之间使用更加人性。尤其是在需要跨语言使用的时候。这种情况下,我会选择使用 string 做 key 而不是宏定义出来的整数。

这就牵扯到实现的效率问题了。好吧,我们又绕回了性能这个话题。

为此,我 谨慎 的给我们的系统添加了一个新概念:const_string 这个类型(注:在《C语言接口与实现》中,这个东西叫 atom ,这是个贴切的名字)。在我的项目中,反对随意的使用 typedef ,因为那意味着不断的新概念的加入,为此,付出更大的体力代价也是值得的。也就是说,宁可在每个结构和联合前显式的敲上 struct 和 union 。

这个类型其实是个特殊的指针,指向一个不变的字符串。如果需要调试输出,可以直接用 (const char *) 强制转换。但是,一个字符串必须通过 api ,build 出这个类型来。

其实现就是建立一个全局的字符串池,用 hash 表索引。里面存放不重复的字符串。我们只在其间存放那些系统中的标识符(用来索引资源用的字符串)。我们在进程生命期间,不再释放任何标识符。因为它们是限的,所以我们不用担心它们会吞噬我们的内存。也不需要用复杂的引用计数或是 gc 来管理它们。

这些特殊 string 可以用简单的指针去使用,不用再顾及生命期。可以直接用高效的变量比较、可以方便的在模块间传递、可以参与排序、可以用于 hash 映射的 key …… 总之,当成基本类型用就好了。一定程度上,可以弥补 C 语言没有原生字符串类型的不足。

我用它们去索引对象的属性名字。这样可以兼顾性能。

具体的实现是:我在每个 C 模块(利用宏)初始化一些字符串常量。当然这本是链接器应该干的事情。可 C 语言的模型并不原生支持这个,依赖 C 语言模型的编译器就不会给你代劳。忍住 C++ 的诱惑,我用丑陋的宏辅助我实现了一点东西。给语言增加并不存在的概念(新的类型)、使用宏、这都让我非常有罪恶感。带着这种感觉做设计,不至于犯太多错误,不至于破坏 KISS 。

然后在系统运行起来后,这个模块中的函数,可以通过简单的 if else if 来筛选不同的属性访问请求。如果对性能要求再苛刻一点的,还可以做一个简单的映射,最终转换为 switch case 。不要问我为什么不使用函数指针数组,在前面已经解释过了。如果真的需要,也值得考虑。

注:虽然 const_string 其实就是一个 const char * ,但也不要直接 typedef const char * const_string; 。这样编译器不会帮你找出错误的类型匹配。正确的方式是定义成 typedef struct literal const_string; ,我们不需要让使用 const_string 的模块了解 struct literal 是什么,实际上它什么都不是。想当成 const char * 的时候,依旧可以强制转换。但直接赋值是编译通不过的。

最终,一个对象的 getter 和 setter 可能被统一成两个 api :

int object_get(struct object *, const_string property, ...);
int object_set(struct object *, const_string property, ...);

有点 printf 风格?这就是 C 语言。


还想问这样会不会导致性能问题?如果在系统里,模块之间存在大量的交互,会高频率的访问对象的属性。这样不关是函数调用的开销了。可能还涉及 hash 表,涉及大量的 if else 比较……

那、就是你的设计问题了。你怎么可以允许这样的存在?

不同的对象活在不同的层次,它们最好是自生子灭。尽量少干涉它们。只在极少情况下改变它们的状态。大部分时间让它们自己运转去。至于模块内部,毋用我说,你不会使用外部接口去操控自己内部的数据吧?

对于 C++ ,不要幻想 inline 总能帮你解决问题;对于 C ,inline 并非传统。

承认 getter 和 setter 的开销。


一家之言,别相信我。请自己思考。实践。

同分类推荐文章

  1. 如何写好设计文档? (2026-06-23 08:00:00)
  2. Designing With Uncertainty: How AI Supercharges Probabilistic Thinking (2026-06-16 23:00:00)
  3. The Benefits Of Cognitive Inclusion In UX Research (2026-06-10 18:00:00)

查看更多 设计 文章 →

建议继续学习

  1. 腾讯抄你肿么办 (累计阅读 7,755)
  2. PHP Extension开发基础 (累计阅读 6,644)
  3. 使用PHP创建一个面向对象的博客 (累计阅读 5,461)
  4. 如何设计一个优秀的API (累计阅读 4,872)
  5. IE的Get请求(URL)的最大长度限制 (累计阅读 4,853)
  6. PHP API 框架开发的学习 (累计阅读 4,807)
  7. JavaScript中级笔记 (累计阅读 4,586)
  8. python十分钟入门 (累计阅读 4,232)
  9. 可靠 UDP 传输 (累计阅读 4,161)
  10. 产品经理应该具备的开发知识 (累计阅读 3,842)