不可变减少副作用

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

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

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


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

查看所有标签

猜你喜欢:

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

构建之法(第三版)

构建之法(第三版)

邹欣 / 人民邮电出版社 / 2017-6 / 69.00元

软件工程牵涉的范围很广, 同时也是一般院校的同学反映比较空洞乏味的课程。 但是,软件工程 的技术对于投身 IT 产业的学生来说是非常重要的。作者有在世界一流软件企业 20 年的一线软件开 发经验,他在数所高校进行了多年的软件工程教学实践,总结出了在 16 周的时间内让同学们通过 “做 中学 (Learning By Doing)” 掌握实用的软件工程技术的教学计划,并得到高校师生的积极反馈。在此 ......一起来看看 《构建之法(第三版)》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具