编程经验:上拔if、下压for


“push ifs up and fors down”是代码结构的经验法则,

 将 if 条件向上推和将 for 循环向下推:

  • 尽可能将 if 条件移出函数并移至调用代码中。这集中了复杂的控制流,并且更容易看到冗余。
  • 从 switch 语句中提取相同的条件或从枚举中删除重复的逻辑来“溶解枚举”。
  • 通过集中控制流、消除冗余检查以及在适当时批量处理数据而不是单独处理数据来降低复杂性。

if语句上拔
如果函数内有条件,请考虑是否可以将其从被调用地方移至调用方

// GOOD
fn frobnicate(walrus: Walrus) {
    ...
}
// BAD
fn frobnicate(walrus: Option<Walrus>) {
  let walrus = match walrus {
    Some(it) => it,
    None => return,
  };
  ...
}

就像上面的例子一样,这种情况经常出现在先决条件中:函数可能会检查内部的先决条件,如果不成立就 "什么也不做",或者将先决条件检查的任务推给调用者,并通过类型(或断言)强制执行先决条件成立。特别是对于先决条件,"向上推 "可能会成为病毒式行为,导致总体检查次数减少,这也是本经验法则的动机之一。

复杂控制流
另一个原因是控制流和 if 复杂,容易产生错误。将 if 向上推,最终往往会将控制流集中到一个函数中,该函数具有复杂的分支逻辑,但所有实际工作都委托给了直线子程序。

如果有复杂的控制流,最好将其集中在一个函数中,而不是分散在整个文件中。更重要的是,将所有流程集中在一处,往往可以发现冗余和死条件。

比较:

fn f() {
  if foo && bar {
    if foo {
    } else {
    }
  }
}
fn g() {
  if foo && bar {
    h()
  }
}
fn h() {
  if foo {
  } else {
  }
}

与 g 和 h 的组合相比,f 更容易发现死逻辑分支!

溶解枚举
与此相关的一种模式就是我所说的 "溶解枚举 "重构。有时,代码最终会变成这样:

enum E {
  Foo(i32),
  Bar(String),
}
fn main() {
  let e = f();
  g(e)
}
fn f() -> E {
  if condition {
    E::Foo(x)
  } else {
    E::Bar(y)
  }
}
fn g(e: E) {
  match e {
    E::Foo(x) => foo(x),
    E::Bar(y) => bar(y)
  }
}

这里有两条分支指令,把它们拉起来就会发现,这是完全相同的条件,但被复制了三次(第三次被重新简化为数据结构):

fn main() {
  if condition {
    foo(x)
  } else {
    bar(y)
  }
}

For循环下压
这是数据导向学派的观点。物少则少,物多则多。

程序运行时通常要处理成批的对象。或者说,至少热门路径通常涉及处理许多实体。正是实体的数量首先使得路径变得热门。

因此,引入 "批量 "对象的概念,并将对批量对象的操作作为基本情况,将标量版本作为批量对象的特例,往往是明智之举:

// GOOD
frobnicate_batch(walruses)
// BAD
for walrus in walruses {
  frobnicate(walrus)
}

其主要优势在于性能。在极端情况下,性能非常强大。

如果你有一大批东西要处理,你就可以摊销启动成本,并灵活掌握处理顺序。事实上,你甚至不需要以任何特定的顺序处理实体,你可以使用矢量化/数组结构技巧,先处理所有实体的一个字段,然后再继续处理其他字段。

这里最有趣的例子可能是基于 FFT 的多项式乘法:事实证明,同时对多个点进行多项式求值比对多个单点求值更快!

关于 fors 和 ifs 的两条建议甚至是相辅相成的!

// GOOD
if condition {
  for walrus in walruses {
    walrus.frobnicate()
  }
} else {
  for walrus in walruses {
    walrus.transmogrify()
  }
}
// BAD
for walrus in walruses {
  if condition {
    walrus.frobnicate()
  } else {
    walrus.transmogrify()
  }
}


GOOD版本之所以好,是因为它避免了重复重新评估条件,从热循环中删除了一个分支,并有可能解锁矢量化。这种模式在微观和宏观层面都有效--好版本就是 TigerBeetle 的架构,在数据平面,我们同时对成批的对象进行操作,以摊销控制平面的决策成本。

jQuery当年相当成功,它的操作对象是元素集合。

抽象矢量空间的语言往往比成串的坐标方程更适合作为思考工具。

网友建议:

  • 最好是将 if 和 for 放在它们所属的位置 - 不高于或低于。如果你不确定,请多考虑一下。
  • 对于那些没有足够经验但尚未内化这些规则的人来说,简单实用的经验规则可以大有帮助。
  • 上下文和前置/后置条件对于代码重用和避免错误非常重要,但建议添加上下文标签作为解决此问题的一种方法。