本人使用过的编程语言,细数包括5个大类,总计超过28种(参见我的技能栈),有些代码写得多,有些写得少,写得最多的是:Java、JS、HTML、CSS、Golang、……,其次是C++、C、PHP、C#、Lua、SQL……,写得少的是Python、VB、Jade、MatLab、……。
下面说一点心得体会(原创、纯手打),供学习、比较、参考。
首先,看基础语法。好的基础语法,应该是简单直接的、易于记忆和书写的。“功能强”跟“简单易用”,是两个矛盾面——在基础语法中,加入常用增强功能,确实能方便编程,但是也让基础语法变得臃肿。比如说,for循环,有些语言甚至有5种以上的写法,虽然每种写法特点不一样,但是这增加了语言学习和代码维护的负担。如果,它把for循环精简到只有两种,比如 for(i=0;i<n;i++)和 while(flag) 这种通用的结构,那么所有程序中,只有这一种写法,维护起来要容易些。但是,也有一个弊端,比如一些特殊数据结构的循环,集合、链表、Map等,用通用结构去写不算是最简洁。
怎么看这个问题?增强语法,绝大多数时候,都可以用封装库函数(工具函数)去解决。我认为,增强程序功能,首先应该从增加库函数入手,而不是增强语法。虽然增强语法比提供库函数,更易用,但要考虑这样做是不是也降低了程序可维护性(因为库函数代码,是标准语法,是大家都看得懂的,而增强语法,如果不学习一下,是不知道怎么回事的)。
什么时候该增强语法呢?我认为是简单直接的、且常用的、且比提供库函数要好得多的地方,这三个条件缺一不可,反例如下:
如果这个功能不常用,那么提供一个库函数,哪怕使用麻烦一点又有多大影响呢?何必要去设计一个新语法,用于一个不常用的功能上呢?
如果这个功能调用库函数的方式也很容易、很方便的做到,为什么要用新语法去做?增加学习和维护成本。
如果为了某一套功能设计出一个非常复杂的语法,那么和直接用库函数相比又有什么本质区别呢,语法的目的就是简化使用,复杂的语法本身也难于使用、难于升级维护,不如不要。
举个例子,Java 8新语法出来这么久了,其实也没增加多少内容,我尝试用过很多次,但我就是记不住,最终不想用了。为什么?因为Java7、Java6的语法已经很强大了,Java8的新语法只是锦上添花、“语法糖”而已。
比如 lambda表达式、方法引用,形如:
// 例1 Function<Integer, int[]> fun = int[]::new; int[] arr = fun.apply(10); // 等价于 int[] arr = new int[10]; // 例2:混搭,凸显出方法引用的不通用性 public static <T> Collector<T, ?, List<T>> toList() { return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, CH_ID); } // 例3:同上,属于混搭,filter处与map处的处理方式不统一。 public List<String> getIdByColumn(Integer column) { return data.entrySet().stream().filter((e) -> { return e.getKey().equals(column); }).map(Map.Entry::getValue).collect(Collectors.toList()); } // 例3:方法引用 Map.Entry::getValue 等价于 (e) -> {return e.getValue()} public List<String> getIdByColumn(Integer column) { return data.entrySet().stream().filter((e) -> { return e.getKey().equals(column); }).map((e) -> {return e.getValue();}).collect(Collectors.toList()); } // 例4:标准语法 list.sort(new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { if(o1>o2) return -1; else if(o1<o2) return 1; else return 0; } } ); // 例4:lambda语法 list.sort( ( o1, o2 ) -> { if(o1>o2) return -1; else if(o1<o2) return 1; else return 0; } ); // 例5:lambda看似节省了3行代码,但是相对于整个函数体复杂逻辑,节省3行微不足道 values.stream() .mapToInt(e -> { int sum = 0; for(int i = 1; i <= e; i++) { if(e % i == 0) { sum += i; } } return sum; }) .sum());
lambda语法这样写省略了3行代码,要简洁一些。但是它让Java代码多了一层不透明的语法包装,而且lambda有使用限制(lambda表达式没有匿名函数的完整功能),不能通用,而且从上面的例3可见,方法引用也是lambda表达式的特例。我认为,一个语法,你不能做到通用、统一,这个语法是很失败的——lambda不能完全取代传统的写法,那么代码中就会存在两种不同的写法,看起来就很恼火。我真切的希望,语法层面能保持简洁,而不是炫技。
Java8的新语法是个败笔,相反,Java 7的一些语法改进则很好,举个正面的例子,例如泛型的自动推导,非常好,它通用、实用、无歧义,因为编译器是可以帮我们自动推导出泛型类型的,这个改进让我们节省了以前那种“冗余”的写法,即便是删掉定义的泛型部分,语义也没有任何影响,如下所示:
// 改进前 List<String> list = new ArrayList<String>(); // 改进后 List<String> list = new ArrayList<>();
一门好的语言,语法应该规则严谨,没有歧义,更没什么黑魔法、变异用法。任何人写出的代码都基本一致,这使得这门语言简单易学、易于阅读和维护。放弃部分“灵活”和“自由”,换来更好的维护性,我觉得是值得的。
我再谈谈一个极端,比如说HTML,这门语言很简单,大家都很容易学习。简单到什么程度呢?它连for循环都没有。就是这么简单的一门语言,用了这么多年,到HTML5才有一个不算大的升级,而且HTML5的写法并没有给之前版本带来多少改变。即便是20年前的代码,现在依然很容易维护。这是相当成功的。
为什么HTML不官方支持模板渲染、for循环,就像JSP、ASP那样。其实理论上HTML也可以做的,但它为什么不做。历史不再去深究,但事实证明,有些事情语言本身不去做,也有方法去实现。语言本身做好最基础的事情就行了。
再说说,一个特殊的例子:Shell脚本。Shell的基础语法并不复杂,只是它的语法有些古老,不够简洁易用(比如写个if后面都还得更一个fi,获取数组长度要用${#arr[@]},当然如果你用熟练之后,其实也是OK的),其功能也不是很完善(比如对数据结构的支持,Map,Set等)。它最主要的作用是编排各种Unix/Linux指令。这种语言,其关键已经不在于语言本身的基础语法了。因为linux命令可以扩展它。但是由于它没有包管理之类的东西,它的运行,必须得依赖于操作系统上各种指令。而且也没有断点调试功能,只能用传统的打印变量的方式跟踪执行。我认为,它的灵活和简陋,会导致它的代码很难维护和大工程化使用,通常一个100行的shell脚本已经相当难维护了——灵活而简陋,意味着,很多功能,比如字符串的常见操作,没有官方的标准API去调用,只能借助各种扩展(比如awk、sed、cut、grep等等),可以这么说,一个稍微复杂点的功能,用Shell实现,10个人写出来的代码,很可能有10个不同版本。而且Shell编程中,确实有很多黑魔法、变异用法,比如“shopt extglob”、"set -eo pipefail"、“xargs -r”( --no-run-if-empty,this option is a GNU extension),不同的操作系统上,不同的shell版本(bash、ksh、tcsh、ash、dash等)也会有一定区别。
最后再说一说,面向过程和面向对象的编程语言。我觉得,面向过程的编程语言,适合小型、微型的独立项目,而面向对象的编程语言,更适合中大型以上规模的项目及工具库。我们知道,面向对象的语言有三大特性:封装、继承、多态。这三大特点,可以说是工程化的设计思想体现,对代码进行分类和复用的方法,它是从大工程项目中总结提炼出来的套路。在很小的项目中,也可以按这个思路来,但是会显得有些繁琐和不够直接。如果是做一个大项目,开发的人很多,代码量也很大,强烈建议用面向对象的编程语言,甚至像JavaScript(ES 6以前的版本)这样大规模被使用的语言,其实都不太适合大项目,真的很难维护!所以才有了ES 6、CoffeeScript、TypeScript等改进版本。
总结:说了这么多,心里应该有数了。我再补充几点:
1、学一门编程语言,首先只学“基础语法中最基础的那些”,先不要碰那些扩展的语法,和高级语法。
2、遵循大多数人遵守的编程语法和习惯,不要利用语法技巧,写出让人不太好理解的代码。
3、代码量、执行效率、可维护性,三者的选择上,首选可维护性。(下面特别说明!)
不要为了节省代码,而降低可维护性,也不要为了提高一点点运行效率而降低代码的可维护性(很多人在这一点上会犯错——包括我,在我刚开始工作的那些年,受到《Effective C++》《Effective Java》等的影响,追求代码的极致运行性能,甚至至今,我写if-return时都要节省一个else,用Link List更好的时候我就不会用Array List,还有,我经常用一些奇葩的写法去改造传统的代码,以提升程序的性能。但是后来我经常看别人的代码,经常改别人的代码——各种语言的代码,我站在程序员和管理者的双重角度看,代码的可维护性优先级应该要高于执行效率,这个问题我也同一些资深架构师讨论过,现在的CPU执行速度非常快了,为了节省程序理论上的一两个指令执行时间,而采用非传统的写法,是弊大于利的,甚至大部分时候,O(n)和O(n^2)复杂度,执行时间都没有本质差别,木桶效应告诉我们,要提高效率应该从最慢的地方入手,比如网络、IO,而不要去纠结O(n)和O(n+k)、O(n^2)的差别)。
下面讨论一些优秀的正面例子和糟糕的反面例子:
定义变量的方式(讨论)
Go语言中至少有两种:
a := float64(5) var a float64 = 5
从 工程学和心理学角度来说,程序员想在程序中定义一个变量(而不是使用一个已存在的变量),他的直接思维顺序是这样的:
我要定义一个变量,要做一个声明(标记)
给这个变量取一个名字
给这个变量声明一个类型
给这个变量赋一个初值
所以,声明变量的直觉方式为: 标记 名字 类型 值,这样的顺序。
比如,我想在for循环之前,声明一个count变量,去获得每个值相加的和。按照常规思维,应该是这样定义:
var count int = 0; // 依次为 标记、名字、类型、值
有几种情况,1、这个值暂时不设置(比如类变量),由外部函数去设置。此时省略赋值,简化为:
// 省略赋值 Go语言的方式 var count int // TypeScript的方式 var count:number
这正是Go语言、TypeScript中支持的方式,非常棒!Go+1分,TypeScript+1分!其他语言-1分
2、可以通过赋值的类型,自动确定变量的类型,此时省略类型定义,简化为:
var count = 0; // 省略类型定义
新版Java才支持这种方式,所以TypeScript、Go等都+1分,新版Java+1分
但是Go还支持另外一种方式:
count := 0 // 用:代替var
这种语法糖,完全没必要,而且不太直观,反倒是增加了学习和维护难度,另外,还违背了变量定义的思维顺序。所以,Go语言 -2分。下面是Go语言中Map的初始化方式,有5种,真头痛:
// 方式一 var m map[int]string // 声明一个map,此时的 map == nil fmt.Println(m) m = map[int]string{} // 初始化一个map,此时的 map != nil,是map[] fmt.Println(m) // 以上两种的区别在于有没有被初始化容量 // 方式二 var m2 map[int]string = map[int]string{} fmt.Println(m2) // 方式三 m3 := map[int]string{} fmt.Println(m3) // 方式四 m4 := map[string]string{ "name":"Tinywan", "school":"BAT_UN" } fmt.Println(m4) // 方式五 m5 := make(map[string]string) m5["name"] = "Linux" m5["school"] = "Unix" // 注意:m4和m5两种初始化的方式等价
3、在代码的固定位置,只能声明变量,不能使用变量。此时可以省略 声明标记。
例如函数的参数声明,类的变量声明。这些地方出现变量,默认都是第一次声明。
class User { // 省略 变量定义的 var 标记 age int; name string; sex = 0; // 赋初值,可省略类型 public User(age int, name string, sex = 0) { // 省略var标记 // ... } }
在Go语言的包全局变量中,始终要用 var,例如 var bb int,同理TypeScript也是,所以-1分,但是注意,Go的结构体参数不需要用var标记,算是弥补了包变量的缺陷吧。
在Java类变量中,比较接近于 age int 这种形式,但是不支持 sex =0,所以-0.5分。
在函数的参数声明中,Go和TypeScript都不需要var,而且TypeScript支持sex = 0,非常棒,但是Go中函数的参数不支持默认值,所以TypeScript + 1分,Go + 0.5分,新版Java + 0.5分。
总结:
说实话,对新兴编程语言Go的期望大,失望也不小。(后文还会讨论更多方面)
然而微软的TypeScript,从设计上讲还真的非常不错!
新版Java(Java 10、11)也吸取了一些精华。对于一门历史悠久、用户庞大的编程语言来说,语法上的更新一定是非常谨慎、保守的,除非一些非常好的改进(有70%以上的支持者)才会考虑实现(上文说到的Java8的lambda表达式是一个反例,如果做一个公开投票,支持率应该远远低于70%)。
Go语言中的数组切片
我认为这是非常糟糕的设计,明明是可以通过内置函数(例如slice)解决的数据操作问题,非要弄成语法糖,难学又难用。
语言设计中,一定要遵循:Less is More的原则,没有什么是必要的,除非没有它不行。显然Go语言并没有遵循这种哲学。
举例:
var arr = [...]int{19, 2, 0} // 定义一个数组 var arr = []int{19, 2, 0} // 定义一个数组的切片
还有:
var a = [5]int{76, 77, 78, 79, 80} var b []int = a[1:4] var c = a[:] i := make([]int, 5, 5) // 提前知道数组中的值 var a[2]int{11,22} var a[20]int{19:11} // 索引值为19的元素赋值为 11 ,其他的默认为 0 // 不指定数组的长度 ... var c = [...]int{11,22,33,44} var d = [...]int{19:90} // 尽可能的满足索引值得数组 // 第一种方式 n := [10]int{} n[1] = 10 // 第二种方式 m := new([10]int) m[1] = 20
看着头疼,而且还有一个很难接受的问题:
数组和数组切换,都是数组形式,但是数组参数是值传递,切片参数是引用,对函数内部来说,它根本不知道传入的参数是值传递的,还是引用传递的。(没有显式声明,例如C语言中的&,不具备一致性)
func changeLocal(num [5]int) { num[0] = 55 fmt.Println("inside function ", num) } num := [...]int{5, 6, 7, 8, 8} // 数组 changeLocal(num) // 值传递,num不变 nos := []int{8, 7, 6} // 切片 changeLocal(nos) // 引用,nos会变
参见上面的注释。而且数组切片很容易引起内存泄漏问题。
Go语言中的Map
对比Java、TypeScript 与Go语言中的Map定义:
Map<String, Map<String, Integer>> map = new HashMap<>(); map.put("k1", ?); // 新增 map.remove("k1"); // 删除,remove是Map的一个API var map1 = make(map[string]map[string]int) // map是语法关键字,变量名不能为 map map1["k1"] = ? // 新增 delete(map1, "k1") // 删除,delete是系统内置函数 let map = new Map(); map.set("k1", ?); // 新增 map.delete("k1"); // delete是Map的一个API
有两个问题:
前面说了,Go语言中一共有5种定义map的方式,上面的写法只是其中一种。
Java是面向对象的,Map只是接口,可以有HashMap、TreeMap、LinkedHashMap等多种实现方式。而Go语言中,将map作为了语法关键字,这样通用性、灵活性大打折扣,想要实现TreeMap那就不能用传统map的用法了。
Java和TypeScript是将 接口(方法)定义在对象上的,可扩展性非常强,例如Map有很多API:remove(key, value),replace(key, value)。而Go,只有delete(key),没有delete(key, value),也没有 replace(key,value),这是有本质区别的。
我对问题1,是反感,对第2、3问题,则是忧虑(十分忧虑)。
Go语言中的字符串
Go语言中的字符串,是由“字节”数组构成,这完全不同于其他语言。例如在Java中,字符串是由“字符”数组组成。
一个是“字节”,一个是“字符”。显然,我觉得“字符”更好。所谓“字符”,其实是“Unicode Character Representations”(Unicode字符表示法),它是常用的字符标准,在程序内部可以完美的工作。用“字节”在程序内部表示字符串,编程者更难用,而且也没必要。所以,Go为了方便大家使用,新增了内置类型“rune”(int32的别名,配合格式化符c%,可以转换成字符类型)。下面的例子,说明了问题:
// Go语言中,这种打印字符串的方式,得到的结果可能是错误的 func printChars(s string) { for i:= 0; i < len(s); i++ { fmt.Printf("%c ",s[i]) } } // 要换成下面的rune方式 func printChars(s string) { runes := []rune(s) for i:= 0; i < len(runes); i++ { fmt.Printf("%c ",runes[i]) } } // 而Java等其他语言中,通常直接打印char即可 void printChar(String s) { for (Character c : s.toCharArray()) { System.out.print(c); } }
获取字符串的长度,不能使用len(因为len获取的是字节长度),需要用utf8.RuneCountInString(string)函数(是不是很抓狂?),数字与字符串相加,不能自动转换类型,得转换后才能用:
var l = len(s) // 获取字符串长度,错误的方法 fmt.Println(l) l = utf8.RuneCountInString(s) // 正确的方法 fmt.Println(l) var port = 8080 // int var redio = 3.14 // float64 // Java字符串相加 String str = "price:" + redio + port; // Go字符串相加 var str = "price:" + strconv.FormatFloat(redio, 'E', -1, 64) + strconv.Itoa(port) // 爽不爽?
Go语言的其他问题
iota这个特殊的值,很难用
缺少标准容器,例如队列、堆栈等
没有泛型,(泛型这个功能,我认为还是很不错的)
缺乏简单的通用资源(而不仅仅是内存)
不适合面向对象的编程,继承和多态都十分难用!
方法有指针接收者和值接收者两种写法,值接收者涉及到隐式内存拷贝,是个坑,没必要。
指针让人头疼,弊大于利。
Go的代码运行时调试非常垃圾
Go的异常处理(panic、recover)机制非常垃圾
函数参数问题(多个问题,见后文)
后面有时间再详细来分析这些。总的来说,个人感觉:不能拿Go跟其他高级语言相比,仅仅与C语言比,Go还是有一点先进的,即使和其他高级语言比,Go也有一些可取之处。另外,个人感觉Go不适合大型企业级应用开发,但是作为C语言的升级版,作为中间件及运维工具的开发语言,还是可以的,比起Python、Ruby都有优势,日常写一些微小的Web项目也还凑合。
7.指针问题
下面程序 perimeter(r) 会报错,而 r.perimeter() 则不会。一个接受指针为参数的函数不能接受一个值作为参数,但是r.perimeter() 却可以,这是因为这一行将被 Go 解析为 (&r).perimeter()。这是为了方便 Go 给我们提供的语法糖。
package main import ( "fmt" ) type rectangle struct { length int width int } func perimeter(r *rectangle) { fmt.Println("perimeter function output:", 2*(r.length+r.width)) } func (r *rectangle) perimeter() { fmt.Println("perimeter method output:", 2*(r.length+r.width)) } func main() { r := rectangle{ length: 10, width: 5, } p := &r //pointer to r perimeter(p) p.perimeter() /* cannot use r (type rectangle) as type *rectangle in argument to perimeter */ //perimeter(r) r.perimeter()//calling pointer receiver with a value }
10.函数参数问题
问题2:go语言可变参数的坑
我们是否需要三元运算符?
正方、反方都有很多论点,比如Rust语言的这个issues就有很大人讨论:
https://github.com/rust-lang/rfcs/issues/1362
理性的思考后,我觉得,三元运算符确实和if-else的语义重复,而且当三元运算符嵌套时,非常难看。因此,基于我上文所说的原则,三元运算符 确实没必要。只不过大家在其他语言中用习惯了而已,但三元运算符也并不是很常用。
说了这么多缺点,说一下Go的部分优点吧
switch-case循环,去掉了令人苦恼的break,换成了fallthrough,这个设计很棒!case多个值也很方便。
在强类型的语言中,函数还能做到有多返回值,优秀!(但是和TypeScript这种可以随处自定义对象 {a:1, b:2}比,Go还是显得不够方便)
统一了for循环的方式(但是for循环的形式也并不是很好)
下面对我熟悉的各种语言的for循环做一个对比,至于优劣,自己感受:
第一种情形,带索引:
// Go语言的for循环用法: for...range for key, val := range arr { Println(key, val) } // JavaScript(ES6):forEach arr.forEach((val, key) => Println(key, val)) // CoffeeScritp的for循环用法:for...in Println key, val for key, val in arr
第二种情形,不带索引:
// Go语言的for循环用法 for _, val := range arr { Println(val) } // JavaScript(ES6):for...of for(val of arr) { Println(val) }
显然,第二种更优。
第三种情形,只根据条件进行循环:
// Go for i := 0; i <= 10; i++ { Println(i) } // JavaScript for(var i = 0; i <= 10; i++) { Println(i) }
总结:
1、for作为一个通用语法关键字,是有必要的,比 forEach() 这种函数要更好、更通用。
2、for的用法,通常只有上面三种场景,对于第1、2种,可以统一为:
for(val in arr) { Println(val) } for(val, key in arr) { // 类似于CoffeeScript的用法 Println(val) }
但是Go语言选择了使用 “ for ? := range ? ” 句式,显得冗余(:=range),但总体思路是对的(统一化)。
对于第3中场景,使用 “for ?;?;? {}" 句式,是大多数编程语言通用的。三个"?"分别代表:初始局部变量、循环开始的判断条件(boolean)、循环结束执行代码”。这个套路是对的,但是语法设计不够优美、灵活。最后一步“循环结束执行代码”,其实可以放在循环体中,下面是我设计的循环方式:
for (var i = 0) when (i <= 10) { Println(i); i++; }
支持复杂的场景:
for ( var pos = 0; var count = getCountFromFile(pos); ) when(pos<16 && count!=1) { Println(count); // 每次循环结束时执行 pos++; count = getCountFromFile(pos); }
这个循环,按照Go语言的写法,只能为:
var pos = 0; var count = getCountFromFile(pos); for pos<16 && count!=1 { Println(count); // 每次循环结束时执行 pos++; count = getCountFromFile(pos); }
显然,前者更好,Go语言的写法要差一些,原因有两个:
Go的for语句不能完美定义“循环体内使用的局部变量”,导致变量被提到了循环体之外;
“ for x=10 when(x<10) {} ” 这种语句更自然:for后面跟循环的对象,when后面跟循环的判断条件。对比之下,Go的这种“for 跟判断条件”句式,看着像是 if语句,逻辑上不自然。
让人烦恼的Go语言中的数字类型
因为业务需要,我写了如下一段代码:
var x = 1000 var w = int(int(math.Floor(math.Log10(float64(x)) + 1)))
没办法,最简单的方式写出来就这样。
1宗罪:程序员都知道,括号的层级嵌套多了,体验非常不好,而Go语言中,类型转换需要把值包裹起来,于是括号嵌套就可能非常多!
而换成其他语言,比如Java,就简洁很多:
int x = 1000 int w = (int) Math.floor( Math.log10(x) + 1);
2宗罪:注意到上述代码,两者都是强类型语言,但是却有一个差别,Java数字类型是可以自动向上转型的(例如int是可以赋值给float变量的,因为float变量范围大于int),而go语言不能,得傻不拉几的手动写一个转换。
极容易采坑的nil及隐式类型转换
nil转换有时候是不合常理的,示例如下:
func GetString() *String { return nil } func CheckString(s Stringer) bool { println(s) return s == nil } var ss *String = GetString() println(ss) // 输出Nil的地址:0x0 println(ss == nil) // true println(CheckString(nil)) // (0x0,0x0) true println(CheckString(ss)) // (0xb3f820,0x0) false
注意到:ss == nil,但是 CheckString(ss) 跟 CheckString(nil) 的值却不一样,太无语了!!(原因是ss是动态类型,nil是静态类型,ss的实际值为*String(nil),在静态编译阶段,CheckString(ss) 中的ss就被转换成了Stringer(nil)!)
这个坑,很容易在 if err != nil {} 判断的时候遇到。参见这篇文章:golang的类型转换的坑和分析
关于指针和闭包
几句话说不清,直接说结论吧:
1、不得不说,像Java这种屏蔽掉“指针”是很好的做法,指针真的会增加程序的复杂度、降低可维护性,而且Java也证明,没有指针语法,并不是坏事。
2、闭包也是语言的副作用产品,很难用,看下面例子:
func test() []func() { var funs []func() for i:=0;i<2;i++ { funs = append(funs,func() { println(&i,i) }) } return funs } func main(){ funs:=test() for _,f:=range funs { f() } }
这个例子中,闭包是for循环完后的值,即都为2。再看下面例子:
func test(x int)(func(),func()) { return func() { println(x) x+=10 }, func() { println(x) } } func main() { a,b:=test(100) a() b() }
输出结果是 100、110。闭包的神奇在于,它使得值类型变量,与函数体绑定,在多个函数中共享,就好像是引用变量一样。
再看Java的设计,好贴心,屏蔽掉了“闭包”。它是怎么做的呢?它规定,“闭包”内的外部变量,必须是final类型。例如:
interface User { int getAge(); } public void test() { int x = 100; User u1 = new User() { @Override public int getAge() { x += 10; return x; } }; User u2 = new User() { @Override public int getAge() { return x; } }; }
这段代码编译会报错,闭包内不能改变x的值!!x为final类型变量(JDK1.8以下需要显示的声明final)。
同样,事实也再一次证明,没有闭包和指针的编程语言,例如Java,也是一样强大,闭包和指针并不是什么好东西。上面这个例子可以修改成:
public void test() { int x = 100; User u1 = new User() { int x; @Override public int getAge() { x += 10; return x; } User set(int x) { this.x = x; return this; } }.set(x); }
任何闭包的写法,都可以用这种 set/get 值的方式来透明的传递值。