《深入理解Scala》——第2章,第2.2节优先采用面向表达式编程

2.2 优先采用面向表达式编程
深入理解Scala
面向表达式编程是个术语,意思是在代码中使用表达式而不用语句。表达式和语句的区别是什么?语句是可以执行的东西,表达式是可以求值的东西。在实践中这有什么意义呢?表达式返回值,语句执行代码,但是不返回值。本节我们将学习面向表达式编程的全部知识,并理解它对简化程序有什么帮助。我们也会看一下对象的可变性,以及可变性与面向表达式编程的关系。
作者注:语句VS表达式
语句是可以执行的东西,表达式是可以求值的东西。
表达式是运算结果为一个值的代码块。Scala的一些控制块也是表达式。这意味着如果这个控制结构是有分支的,那么每个分支也必须被计算为一个值。if语句就是个极佳的例子。if语句检查条件表达式,然后根据条件表达式的值返回其中一个分支的结果。我们来看个简单的REPL会话:

如你所见,Scala的if块是个表达式。我们的第一个if块返回5,也就是表达式true的结果。第二个if块返回hello,也就是表达式false的结果。要在Java里达到类似的目的,你得用下文所示的?:语法:

因此Java里的if块和?:表达式的区别在于if不被运算为一个值,Java里你不能把if块的结果赋值给一个变量。而Scala统一了?:和if块的概念,所以Scala里没有?:语法,你只需要用if块就够了。这只是面向表达式编程的开始,实际上,Scala绝大部分语句都返回其最后一个表达式的值作为结果。

2.2.1 方法和模式匹配
面向表达式编程挑战了其他语言的某些好的实践。用Java编程时,有个常用的实践是每个方法只有一个返回点。这意味着如果方法里有某种条件逻辑,开发者会创建一个变量存放最终的返回值。方法执行的时候,这个变量会被更新为方法要返回的值。每个方法的最后一行都会是个return语句。我们来看个例子。

如你所见,result变量用来存放最终结果。代码流过一个模式匹配,相应地设置出错字符串,然后返回结果变量。我们可以用模式匹配提供的面向表达式语法稍微改进一下代码。事实上,模式匹配上返回一个值,类型为所有case语句返回的值的公共超类。如果一个模式都没有匹配上,模式匹配会抛出异常,确保我们要么得到返回值要么出错。我们把上面的代码翻译成面向表达式的模式匹配实现。

你应该注意到两件事。首先,我们把result变量改成了val,让类型推导来判断类型。因为我们不在需要在赋值后改变result的值,模式匹配应该能够判断唯一的值(和类型)。所以我们不仅减少了代码的大小和复杂度,我们还增加了程序的不变性。不变性(immutability)是指对象或变量赋值后就不再改变状态,可变性(mutability)是指对象或变量在其生命周期中能够被改变或操纵。我们将在下一节探讨可变性和面向表达式编程。你经常会发现面向表达式编程和不变对象合作无间。
我们做的第二件事是去掉了case语句里的所有赋值。case语句的最后一个表达式就是case语句的“结果”。我们可以在每个case语句里嵌套更深的逻辑,只要在最后能得到某种形式的表达式结果就行。如果我们不小心忘了返回结果,或者返回结果不对,编译器也会警告我们。
代码看上去已经简洁多了,不过我们还可以再改进一点。用Scala开发时,大部分开发者会避免在代码里使用return语句,而更喜欢用最后一句表达式作为返回值(这也是所有其他面向表达式语言的风格)。实际上,对于createErrorMessage方法,我们可以完全去掉result这个中间变量。我们看下最后改进的结果。

你注意到我们甚至没为这个方法开个代码块吗?模式匹配是这个方法唯一一个语句,而它返回个字符串类型的表达式。我们完全把这个方法转化为了面向表达式的语法。注意到现在代码变得简洁得多,表达力也强多了吗?同时请注意,如果有任何类型不匹配或者无法走到的(unreachable)case语句,编译器会警告我们。

2.2.2 可变性
面向表达式编程一般与不变性编程(immutable programming)搭档得很好,但是与可变对象协作就没那么好了。不变性是个术语,拿对象来说,一旦对象构造完毕,其状态就不再改变。面向表达式编程和可变性(也就是对象在其生命周期中可以改变状态)混搭的时候,事情就变得复杂了一点。因为使用可变对象的代码一般倾向于用命令式(imperative)的风格编码。
命令式编码可能是你以前熟悉的风格。很多早期语言,如C、Fortran和Pascal都是命令式的。命令式代码一般由语句构成,而不是表达式。先创建对象,设定状态,然后执行语句,而语句会“操纵”或改变对象的状态。对那些没有对象的语言也是一样,只不过改成了操纵变量和结构。我们来看个命令式编码的例子。

