首页 > 解决方案 > 社区标准和利弊?破坏性与非破坏性迭代

问题描述

我见过两种基本的方法来迭代一组项目(例如 a listarray等)。一种是增加或减少索引,这往往是许多“内置”(例如forpython中的循环)的默认值。另一种是对集合进行切片并始终评估第一项。

这两种方法可以“工作”(但可能并不理想),IIRC,在许多常见的网络语言中,包括 Ruby、Javascript 和 Python。因此,正如@DavidMaze 指出的那样,当我抽象地询问非特定语言的方法时,这个问题可能不适用于每种语言,或者可能存在巨大的特定语言差异。

例如,使用 Python:

collection_1 = [0, 1, 2, 3, 4]
x = 0
while x < len(collection_1):
  print(collection_1[x])
  x += 1
print("collection_1: {}".format(collection_1))

结果:

0
1
2
3
4
collection_1: [0, 1, 2, 3, 4]

另一个

collection_2 = [0, 1, 2, 3, 4]
while len(collection_2) > 0:
  print(collection_2[0])
  collection_2 = collection_2[1:]
print("collection_2: {}".format(collection_2))

结果:

0
1
2
3
4
collection_2: []

显然 ONE 是保存数据所必需的,如果存储空间有限并且迭代后不再需要数据,则 THE OTHER 会提供一些优势。除了这些场景之外,我认为 ONE 通常更有用(并且不太可能引入错误)。我还认为 ONE可能总是或几乎总是更便宜以获得相同的结果,但我想测试上述假设。我感兴趣的是:

标签: javascriptpythonrubyiterationmutation

解决方案


我会建议一个评分标准:

1.匹配你现有代码库的风格。

在大型项目中,如果以一致的风格编写内容会很有帮助。对于像列表迭代这样的超低级别的事情,这不一定是什么大问题,但如果大多数事情都是“一种”方式来完成相同的任务,那么“另一种”方式会让后来的人感到困惑。

2. 如果您的语言有本地列表迭代(如“地图”功能),请使用它。

大多数较新的语言都有一些方法可以对整个列表采取行动,或者有一些标准的方法来遍历列表。

# Python
for x in collection_1:
    print(x)
// Javascript
collection_1.forEach(x => console.log(x))
-- Haskell
mapM_ putStrLn collection1
// Go
for _, x := range collection1 {
        fmt.Printf("%d\n", x)
}

在 C 及其密切派生的语言中,索引数组是惯用的,如果它是该语言的通常约定,那么就这样做。

/* C */
void printList(int *l, int len) {
    int i;
    for (i = 0; i < len; i++) {
        printf("%d\n", l[i]);
    }
}

3.了解“列表”是什么类型。

不同的语言对具有不同特征的列表有不同的默认实现。如果“列表”是单链表(Lisp、Haskell),那么一次迭代一个元素并传递“列表的其余部分”实际上是正确的。

-- Haskell
-- printAList is a function that takes a list parameter
-- and returns an IO action producing no value in particular.
printAList :: [Int] -> IO ()
-- To print an empty list, do nothing.
printAList [] = return ()
-- To print a non-empty list:
printAList (x:xs) = do
  -- Print the first element;
  putStrLn (show x)
  -- Then print the list of things starting at the second element.
  printAList xs

但是,由于它是一个单链表,按索引获取元素可能需要 O( n ) 时间,而且您绝对不想这样做。

在某些语言中,“列表”是起始位置加上长度(C 中的数组,Go 中的切片),在这种情况下,“创建其余列表”并不是那么昂贵,尽管它看起来有点奇怪.

/* C programmers will look at you funny */
void printList2(int *l, int len) {
    for (; len > 0; l++, len--)
        printf("%d\n", l[0]); /* or *l */
    }
}
// Go: also strange, but not wildly inefficient
func printList(l []int) void {
        while (len(l) > 0) {
                fmt.Printf("%d\n", l[0])
                l = l[1:len(l)]
        }
}

但是,如果“列表”是一个项目块并且更改它涉及复制整个列表(Python,正如@Ry- 在评论中建议的那样),那么您绝对不希望这样做;但是然后按索引查找项目相对便宜。

# Python; this is common to update a list in place
for i in range(len(container_1)):
    container_1[i] += 1

4. 温和地偏爱非破坏性的动作。

在现代硬件上,昂贵的东西是 I/O、网络调用和糟糕的算法。小的效率大多无关紧要。两者之间是否有实际区别:

# Python

def doubleListInPlace(l):
  for i in range(len(l)):
      l[i] *= 2
  return l

def doubleListAndCopy(l):
  return [2 * x for x in l]

def main():
  l = [1, 2, 3]
  ll = doubleList...(l)
  lll = doubleList...(ll)
  print("Doubled list: " + repr(ll))
  print("Quadrupled list: " + repr(lll))

不可变 ("...andCopy") 表单分配了一个额外的列表,当然,但是当您稍后使用结果时,等待的惊喜更少,并且您(通常)不会注意到额外的分配。使用更不可变的样式在测试(您可以创建一次测试夹具并在测试中重用它)和维护撤消堆栈(这是 Javascript Redux 框架中“时间旅行调试器”的核心,对于实例)。

一种方法是否更昂贵?

如果您的“本机”列表类型是单链表,则尝试按索引遍历它需要 O( n ^2) 时间(您必须遍历所有n 个元素才能到达最后一个元素,加上n -1 才能获得到倒数第二个,加上...)。如果您的“本机”列表类型基于数组或向量,那么使用子列表将花费 O(n^2) 时间(您必须复制所有尚未处理的n -1 项)。对此没有特定的通用跨语言答案。


推荐阅读