如何拥有 Linus Torvalds 所说的编码“好品味”

2016-11-11 14:14:57 +08:00
 darluc

翻译自:Applying the Linus Torvalds "Good Taste" Coding Requirement

点击阅读全文

最近一次对 Linus Torvalds ( Linux 之父 ) 的采访视频中,约 14 分 20 秒的位置,他提到了带着“好品味”编程的观点。好品味?采访者对其细节进行了追问,而 Linus 则是早有准备。

他首先展示了一个代码段。不过这不是一个有着“好品味”的代码。这个代码段是个坏样例,用作对比的原始代码。

remove_list_entry(entry) 
{
  prev = NULL;
  walk = head;
  
  // Walk the list
  
  while (walk != entry) {
    prev = walk;
    walk = walk->next;
  }
  
  // Remove the entry by updating the
  // head or the previous entry
  
  if (!prev)
    head = entry->next;
  else
    prev->head = entry->next;
}

无品味的代码示例

这是个 C 语言实现的函数,用于移除链表中的某个节点。共 10 行代码。

他让我们注意底部的 if 语句。就是这条 if 语句招致了他的批评。

我暂停了视频,研究了一下幻灯片。最近我有写过一些类似的代码。觉得 Linus 实际上就是在说我的品味差。我只好暂时放下我的自尊心,继续看视频。

Linus 向观众解释,从链表中移除一个节点时,有两种情况需要考虑。如果节点处于链表头,会于节点处于链表中的处理方式有所不同。而这就是那个 if 语句”坏品味“的来源。

不过既然他也承认这种特殊处理是必须的,那它到底坏在哪里?

接着,他又向观众展示了第二张幻灯片。这是他实现的同一个函数,不过是有着“好品味”的。

remove_list_entry(entry)
{
  // The "indirect" pointer points to the
  // *address* of the thing we'll update
  indirect = &head;
  
  // Walk the list, looking for the thing that
  // points to the entry we want to remove
  
  while ((*indirect) != entry)
    indirect = &(*indirect)->next;
  
  // .. and just remove it
  *indirect = entry->next;
}

好品味代码示例

最初的 10 行代码现在缩减为了 4 行。

但是重点并不在于代码行数的减少。重点是那个 if 语句消失了,不再被需要。代码重构后, 无论节点在链表中的什么位置,都可以采用相同的处理过程来进行移除。

Linus 解释了这段新代码对于边界情况的消除才是重点。采访继续,进入了下个话题。

我研究了一会这个代码。 Linus 是对的。第二个幻灯片所展示的代码更好。如果这是一道测试编码好坏的题目,那我可能已经挂了。消除条件判断的想法从未在我的脑子里出现过。而且我也像那坏例子一样写了不止一次了,因为我经常进行链表处理操作。

这个关于品味的展示,好处不仅仅在于教会你如何从一个链表中去除节点, 它还使你去思考你曾经写过的代码,你曾经在程序中写过的小小算法或许还有改进的空间,并且以一种你从未想过的方式出现。

所以最近我审阅项目代码时,这成为了我的关注点。很巧的是,这个项目也是使用的 C 语言。

就我的理解而言,“好品味”的关键是消除边界情况,而这些情况一般表现为条件判断语句。越少使用条件判断,你的代码“品味”就会更好。

下面有个我对代码进行改进的例子分享给大家。

初始化网格边缘

以下是我写的一个算法,用于初始化网格边缘的点,该网格使用一个多维数组表示:grid[rows][cols]

这段代码的作用是初始化网格的边缘座标点 —— 即顶部一行,底部一行,左侧一列,和右侧一列。

为了完成工作,一开始我遍历了网格中的每个点,并且利用条件来判断他们是否处于边缘。代码如下:

for (r = 0; r < GRID_SIZE; ++ r) {
  for (c = 0; c < GRID_SIZE; ++ c) {
    
    // Top Edge
    if (r == 0)
      grid[r][c] = 0;
    
    // Left Edge
    if (c == 0)
      grid[r][c] = 0;
    
    // Right Edge
    if (c == GRID_SIZE - 1)
      grid[r][c] = 0;
    
    // Bottom Edge
    if (r == GRID_SIZE - 1)
      grid[r][c] = 0;
  }
}

虽然这段代码可以正常工作,回头来看,确实有点问题:

  1. 复杂度 —— 在二重循环中使用 4 个条件判断语句,看起来有些过于复杂了。
  2. 效率 —— 如果 GRID_SIZE 等于 64 ,这个循环迭代了 4096 次,只是为了给 256 个边缘点赋值。

Linus 应该会觉得,这可真没品味。

所有我对它进行了一些修补。花了些时间后,我可以降低它的复杂度,使其只需要一个包含四个条件判断的 for 循环。这只是在复杂度上改进了一点点,却极大地提升了性能,因为它只循环了 256 次,每个边缘上的座标点对应一次循环。

for (i = 0; i < GRID_SIZE * 4; ++ i) {
  
  // Top Edge
  if (i < GRID_SIZE)
    grid[0][i] = 0;
  
  // Right Edge
  else if (i < GRID_SIZE * 2)
    grid[i - GRID_SIZE][GRID_SIZE - 1] = 0;
  
  // Left Edge
  else if (i < GRID_SIZE * 3)
    grid[i - (GRID_SIZE * 2)][0] = 0;
  
  // Bottom Edge
  else
    grid[GRID_SIZE - 1][i - (GRID_SIZE * 3)] = 0;
}

这的确是一次改进。但是代码真是太丑陋了。而且并不易于理解。因此,我对这段代码并不满意。

点击阅读全文

5751 次点击
所在节点    Coding
10 条回复
ianva
2016-11-11 15:00:35 +08:00
是一个良好的数据抽象解决问题的例子,但还缺少一个良好的过程抽象
iEverX
2016-11-11 16:33:41 +08:00
我会写四个循环
TerrenceSun
2016-11-11 19:50:30 +08:00
这样呢?
for (i = 0; i < GRID_SIZE; ++i) {
grid[0][i] = grid[GRID_SIZE-1][i] = grid[i][0] = grid[i][GRID_SIZE-1] = 0;
}
holyghost
2016-11-11 20:51:14 +08:00
justfly
2016-11-11 21:00:47 +08:00
这种感觉的培养,不止局限在代码层面,而是一种找到问题的本质和化繁为简的一种思想。

其实在生活中遇到的各种事情,分析业务需求,日常写业务时都可以锻炼的,很多时候会明显感到是相通的。
zhy0216
2016-11-12 00:43:00 +08:00
第一个例子 不知道用 python 怎么做 ..
cnnblike
2016-11-12 00:56:32 +08:00
确实有道理
lsmgeb89
2016-11-12 10:45:53 +08:00
可以说是品味问题也不是,对有些人来说即使他意识到了他的处理方法不够 general ,他也很难想到方法二。
lsmgeb89
2016-11-12 10:57:25 +08:00
具体看了下代码,感觉方法二要看一会才能理解,需要对二级指针有很深的理解。
alexapollo
2016-11-12 19:58:03 +08:00
不认为 linus 的这个做法是对的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/319814

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX