内容简介:在JVM系统语言如Scala与Kotlin中有两个关键字定义变量为什么新的语言需要强调变量Rust在mutable(可变)与immutable(不可变)上相比Scala上更进了一步:
可变与不可变
在JVM系统语言如Scala与Kotlin中有两个关键字定义变量
- var是一个 可变 变量,可以通过重新分配来更改为另一个值的变量
- val是一个 只读 变量,创建的时候必须初始化,以后不能再被改变
为什么新的语言需要强调变量 不可改变
? 我再来看一下Rust语言中的变量不可改变。
- let,采用此关键字来绑定变量,变量默认不可变
- let mut,采用此关键字来绑定可以变更的变量
Rust在mutable(可变)与immutable(不可变)上相比Scala上更进了一步:
- Scala的val只能约束了同一个变量名不可再重新赋值,变量绑定的对象是可以改变的(如val的list对象,可以调用它的append方法修改对象内容)
- Rust通过借用(borrow)语义与mut关键字,约束了只有声明为 mut 的变量,才能对绑定的对象是进变更(如只有是mut的vec对象,才能调用它的push方法修改其内容)
小结一下,关于var、val与mutable、immutable的区别:
绑定
可变的问题
对于变量声明,var相比val有如下问题:
- 分支遗漏:var变量多个地方重用,可能存在某个分支遗漏修改,导致代码逻辑错误
- 未初始使用:变量可能会在使用前没有初始化的代码,会导致空指针异常
- 可读性变差:阅读代码时,确定变量的值是比较困难,因为存在不同的地方对它可能的修改
在编程中我们更希望是对象是immutable(不可变)的,简言之:
- mutable:对象的内部数据可变,变化就会引入风险
- immutable:对象的内部数据的不可变导致其更加安全,可以用作多线程的共享对象而不必考虑同步问题
不可变其实是函数式编程相关的重要概念,函数式编程中认为可变性是万恶之源,因为可变性的对象会给程序带来“副作用”;函数式编程也认为: 只有纯的没有副作用的函数,才是合格的函数。
什么是“副作用”:
在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。--维基百科
函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。严格的函数式语言要求函数必须无副作用。
而面向对象语言中虽强调对象的封装性,但没有在语义上强制约束对象的不可变性。面向对象的编程通过 封装可变 的部分来构造能够让人读懂的代码,函数式编程则是通过最大程度地 减少可变 的部分来构造出可让人读懂的代码。
函数式风格
Scala与Kotlin鼓励使用val,变量只是只读,使代码像函数式风格。我们来一个简单的Scala例子:
def printArgs(args: Array[String]): Unit = { var i = 0 while (i < args.length) { println(args(i)) i += 1 } }
可以通过去掉var的办法把这个代码变得偏函数式风格:
def printArgs(args: Array[String]): Unit = { args.foreach(println) }
很显然,重构后的代码比原来的代码更简洁明了,也更少机会犯错。因为它消除了var变量,也消除了var变量上述可能导致的问题。
当然它并不是纯函数式的,因为它有副作用,其副作用是打印到标准输出流。如果某个函数不返回任何值,就是说其结果类型为Unit,那么这个函数唯一能让其有点儿变化的办法就是通过某种副作用。而函数式的方式应该是定义对需打印的arg进行格式化的方法,但是仅返回格式化之后的字串。
def formatArgs(args: Array[String]) = args.mkString("\n") val res = formatArgs(Array("zero", "one", "two")) println(res)
回到Java
Java中的String类的对象都是典型的immutable数据类型,一个String对象一旦被new出来,其代表的数据便不可被重新修改。
对于变量是否可以重新赋值,Java采用final关键字,同时被final修饰的方法不能被重写,他们也都强制变量或方法不可变性。Java还有一种用法,匿名内部类用的变量必须final,为用什么要有这种约束?
是为了保护数据安全和代码稳定,Java通过类的封装规范了类与类之间的访问权限,而内部类却打破了这种规范。它可以直接访问自身所在的外部类里私有成员,而且自身还可以创建相同的成员(另一个有意思的问题,变量遮蔽Shadow)。从作用域角度看,内部类的新成员修改了什么值,外部方法也是不知道,因为程序的运行由外而内的,所以外部根本无法确定内部这时到底有没有这个东西。综上所述,选择final来修饰外部方法的成员,让其引用地址保持不变、值也不能被改变保证了外部类的稳定性。
多使用final
除了匿名内部类用的变量必须final有这种约束,Java没有其它的语法上强约束不变性。我们还是可以善用不可变性的特点,来减少由可变带来的风险,提升代码的安全性与健壮性。
建议多使用final让对象不可变、让变量不可变:
- 类的域值不可变:尽可能把成员变量声明成final,对于构造方法传入外部参数,若此参数是直接赋值给成员变量,那把此声明final;在构造方法中能通过计算初始化的成员变量,那把此声明final。
- 类与方法不可变:将类或方法声明为final,这样就不会重写它,不允许将类子类化,也不会存在子类来修改父类的成员变量与方法。Kotlin直接在语言上就遵循了这一条最佳实践,Kotlin中的类默认是final的,若想能子类化,则必须声明为open。
- 返回值不可变:对于成员变量的getter方法,其返回值尽可能是新对象,防止外部直接修改内部数据。如返回list类型的成员变量,不是直接返回其引用,而是直接再new一个list对象,拷贝成员变量的值,因为外部直接引用的修改,内部不感知
- 参数变量不可变:对于方法的输入参数,我们尽可能地通过final修饰,避免在方法内对入参重新赋值操作。
- 局部变量不可变:对于局部变量,尽可能地通过final修饰,避免不同的分支对变量多次赋值操作。
函数式编程
函数式编程是 java 8的一大特色,说到函数式编程,就不得不提及流Stream。
Stream其中有一个特点:它不会改变原集合,它是一堆元素顺序或者并行执行我们串起来的函数,函数并不会对集合中的元素造成影响。对Stream的使用就是实现一个filter-map-reduce过程,这个过程我也叫做聚合操作,产生一个最终结果。
final List<Integer> nums = Arrays.asList(1, 2, 3, 4); final Integer sum = nums.stream() .filter(n -> n % 2 == 0) .map(n -> n * n) .reduce(0, Integer::sum);
正如上面的代码,我们对nums重新聚合,新的结果sum并没有对原有nums产生副作用。同时我们都可以把两个变量都声明为final,不需要对变量进行改变。
结语
不可变可以摈弃Java中许多一些典型烦心的缺陷。因为改变越多,就需要越多的测试来确保导致变化的做法是正确的。通过严格限制改变来隔离变化的发生,那么错误的发生在更小的空间,需要测试的地方也就更少。
而函数式认为可变是万恶之源,不可变的好处是使得开发更加简单,测试友好,减少了任何可能的副作用。做一名传统的面向对象语言的开发人员,我们更要吸纳函数式语言的特点,在代码尽可能让变量不可变,对象不可变,来提升我们代码中的可读性与安全性。
以上所述就是小编给大家介绍的《不可变减少副作用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 副作用与序列点——C语言
- JavaScript整洁代码-函数参数和副作用
- 如何使用纯函数式 JavaScript 处理脏副作用
- 集合对象可变与不可变的那点事
- Python中不可变数据类型和可变数据类型
- C 可变长参数 VS C++11 可变长模板
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Effective C++中文版
[美] Scott Meyers / 侯捷 / 华中科技大学出版社 / 2001-9 / 49.80元
Effective C++是世界顶级C++大师Scott Meyers的成名之作,初版于1991年。在国际上,这本书所引起的反响之大,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍的推荐名单上,这部专著都会位于前三名。作者高超的技术把握力,独特的视角、诙谐轻松的写作风格、独具匠心的内容组织,都受到极大的推崇和仿效。 书中的50条准则,每一条都扼要说明了一个可让你写出更好的C+......一起来看看 《Effective C++中文版》 这本书的介绍吧!