Linus Torvalds 在 TED 演讲上所说的有品味的代码

2022-05-19 22:02:14 +08:00
 Biwood

需求是从单向链表中删除一个指定节点。

教科书上的(普通的)写法:

void remove_cs101(list *l, list_item *target)
{
        list_item *cur = l->head, *prev = NULL;
        while (cur != target) {
                prev = cur;
                cur = cur->next;
        }
        if (prev)
                prev->next = cur->next;
        else
                l->head = cur->next;
}

优雅的(有品味的)写法:

void remove_elegant(list *l, list_item *target)
{
        list_item **p = &l->head;
        while (*p != target)
                p = &(*p)->next;
        *p = target->next;
}

目测充分利用的指针的特性,代码量少了不少。

代码仓库和详细解释在这里: https://github.com/mkirchner/linked-list-good-taste/blob/main/README.md

11436 次点击
所在节点    程序员
120 条回复
adoal
2022-05-19 23:26:32 +08:00
话说如果我来写的话,第一反应肯定想不到 Linus 的写法,还是会对边界做判断的。但我会把判断写成卫式,在循环之前,当 target 就是 l->head 时替掉 l->head ,而不是循环结束才根据 prev 是否为 NULL 来判断。那样我觉得不如先判断更符合直觉。
Leviathann
2022-05-19 23:29:34 +08:00
意思就是说 C 可以拿到 指向关系 这个东西本身并修改是吧
LeegoYih
2022-05-19 23:35:47 +08:00
确实牛
kidlj
2022-05-20 01:01:44 +08:00
爱丽跟头!
GeruzoniAnsasu
2022-05-20 03:13:33 +08:00
看了这个帖子感觉很多人的代码修炼筑基并不那么理想


「学术地、迂腐地」来讲,教科书上链表有两种典型实现,一种是用空的头结点来作为表头,一种是用指向第一个结点的指针来作为表头。


如果用头结点来实现,那天生可以与第二种「同样简洁」:

list_item*p = &l->head // 注意 head 是元素了,不再需要多一个 prev 了
while (p->next != target) p=p->next // 注意 p 不可能 null ,p->next 一定是存在的( p->next 可以 null 但->访问必定合法)
p->next = target->next // 除了 target 为 null 这种原代码也不考虑的边界,其它条件都不需要 sentinel


linus 给出的「优雅版」本质上是,把指针本身视为节点,p 指向的是元素结构的 [next 指针],而使用「头指针」实现的链表虽然第一个元素可能不存在,但第一个「 next 指针」是必定存在的,这跟我上面给的头结点例子写法就基本一样了

不过为什么基本见不到使用头结点实现的链表呢,因为 1. 浪费无谓的空间 2.泛型化就不好写或不优雅了
rpman
2022-05-20 04:22:53 +08:00
两个 case 都没有考虑 target 不在链内的异常
mingl0280
2022-05-20 04:45:48 +08:00
……这还要考虑第一个有蛋用,C/C++的单向链表不都应该默认写成第二种形式?说得那么玄乎,不就是拿二级指针替掉了 prev ?你把 list_item 视作`template<Typename T>`就是天然的泛型化了。
这还看不懂还要解释水平有多菜?
yzbythesea
2022-05-20 05:03:11 +08:00
The most interesting port to me is that last if statement (1st approach), (2nd approach) is better and it does not have the if statement. It rewrite it so that a *special case* goes away and becomes the normal case. And that's good code.

I sent your this stupid example, that is not relevant because it is too small. Good taste is much bigger. Good taste is about really seeing the big patterns and kind of instinctively knowing what's the right way to do things.

Linus 的原意是第二种写法去掉了对于 edge case 的处理,属于 generalize 了代码,看到了 big pattern 。并不是要秀什么语法或者算法技巧。
ColorfulBoar
2022-05-20 05:42:33 +08:00
我是没看出来 Linus 有啥 taste……正常人会写成这样,扫一眼就知道在干什么

(你说里面是不是有 UB ?反正出事也是 container_of 这种玩意比我先死)
weyou
2022-05-20 08:36:47 +08:00
不用考虑节点找不到的情况么?
weyou
2022-05-20 08:42:28 +08:00
@ColorfulBoar 确实,这个才是一般能想到的写法
Austin2035
2022-05-20 08:44:34 +08:00
不做评论,分享一下 Redis 双向链表的删除节点写法

/* 删除一个节点 */
void list_del_node(list *list, list_node *node)
{
/* 判断该节点是否有直接前驱 */
if (node->prev)
node->prev->next = node->next;
else
list->head = node->next;

/* 判断该节点是否有直接后继 */
if (node->next)
node->next->prev = node->prev;
else
list->tail = node->prev;

/* 释放该节点 */
free(node);
list->length--;
}
luxor
2022-05-20 08:57:39 +08:00
源码上的优雅不代表编译后也是高效的。长得好看不等于就有饭吃。
Biwood
2022-05-20 09:44:38 +08:00
@luxor 也许你们以前所说的“优雅”不是真正的优雅,这个例子里的优雅代码恰好是更高效的写法,你可以看看原文的解释。感觉就像是裁剪掉了不必要的细枝末节,回归事物原本的简单自然。
greygoo
2022-05-20 09:47:05 +08:00
@luxor 优雅的写法的确生成了更高效的代码: https://godbolt.org/z/6qo3Yqez3
hitmanx
2022-05-20 09:52:27 +08:00
@adoal 我估计我第一反应也是如此。这种方法可读性也挺好的,除了还有分支以外。
agagega
2022-05-20 10:31:10 +08:00
明显第二种好理解,因为省去了不必要的分支条件。写 LeetCode 各种带 Corner Case 的题时就很想要这种代码
bung
2022-05-20 10:45:07 +08:00
对指针很有感觉的人,看第二种好理解。

很多人见到多级指针要延迟反应一下,就觉得第一种好理解。
hccsoul
2022-05-20 10:50:07 +08:00
你自己写自己的项目想怎么优雅怎么优雅 上班写业务代码可读性还是比较重要,你永远不知道你同事的水平有多差你写的再好他也看不懂
qqg1530
2022-05-20 11:04:47 +08:00
可以但没有必要

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

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

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

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

© 2021 V2EX