不可变减少副作用

栏目: 编程语言 · 发布时间: 5年前

内容简介:在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中许多一些典型烦心的缺陷。因为改变越多,就需要越多的测试来确保导致变化的做法是正确的。通过严格限制改变来隔离变化的发生,那么错误的发生在更小的空间,需要测试的地方也就更少。

而函数式认为可变是万恶之源,不可变的好处是使得开发更加简单,测试友好,减少了任何可能的副作用。做一名传统的面向对象语言的开发人员,我们更要吸纳函数式语言的特点,在代码尽可能让变量不可变,对象不可变,来提升我们代码中的可读性与安全性。


以上所述就是小编给大家介绍的《不可变减少副作用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Effective C++中文版

Effective C++中文版

[美] Scott Meyers / 侯捷 / 华中科技大学出版社 / 2001-9 / 49.80元

Effective C++是世界顶级C++大师Scott Meyers的成名之作,初版于1991年。在国际上,这本书所引起的反响之大,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍的推荐名单上,这部专著都会位于前三名。作者高超的技术把握力,独特的视角、诙谐轻松的写作风格、独具匠心的内容组织,都受到极大的推崇和仿效。 书中的50条准则,每一条都扼要说明了一个可让你写出更好的C+......一起来看看 《Effective C++中文版》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具