注意看这里构造了一个Vector,然后通过magnify方法操纵其状态。而面向表达式的代码喜欢让所有的语句返回某个表达式或值,magnify方法也不例外。在这个操纵对象的例子里,应该返回什么值呢?一个选择是返回刚被操纵过状态的对象。

乍看上去这是个很棒的选择,但实际上有严重的缺陷。尤其难以判断对象的状态是什么时候被改变的,在跟不变对象混用时缺陷就更明显。假设Vector2D的 - 方法符合数学上的定义,请你试试看能否判断出下面这段代码在结束时会打印出什么值?

最后一句表达式的结果是什么呢?第一眼看上去结果应该是vector(3.0,3.0)减去vector(6.0,0.0),也就是(-3.0,3.0)。然而这里面每个变量都是可变的,也就是说变量的值是按照操作顺序修改的。我们来演算一下实际编译的结果。首先x,vector(1.0,1.0)被放大3倍变成了(3.0,3.0)。然后我们用x减y,x变成了(2.0,4.0)。为什么?因为 -方法右边的代码要先计算,其中(x-y)要先计算。接着我们再把x放大3倍,变成了(6.0,12.0)。最后我们用x减去x自己,结果是(0.0,0.0),你没看错,x自己减自己。为什么?因为减号左边的表达式和减号右边的表达式都是x变量开头的。因为我们使用可变对象,也就说每个表达式最后返回x自身。所以不管我们做什么,我们最后都是调用x-x,结果就是vector(0.0,0.0)。
因为存在这种混淆性,在用面向表达式编程时最好使用不可变对象。尤其在有操作符重载的场合下,比如上例。而在有些场景下可变性和面向表达式编程也可以合作得很好,尤其是在使用模式匹配或if语句时。
编码时一个常见的任务是根据某个值查找某个对象的值。这些对象可以是可变的,也可以是不变的。而面向表达式编程可以发挥作用的地方是简化查找。我们来看个简单的例子:根据用户点击的菜单按钮查找需要执行的操作。当按下菜单按钮的时候,我们从事件系统接受到一个事件。这个事件里有哪个按钮被按下的标记。我们要执行某种操作并返回状态。我们看下面的代码。

注意看我们是怎么就地操纵对象并返回结果的。我们没有明确地用return语句,而是简单地写下我们打算返回的表达式。你可以看到这样的代码比创建一个用于存放返回值的变量来得简洁。也可以看到在表达式里混入操纵状态的语句导致代码的清晰性有所降低。这是我们更推崇不变性代码的原因之一,也就是下一节的主题。
面向表达式编程可以减少样板代码(boiler plate),使代码更优雅。其做法是让所有语句返回有意义的值,这样就可以减少代码的凌乱,增加代码的表达力了。现在是时候学习为什么我们要关注不变性了。

时间: 2024-10-03 19:57:41

《深入理解Scala》——第2章,第2.2节优先采用面向表达式编程的相关文章

《深入理解Scala》——导读

目 录 第1章 Scala--一种混合式编程语言1.1节Scala一种混合式编程语言 1.2 当函数式编程遇见面向对象 1.3 静态类型和表达力 1.4 与JVM的无缝集成 1.5 总结 第2章 核心规则 2.1 学习使用Scala交互模式(REPL)2.2 优先采用面向表达式编程 2.3 优先选择不变性2.4 用None不用null2.5 多态场景下的判等2.6 总结 第3章 来点样式-编码规范 第4章 面向对象编程 第5章 利用隐式转换写更有表达力 第6章 类型系统 第7章 隐式转换和类型系

《深入理解Scala》——第2章,第2.1节学习使用Scala交互模式(REPL)

第2章 核心规则深入理解Scala 本章包括的内容: • 使用Scala交互模式(Read Eval Print Loop 简称REPL) • 面向表达式编程 • 不变性(Immutability) • Option类 本章内容覆盖了每个新Scala开发者都需要知道的几个主题.本章不会深入到每个主题里,但是会讲到可以让你自己去接着探索的程度.你将学会使用REPL,学会如何利用这个工具做软件的快速原型开发.然后我们会学到面向表达式编程,并从另一个视角来看控制结构是怎么回事.在此基础上,我们来研究不

