列表生成的三种方式比较(原生语法、流、DSL)
概述
列表生成是平时开发中经常遇到的小功能。例如,生成一个0到n的列表,根据已有列表过滤一些元素来生成新列表等等。这样的问题一般几行或者十几行代码就处理掉了,也没有去注意。最近学习 Python 列表生成式的特性,在看到示例时联想到以前用 Java 和 Common Lisp 解决这类问题的方式,就写下本文来比较和总结各种方式的特点和优劣。
三种方式
- Python 列表生成式:Python中的列表生成式是在原生语法上对列表生成提供支持;
- Java Stream 类:Java 8 中引入的 Stream 相关类是 Stream API 在 JVM 平台的一种实现;
- Common Lisp 的 loop 宏:loop 宏虽然是 ANSI 标准中就提供的宏,但是也可以自行编写出来,它用一种更接近英语的形式来表达你的程序,然后这个形式被翻译成 Lisp,属于一类内部 DSL。
示例
-
生成1-10的列表:
Python 的列表生成式:
list(range(1, 11))
Java 的 Stream:
IntStream.range(1,11).boxed().collect(Collectors.toList())
Common Lisp 的 loop 宏:
(loop for i from 1 to 10 collect i)
-
生成1-10的平方和
Python 的列表生成式:
[x * x for x in range(1, 11)]
Java 的 Stream:
IntStream.range(1,11).map(i -> i*i) .boxed().collect(Collectors.toList())
Common Lisp 的 loop 宏:
(loop for i from 1 to 10 collect (* i i))
-
生成1-10的偶数的平方和
Python 的列表生成式:
[x * x for x in range(1, 11) if x % 2 == 0]
Java 的 Stream:
IntStream.range(1,11).filter(i -> i % 2 == 0).map(i -> i*i) .boxed().collect(Collectors.toList());
Common Lisp 的 loop 宏:
(loop for i from 1 to 10 when (evenp i) collect (* i i))
-
生成"abc"和"xyz"排列组合
Python 的列表生成式:
[m + n for m in 'ABC' for n in 'XYZ']
Java 的 Stream:
"abc".chars().mapToObj(i -> (char) i).flatMap(c1 -> "xyz".chars().mapToObj(i -> (char) i).map(c2 -> c1.toString() + c2)).collect(Collectors.toList())
Common Lisp 的 loop 宏:
(loop for c1 across "abc" append (loop for c2 across "xyz" collect (coerce (list c1 c2) 'string)))
总结
- Python 的列表生成式是专门针对这类问题的语法,采用了最少的字符,在语法层面解决这个问题,看起来是最优雅的。但是降低了语法的一致性,提高了语法的复杂度; 另外学习成本不高,不过由于其只能解决特定问题,且扩展性有限,因此学习的性价比也不是很高。
- Java 的 Stream 相关类是 Stream API 的 Java 实现,主要用于处理序列和数据流,刚好也可以用来解决这类问题。由于是在类和方法层面实现,因此可以有多种实现(例如,RxJava),开发者也可以根据需要实现自己的 Stream 库,扩展性较强;其次,因为 Stream 是一个相对通用的概念,很多语言都有自己的实现(或者第三方库实现),学习成本稍高(最好懂函数式编程),但是学习的性价比很高。但是因为 Java 对函数式编程的支持不那么好,Java 对 Stream API 的实现使用起来并不是那么舒服且略微有些繁琐。
- Common Lisp 中的 loop 宏是用宏实现的内部DSL,主要用于处理循环逻辑,也可以用来处理这类问题。DSL所带来的表达上的优势是非常明显的;另外宏是 Lisp 提供的基本特性,开发者可以根据自己的需要来编写自己的 loop 宏,可见扩展性也是不错的。但是DSL的维护门槛却是当下很难避免的缺点(好在loop宏已经成为标准的一部分)。