《深入理解Scala》——第2章,第2.6节总结

2.6 总结 深入理解Scala 本章中我们了解了Scala编程时的第一个关键组成部分.利用REPL做快速原型是每个成功的Scala开发者必须掌握的关键技术之一.面向表达式编程和不可变性都有助于简化程序和提高代码的可推理性.Option也有助于可推理性,因为它明确声明了是否接受空值.另外,在多态的场景下实现好的判等可能不容易.以上这些实践可以帮助我们成功踏出Scala开发的第一步.要想后面的路也走得顺利,我们就必须来看一下编码规范,以及如何避免掉进Scala解析器的坑.

《深入理解Scala》——第1章,第1.4节与JVM的无缝集成

1.4 与JVM的无缝集成 深入理解Scala Scala的吸引力之一在于它与Java和JVM的无缝集成.Scala与Java有很强的兼容性,比如说Java类可以直接映射为Scala类.这种紧密联系使Java到Scala的迁移相当简单,但在使用Scala的一些高级特性时还是需要小心的,Scala有些高级特性是Java里没有的.在Scala语言设计时已经小心地考虑了与Java无缝交互的问题,用Java写的库,大部分可以直接照搬(as-is)到Scala里. 1.4.1 Scala调用Java 从S

《深入理解Scala》——第1章,第1.3节静态类型和表达力

1.3 静态类型和表达力 深入理解Scala 开发人员中有一个误解,认为静态类型必然导致冗长的代码.之所以如此是因为很多继承自C的语言强制要求程序员必须在代码中多处明确地指定类型.随着软件开发技术和编译器理论的发展,情况已经改变.Scala利用了其中一些技术进步来减少样板(boilerplate)代码,保持代码简洁. Scala做了以下几个简单的设计决策,以提高代码表达力. • 把类型标注(type annotation)换到变量右边. • 类型推断. • 可扩展的语法. • 用户自定义的隐式转

《深入理解Scala》——第1章,第1.5节总结

1.5 总结 深入理解Scala 本章中,你学到了一些Scala的设计理念.设计Scala的初衷在于把不同语言中的多种概念融合起来.Scala融合了函数式和面向对象编程,尽管显然Java也已经这么做了.Scala精选其语法,极大地减少了语言中的繁冗之处,使一些强大的特性可以优雅地表达,比如类型推断.最后,Scala和Java能够紧密集成,而且运行在Java虚拟机上,这或许是让Scala变成一种实用选择的最重要的一点.几乎不花代价就可以把Scala用于我们的日常工作中. 因为Scala融合了多种概

《深入理解Scala》——第1章,第1.1节Scala一种混合式编程语言

第1章 Scala--一种混合式编程语言 Scala是一种将其他编程语言中的多种技巧融合为一的语言.Scala尝试跨越多种不同类型的语言,给开发者提供面向对象编程.函数式编程.富有表达力的语法.静态强类型和丰富的泛型等特性,而且全部架设于Java虚拟机之上.因此开发者使用Scala时可以继续使用原本熟悉的某种编程特性,但要发挥Scala的强大能力则需要结合使用这些有时候相互抵触的概念和特性,建立一种平衡的和谐.Scala对开发者的真正解放之处在于让开发者可以随意使用最适合手头上的问题的编程范式.

《深入理解Scala》——第1章,第1.2节当函数式编程遇见面向对象

1.2 当函数式编程遇见面向对象 深入理解Scala 函数式编程和面向对象编程是软件开发的两种不同途径.函数式编程并非什么新概念,在现代开发者的开发工具箱里也绝非是什么天外来客.我们将通过Java生态圈里的例子来展示这一点,主要来看Spring Application framework和Google Collections库.这两个库都在Java的面向对象基础上融合了函数式的概念,而如果我们把它们翻译成Scala,则会优雅得多.在深入之前,我们需要先理解面向对象编程和函数式编程这两个术语的含义

《深入理解Scala》——第2章,第2.3节优先选择不变性

2.3 优先选择不变性 深入理解Scala 编程中的不变性指对象一旦创建后就不再改变状态.这是函数式编程的基石之一,也是JVM上的面向对象编程的推荐实践之一.Scala也不例外,在设计上优先选择不变性,在很多场景中把不变性作为默认设置.对此,你可能一下子会不适应.本节中,我们将学到不变性对于判等问题和并发编程能提供什么帮助. Scala里首先要明白的是不变对象和不变引用(immutable referene)的区别.Scala里的所有变量都是指向对象的引用.把变量声明为val意味着它是个不变"引