├── .gitignore
├── .idea
└── uiDesigner.xml
├── LICENSE
├── README.md
├── build.sbt
├── doc
├── 0 Scala命令行详解.markdown
├── 1 Scala中的类.markdown
├── 12 Scala中的常用Monad类型实战.markdown
├── 13 Scala中的异步编程之 Future.markdown
├── 2 Scala中的对象.markdown
├── 4 Scala中的特质Trait.markdown
├── 5 Scala中的类型参数.markdown
├── 6 Scala中的隐式转换.markdown
└── 7 Scala中的集合.markdown
└── src
├── main
└── scala
│ ├── chapter0
│ ├── HelloWorld.scala
│ └── Sample.scala
│ ├── chapter1
│ ├── CompositionAndInheritance.scala
│ ├── CreateClass.scala
│ └── Rational.scala
│ ├── chapter13
│ └── future
│ │ └── FutureInAction.scala
│ ├── chapter4
│ └── traits
│ │ ├── TraitWithAbstractField.scala
│ │ ├── TraitWithPreDefinition.scala
│ │ └── TraitWithSpecificFields.scala
│ └── chapter7
│ └── collections
│ └── stream
│ └── MyStream.scala
└── test
└── scala
└── chapter13
└── future
└── TheMostCommonUseCase$Test.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | # Intellij
3 | .idea/
4 | *.iml
5 | *.iws
6 | .idea
7 |
8 | # Mac
9 | .DS_Store
10 |
11 | # Maven
12 | log/
13 | target/
14 | *.class
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scala-in-practice
2 | 通过实例来演示Scala中的各种特性!
3 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 |
2 | name := "scala-in-practice"
3 |
4 | version := "1.0"
5 |
6 | scalaVersion := "2.11.8"
7 |
8 | resolvers ++= Seq(
9 | "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
10 | "Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases",
11 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
12 | )
13 |
14 | fork := false
15 |
16 | libraryDependencies += "commons-io" % "commons-io" % "2.4"
17 |
18 | libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "0.9.5"
19 |
20 | libraryDependencies += "com.github.scala-blitz" %% "scala-blitz" % "1.2"
21 |
22 | libraryDependencies += "com.netflix.rxjava" % "rxjava-scala" % "0.19.1"
23 |
24 | libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "1.0.1"
25 |
26 | libraryDependencies += "org.scala-stm" %% "scala-stm" % "0.7"
27 |
28 | libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.2"
29 |
30 | libraryDependencies += "com.typesafe.akka" %% "akka-remote" % "2.3.2"
31 |
32 | libraryDependencies += "com.storm-enroute" %% "scalameter-core" % "0.6"
33 |
34 | libraryDependencies += "org.scalaz" %% "scalaz-concurrent" % "7.0.6"
35 |
36 | libraryDependencies += "com.typesafe.akka" %% "akka-stream-experimental" % "0.4"
37 |
38 | libraryDependencies += "com.storm-enroute" %% "reactive-collections" % "0.5"
39 |
40 | libraryDependencies += "joda-time" % "joda-time" % "2.7"
41 |
42 | libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test"
43 |
--------------------------------------------------------------------------------
/doc/0 Scala命令行详解.markdown:
--------------------------------------------------------------------------------
1 | # 0 Scala命令行详解
2 |
3 | 标签(空格分隔):深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | ### 1. 进入Scala命令行
12 | 在安装Scala后,在命令行中输入scala,即可进入Scala的REPL中。
13 | ```
14 | $ scala
15 | Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_66).
16 | Type in expressions to have them evaluated.
17 | Type :help for more information.
18 |
19 | scala>
20 |
21 | ```
22 |
23 | ### 2. 操作命令行
24 | 而在REPL中进行输入时,必备的几个快捷键可以给你带来更多的便利:
25 | >命令行中的光标移动:
26 | 移动到行首:Ctrl+A
27 | 移动到行尾:Ctrl+E
28 | 删除光标之前的所有字符:Ctrl+U
29 | 删除光标之后的所有字符:Ctrl+K
30 | 一个一个的删除光标之后的字符:Ctrl+D
31 |
32 | >注:上面的快捷键与字母大小写无关
33 |
34 | ### 3. 退出命令行
35 | 从Scala的REPL中退出,两种方式:
36 | >第一种方式: Ctrl+C
37 | 第二种方式: scala> :quit
38 |
39 | ### 4. 命令行中运行Scala脚本文件
40 | Scala的命令行支持两种运行方式:
41 | >命令行交互式方式
42 | 批处理方式(batch mode)
43 |
44 | 创建一个Scala脚本HelloWorld.scala,内容如下:
45 | ```
46 | println("This script is used to testing loading a file in Scala REPL!")
47 | ```
48 | 然后加载这个提前写好的Scala脚本HelloWorld.scala到Scala REPL中并执行(**命令行交互式方式**):
49 | ```
50 | scala> :load HelloWorld.scala // 加载并执行
51 | Loading HelloWorld.scala...
52 | This script is used to testing loading a file in Scala REPL! // 执行结果
53 |
54 | scala>
55 |
56 | ```
57 |
58 | 或者使用批处理方式,就是通过scala命令来运行一个脚本文件:
59 | ```
60 | $ scala HelloWorld.scala
61 | This script is used to testing loading a file in Scala REPL!
62 | ```
63 |
64 | >注:Scala中的脚本文件一般是若干方法或函数的调用,不涉及到继承或调用别的类文件。
65 |
66 | ### 5. Scala中的Shell脚本
67 | Scala中的Shell脚本,创建一个hello.sh文件,内容如下:
68 | ```
69 | #!/usr/bin/env scala
70 | /**
71 | * Running as a Stand-alone Script on Unix-like Systems
72 | *
73 | * $ ./hello.sh world
74 | * $ scala hello.sh world
75 | *
76 | **/
77 | println("Hello " + args(0)) // 接收一个参数
78 | ```
79 | 在该文件的目录下,执行该脚本:
80 | ```
81 | $ ./hello.sh world
82 | Hello world
83 |
84 | $ scala hello.sh world
85 | Hello world
86 |
87 | ```
88 |
89 | ### 6. 编译Scala中的object,class, trait,并运行
90 |
91 | 创建一个Scala object文件,Sample.scala,内容如下:
92 | ```
93 | object Sample extends App {
94 | println("Hello Scala")
95 | }
96 |
97 | ```
98 | 这个并非是一个Scala 脚本,如果以脚本的方式运行,不会有任何的结果输出:
99 | ```
100 | $ scala Sample.scala
101 |
102 | // 无输出
103 | ```
104 |
105 | 要想运行这样的非Scala脚本文件,必须先编译,在运行:
106 | ```
107 | // 在Sample.scala文件的目录下,编译该文件
108 | $ scalac Sample.scala
109 |
110 | // 使用Scala的命令运行
111 | $ scala Sample
112 | Hello Scala
113 |
114 | // 或者使用Java 命令运行
115 | $ java -classpath /usr/local/Cellar/scala/2.11.7/libexec/lib/scala-library.jar:. Sample
116 | Hello Scala
117 |
118 | ```
119 |
--------------------------------------------------------------------------------
/doc/1 Scala中的类.markdown:
--------------------------------------------------------------------------------
1 | # 1 Scala中的类
2 |
3 | 标签(空格分隔): 级别A1:初级程序设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | 本篇主要介绍Scala中的类,及类的构造器,类中的字段/属性,类中的方法/函数,类的继承和类的具体使用场景。
12 | 首先通过例子驱动的方式,介绍类的各种特性及使用时需要注意的“陷阱”。
13 |
14 | ## 1.1 如何创建一个类
15 | 在Scala中通过class关键字来定义一个类。
16 | ```
17 | class Person {}
18 |
19 | object PersonTest extends App {
20 | val person = new Person
21 | }
22 | ```
23 | 然后通过new的方式实例化一个Person的对象。
24 |
25 |
26 | ----------
27 |
28 |
29 | 一个类必须有字段、方法,才能有其实用价值。下面就为该Person类增加字段。
30 | ```
31 | class Person {
32 | val name = "jack" // scala中默认是public
33 | }
34 | ```
35 | val意味着定义的字段是不可变的,上面的类被编译后,会生成一个final字段,并且会提供一个public的getter方法。具体如下:
36 | ```
37 | $ scalac Person.scala
38 |
39 | $ javap -private Person
40 | Compiled from "Person.scala"
41 | public class Person {
42 | private final java.lang.String name; // final的不可变字段
43 | public java.lang.String name(); // 一个public的getter方法
44 | public Person();
45 | }
46 | ```
47 |
48 | ----------
49 |
50 |
51 | 如果为该字段增加private修饰后,则唯一的getter方法就会变为private。
52 | ```
53 | class Person {
54 | private val name = "jack"
55 | }
56 |
57 | $ scalac Person.scala
58 |
59 | $ javap -private Person
60 | Compiled from "Person.scala"
61 | public class Person {
62 | private final java.lang.String name;
63 | private java.lang.String name(); // 一个private的getter方法
64 | public Person();
65 | }
66 |
67 | ```
68 |
69 |
70 | ----------
71 |
72 | 为Person增加一个可变字段。
73 | ```
74 | class Person {
75 | var age = 0; // 定义一个public的可变字段
76 | }
77 | ```
78 | 该类经过编译后,会生成一个私有的age字段,并提供public的getter/setter方法。
79 | ```
80 | $ scalac Person.scala
81 |
82 | $ javap -private Person
83 | Compiled from "Person.scala"
84 | public class Person {
85 | private int age; // 字段变为private
86 | public int age(); // 一个public的getter
87 | public void age_$eq(int); // 一个public的setter
88 | public Person();
89 | }
90 |
91 | ```
92 |
93 |
94 | ----------
95 |
96 |
97 | 但是如果该类的字段也用private修饰后,则getter/setter方法也会变为私有的。如下:
98 | ```
99 | class Person {
100 | private var age = 0; // 定义一个private的字段
101 | }
102 |
103 | $ scalac Person.scala
104 |
105 | $ javap -private Person
106 | Compiled from "Person.scala"
107 | public class Person {
108 | private int age; // 私有字段
109 | private int age(); // 私有的getter
110 | private void age_$eq(int); // 私有的setter
111 | public Person();
112 | }
113 |
114 | ```
115 |
116 |
117 | ----------
118 |
119 |
120 | Scala中,其访问修饰符可以更小粒度的来进行访问作用域的控制。
121 | 比如:private[this],private[包名称]。后者很容易理解,就是在指定的包及其子包内都可访问。
122 | 那么,private[this]呢?它和单独的private修饰到底有什么区别,请看具体代码:
123 | ```
124 | class Person {
125 | private[this] var age = 0; // 定义一个private[this]的字段
126 | }
127 |
128 | $ scalac Person.scala
129 |
130 | $ javap -private Person
131 | Compiled from "Person.scala"
132 | public class Person {
133 | private int age; // 只有private的字段
134 | public Person();
135 | }
136 |
137 | ```
138 | 通过编译后的代码我们发现,如果是private[this]修饰的字段,是压根就不会产生getter/setter方法的。
139 | 下面的例子是private和private[this]在具体使用场景中的主要区别:
140 | ```
141 | class Person {
142 | val name = "jack"
143 | private var age = 0
144 | private[this] var nationality = "china"
145 |
146 | /**
147 | * 在本Person类中接收一个Person类型的参数,并访问该参数对象的字段时,
148 | * 就充分体现了private和private[this]的主要区别
149 | * @param p 一个Person类型的参数
150 | */
151 | def invite(p: Person) = {
152 | // private修饰时,在这种情况下是可以访问的
153 | val sumAge = age + p.age
154 |
155 | // 而用private[this]修饰时,在此种情况下会编译报错: Symbol nationality is inaccessible from this place
156 | val nationalities = s"$nationality + ${p.nationality}"
157 | }
158 | }
159 |
160 | ```
161 | 更具体的分析请看下面的内容。
162 |
163 | ## 1.2 简单类和无参方法
164 |
165 | Scala中最简单的类,形式上和Java的很相似。
166 |
167 | ```
168 | // 不推荐在Scala中这样使用一个类,这是Java Style
169 | class Counter {
170 | private var value = 0 // 字段必须初始化
171 | def increment() { value += 1 } // 方法默认为public, 修改器使用括号(),Use () with mutator
172 | def current = value // 读取器不使用括号(),Don’t use () with accessor
173 | /**
174 | * 首先对两个术语进行说明:
175 | *
176 | * 1. 声明一个方法,就是有方法的名称,参数列表,返回类型等。例如:
177 | * abstract class Hello {
178 | * def get(id: Int): String // 方法声明
179 | * }
180 | *
181 | * 2. 方法定义,也就是方法实现,实现了一个完整的方法。例如:
182 | * class Hello {
183 | * // 方法定义
184 | * def get(id: Int): String = s"id = $id, hello world!"
185 | * }
186 | *
187 | * 对无参方法的定义:
188 | * 上面定义了两个无参方法,increment和current。
189 | * 但是一个使用了括号,而另一个没有使用括号。
190 | *
191 | * 原因是涉及到一个约定俗成的规则:
192 | * 对于改变对象状态的方法【改值器方法】就使用(),比如,increment()改变了对象的状态就使用了括号
193 | * 而对于不改变对象的方法【取值器方法】就使用无括号调用,比如,current只是获得值,没有改变对象的状态,就不使用括号。
194 | *
195 | * 对无参方法的调用:
196 | * 1 如果无参方法有括号,调用时,括号可写可不写
197 | * myCounter.increment
198 | * myCounter.increment()
199 | * 2 如果无参方法无括号,则必须使用无括号调用
200 | * myCounter.current
201 | * myCounter.current() // 编译错误
202 | */
203 | }
204 |
205 | ```
206 |
207 | 在Scala中,类并不声明为public。Scala源文件可以包含多个类,所有这些类都是public的。
208 |
209 | ---
210 |
211 | ---
212 |
213 | ## 1.3 带getter和setter的属性
214 |
215 | - java中的例子:
216 |
217 | ```
218 | public class Person { // 定义一个带有字段age的Java类
219 | public int age; // 声明了一个字段(field),因为没有初始化,所以称为声明,而不是定义。Java中并不推荐这样写
220 | }
221 | ```
222 |
223 | 如果这样使用公有字段的话,任何人都可以随便改变age的值。
224 | 所以,我们更趋向于使用getter和setter方法。
225 |
226 | ```
227 | public class Person { // java推荐写法,带getter和setter的属性age
228 | private int age; // 声明了一个属性(property),
229 | public int getAge() {return age;}
230 | public void setAge(int age) {this.age = age;}
231 | }
232 | ```
233 | >注意称呼的改变,第一个类中age是字段,第二个类中age是属性。
234 | 带有getter和setter的字段就是属性。
235 |
236 | 这样做的好处是什么呢?单纯的看上面的实现,属性除了比字段冗余了两个方法之外,并没有体现其价值啊?
237 | 答案非也。通过对getter和setter的引入,我们加强了对字段的控制权。其体现在:
238 | ```
239 | // 通过对getter和setter的引入,可以在这两个方法里面引入判断逻辑
240 | public void setAge(int newAge) {
241 | if(newAge > age) age = newAge;
242 | }
243 | ```
244 | 如果单纯使用字段,就不能有这样的控制。
245 | 提供属性的话,我们可以在需要的时候进行合理的控制和改进。
246 |
247 | ---
248 |
249 | - Scala中的例子
250 |
251 | **因为scala对每个字段都提供getter和setter方法。所以Scala中的字段就等同于Java中的属性**。
252 | 在这里,我们定义一个公有字段:
253 | ```
254 | class Person {
255 | var age = 0 // 默认是public的
256 | }
257 | ```
258 | Scala生成面向JVM的类,其中有一个私有字段age以及相应的getter和setter方法。
259 | 因为字段age默认是public的,所有这两个方法也是public的。如果定义一个私有的age字段`private var age = 0`,则对应的getter和setter方法也是private的。
260 |
261 | 在Scala中,getter就是age, setter为age_= 。例如:
262 |
263 | ```
264 | $ scalac Person.scala
265 |
266 | $ javap -private Person
267 | Warning: Binary file Person contains scalaclass.Person
268 | Compiled from "Person.scala"
269 | public class scalaclass.Person {
270 | private int age;
271 | public int age();
272 | public void age_$eq(int);
273 | public scalaclass.Person();
274 | }
275 |
276 | // 编译器创建了age和age_$eq方法。(=号被翻译成$eq,是因为JVM不允许在方法名中出现= 。)
277 |
278 | object Person extends App {
279 | val p = new Person
280 | println(p.age) // 0,会调用p.age()
281 | p.age = 10 // 会自动调用setter方法,p.age_= 10
282 | println(p.age) // 10,会调用p.age()
283 | }
284 |
285 | ```
286 |
287 | 可重新定义getter和setter方法:
288 | ```
289 | class Person {
290 | private var privateAge = 0 // 定义私有字段并改名
291 |
292 | def age = privateAge // 自定义getter
293 | def age_=(newAge: Int) = { // 自定义setter
294 | if (newAge > privateAge) privateAge = newAge
295 | }
296 | }
297 |
298 | object Run extends App {
299 | val fred = new Person
300 | println(fred.age) // 0
301 |
302 | fred.age = 21
303 | println(fred.age) // 21
304 |
305 | fred.age = 0
306 | println(fred.age) // 21,新的年龄比当前年龄大才会修改
307 | }
308 | ```
309 |
310 | >说明:
311 | Scala对每个字段都提供getter和setter,但是你也可以通过下面的方式来控制这个过程:
312 | - 如果字段是私有的,则getter和setter方法也是私有的
313 | - 如果字段是val,则只有getter方法生成
314 | - 如果你不需要生成getter和setter方法,则可以将字段声明为private[this]
315 |
316 | ---
317 |
318 | ---
319 |
320 | ## 1.4 只有getter的Scala字段(等同于Java中的属性)
321 |
322 | 对Scala类中只提供只读字段,也就是只有getter方法而没有setter方法。
323 | 该字段在对象构建完成之后就不再改变,则使用val定义。
324 | ```
325 | class Message {
326 | val timeStamp = new java.util.Date // a read-only property with a getter but no setter
327 | }
328 |
329 | $ scalac Message.scala
330 | $ javap -private Message.class
331 | Compiled from "Message.scala"
332 | public class scalaclass.Message {
333 | private final java.util.Date timeStamp;
334 | public java.util.Date timeStamp();
335 | public scalaclass.Message();
336 | }
337 | ```
338 | Scala编译后生成一个private的final字段和一个getter方法,没有setter。
339 |
340 | >总结:在实现字段时,有如下四种选择:
341 | - var foo: Scala会自动合成一个getter和一个setter
342 | - val foo: Scala会自动生成一个getter
343 | - 自定义foo和foo_=方法
344 | - 自定义foo方法
345 |
346 | ---
347 |
348 | ---
349 |
350 |
351 | ## 1.5 关于对象私有字段:
352 |
353 | 在Java中的例子:
354 | ```
355 | public class Counter {
356 | private int value = 0; // 定义一个私有字段,仅仅局限在本类中以各种方式访问
357 | public void increment() {
358 | value += 1; // 方式1: 在本类中直接访问访问
359 | }
360 | public boolean isLess(Counter other) {
361 | return value < other.value; // 方式2: 在本类中通过对象来访问
362 | }
363 | }
364 |
365 | class Test {
366 | public static void main(String[] args) {
367 | Counter c = new Counter();
368 | c.value; // 在Counter之外访问,编译错误
369 | }
370 | }
371 | ```
372 | 在Scala中的例子:
373 | ```
374 | class Counter2 {
375 | private var value = 0 // 通过private关键字定义一个类私有字段,其实private就是private[Counter2]的简写
376 | def increment() { value += 1 } // 在本类中直接访问
377 | def isLess(other: Counter2) = value < other.value // 可以访问另一个对象的私有字段
378 | }
379 |
380 | object Test extends App {
381 | val c2 = new Counter2
382 | c2.value // 在Counter2类之外访问,编译出错
383 | }
384 |
385 | ```
386 | 通过上面的两个例子,private修饰符在Scala和Java中的功效是一样的。
387 | 之所以可以在当前类中访问other.value,是因为other也是一个Counter对象。所以可以访问另一个对象的私有字段。
388 | 如果期望禁止这一点,也就是只允许当前对象可访问,别的Counter对象也不能访问的话,可以使用private[this]来修饰字段:
389 | ```
390 | class Counter {
391 | private[this] var value = 0 // 通过private[this]定义一个对象私有字段,只允许当前对象访问
392 | def increment() { value += 1 }
393 | def isLess(other: Counter) = value < other.value // compile error!!! 别的Counter对象也不能访问
394 | }
395 | ```
396 | 对于`类私有的字段【private修饰的字段】`,Scala生成私有的getter和setter方法。
397 | 但对于`对象私有的字段【private[this]修饰的字段】`,Scala根本不会生成getter和setter方法。
398 |
399 | ---
400 |
401 | ---
402 |
403 | ## 1.6 带有参数的类
404 | Scala中的类是可以带有参数的,需要注意的是,这些参数跟类的主构造器的参数是交织在一起的。也就是类的参数列表跟主构造器的参数列表是一模一样的。引用一个Programming in scala这本书里面的例子:
405 | ```
406 | // 带有两个参数的类
407 | class Rational(n: Int, d: Int) {
408 | require(d != 0)
409 | override def toString = s"$n / $d"
410 |
411 | // 类参数并非是类的字段,所以下面的方法在that.d和that.n的地方会
412 | // 编译出错:Rational.scala:4: error: value d is not a member of Rational
413 | def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d)
414 | }
415 | ```
416 |
417 | 如果为了在定义类参数的时候就自动为该类生成字段的话,很简单,只需要这么来一下即可(注意类参数的修饰符):
418 | ```
419 | class Rational(val n: Int, val d: Int) {
420 | require(d != 0)
421 | override def toString = s"$n / $d"
422 |
423 | // 此时,编译通过
424 | def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d)
425 | }
426 |
427 | ```
428 | 通过val和var修饰的类参数,会自动为该类生成字段。
429 |
430 |
431 | ----------
432 |
433 | ----------
434 |
435 |
436 | ## 1.7 Bean属性
437 | 这是Java Style的写法,不推荐使用,略过!
438 |
439 | ```
440 | import scala.beans.BeanProperty
441 |
442 | class Person(@BeanProperty var name:String)
443 | 这样将会生成四个方法:
444 |
445 | name:String
446 | name_=(newValue:String):Unit
447 | getName():String
448 | setName(newValue:String):Unit
449 |
450 | ```
451 |
452 | ---
453 |
454 | ---
455 |
456 | ## 1.8 辅助构造器
457 |
458 | Scala的类可以有任意多个构造器,但是最重要的就是它的主构造器。
459 |
460 | **其他辅助构造器的名称都为this,每一个辅助构造器必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始**。
461 |
462 | 带有两个辅助构造器的类的例子:
463 | ```
464 | // 类不带参数,既主构造器不带参数
465 | class Person {
466 | private var name = ""
467 | private var age = 0
468 |
469 | def this(name: String) { // 一个辅助构造器
470 | this() // 调用主构造器:就是调用this时,传递的参数跟类参数一致。这里均为空参数。
471 | this.name = name
472 | }
473 | def this(name: String, age: Int) { // 另一个辅助构造器
474 | this(name) // 调用前一个辅助构造器
475 | this.age = age
476 | }
477 | }
478 | ```
479 | 上面的Person类没有显示定义主构造器,在Scala中会自动获得一个无参的主构造器。
480 | 接下来就可以有三种方式来实例化一个Person对象:
481 | ```
482 | val p1 = new Person // 主构造器
483 | val p2 = new Person("Jack") // 第一个辅助构造器
484 | val p2 = new Person("Tony", 30) // 第二个辅助构造器
485 | ```
486 |
487 | ---
488 |
489 | 另一个例子:
490 | ```
491 | // 类带有一个参数,即主构造器也带有一个参数
492 | class Random(val self: java.util.Random) {
493 |
494 | // 第一个辅助构造器
495 | def this() = this(new java.util.Random()) // 调用主构造器:就是调用this时,传递的参数跟类参数一致。这里均为java.util.Random类型
496 |
497 | // 第二个辅助构造器
498 | def this(seed: Long) = this(new java.util.Random(seed)) // 调用主构造器
499 |
500 | // 第三个辅助构造器
501 | def this(seed: Int) = this(seed.toLong) // 调用第二个辅助构造器
502 | }
503 |
504 | ```
505 | 接下来用四种方式来实例化一个Random对象:
506 | ```
507 | val r1 = new Random(new java.util.Random) // 主构造器
508 | val r2 = new Random() // 第一个辅助构造器
509 | val r3 = new Random(10000L) // 第二个辅助构造器
510 | val r4 = new Random(100) // 第三个辅助构造器
511 |
512 | ```
513 |
514 | 通过上面的两个例子的对比,请分清楚到底哪一个才是主构造器。
515 |
516 | ---
517 |
518 | ---
519 |
520 | ## 1.9 主构造器
521 |
522 | 在Scala中,每个类都有主构造器。主构造器并不以this方法定义,而是与类定义交织在一起。
523 |
524 | 1. 主构造器的参数直接放置在类名之后。
525 | ```
526 | class Person(val name: String, val age: Int) {
527 | // 类的参数就可以当做主构造器的参数
528 | }
529 | // 另一种等价写法:使用case class,构造参数不需要显示指定val修饰,case class默认就是val修饰的,
530 | // 但是var在case class中也需要显示指定
531 | case class Person(name: String, age: Int) {
532 | // 类的参数就可以当做主构造器的参数
533 | }
534 | ```
535 | **主构造器的参数如果有val/var修饰时,会被编译成字段,其值被构造时传入的参数初始化。**
536 | 在本例中,name和age成为Person类的字段。如new Person("Fred", 42)这样的构造器调用将设置name和age字段。
537 |
538 | 与之等价的Java中的啰啰嗦嗦的代码如下:
539 | ```
540 | public class Person {
541 | private String name;
542 | private int age;
543 |
544 | public Person(String name, int age) {
545 | this.name = name;
546 | this.age = age;
547 | }
548 |
549 | public String getName() {
550 | return this.name;
551 | }
552 | public int getAge() {
553 | return this.age;
554 | }
555 | }
556 |
557 | ```
558 | 虽然上面的代码可以通过IDE自动生成,但是在阅读代码时,没有Scala那般简洁,干净!
559 |
560 | 2.主构造器会执行类体中定义的所有语句(属于主构造器的语句)。
561 | ```
562 | class Person(val name: String, private val age: Int) {
563 | println("Just constructed another person") // 这是主构造器的一部分。
564 | def description = name + " is " + age + " years old"
565 | }
566 | ```
567 | 上面这个例子中,每当实例化一个Person对象时,主构造器会构造name和age字段,并同时执行println方法。
568 | 当你需要在构造过程中配置某个字段时,这个特性特别有用。例如:
569 | ```
570 | class MyProg {
571 | private val props = new Properties
572 | props.load(new FileReader("myprog.properties"))
573 | //上述语句是主构造器的一部分
574 | }
575 | ```
576 | > 说明:如果类名之后没有参数,则该类具备一个无参数主构造器。这样的主构造器仅仅是执行类体中属于该主构造器的语句。
577 |
578 | 通常可以通过在主构造器中使用默认参数来避免过多的使用辅助构造器。
579 | ```
580 | class Person(val name: String = "", val age: Int = 0)
581 | ```
582 |
583 | 3.主构造器的参数可以有多种修饰符。
584 | 例如:
585 | ```
586 | class Person(val name: String, private var age: Int, private[this] val address: String)
587 | ```
588 | 再次强调,主构造器中的参数有var/val修饰的,会被编译成字段。
589 |
590 | 如果这些参数不带有var/val修饰的话,就是普通的方法参数。但是,这样的参数取决于在类中如何被使用。
591 |
592 | ①如果不带val或var,且这些参数至少被一个方法所使用,它将被升格为字段。
593 | ```
594 | class Person(name: String, age: Int) {
595 | def description = name + " is " + age + " years old"
596 | }
597 | ```
598 | 上述代码声明并初始化不可变字段name和age,而这两个字段是对象私有的。效果等同于private[this] val。
599 |
600 | ②否则,该参数将不被保存为字段。它仅仅是一个可以被主构造器中的代码访问的普通参数。
601 |
602 | 通过①②我们可得,类参数在类体中使用,则类参数会升级为
603 | `private[this] val`修饰的对象字段。否则它只是一个可以被主构造器中的代码访问的普通参数。
604 |
605 | 针对主构造器参数生成的字段和方法:
606 |
607 | |主构造器参数 | 生成的字段/方法|
608 | |--------------|---------------------------------------|
609 | |name: String | 对象私有字段。如果没有方法使用name,则无该字段 |
610 | |private val/var name: String| 类私有字段,私有getter和setter方法 |
611 | |val/var name: String| 类私有字段,公有getter和setter方法 |
612 | |@BeanProperty val/var name: String| 类私有字段,公有的Scala版和JavaBeans版的getter和setter方法|
613 |
614 | 4.主构造器私有化。
615 |
616 | 如果想让主构造器变成私有的,可以在类名之后参数列表之前添加private关键字修饰即可。
617 | ```
618 | class Person private(val id: Int){}
619 | ```
620 | 这样一来用户就必须通过辅助构造器来构造Person对象了。
621 |
622 | private主构造器的例子:
623 | ```
624 | // 无参数的类
625 | class Order1 {}
626 |
627 | // 无参数的类,私有主构造器。第一种写法: 省略空参数列表的括号
628 | class Order2 private {
629 | // 定义一个辅助构造器
630 | def this(orderId: Long) {
631 | this // 辅助构造器的第一句必须是调用主构造器,如果没有调用或者不是在第一句的位置调用,编译报错!
632 | println(orderId)
633 | // more code here ...
634 | }
635 | }
636 |
637 | // 无参数的类,私有主构造器,第二种写法: 显示写出空参数列表
638 | class Order22 private() {
639 | // 定义一个辅助构造器
640 | def this(orderId: Long) {
641 | this // 或者this() 辅助构造器的第一句必须是调用主构造器,如果没有调用或者不是在第一句的位置调用,编译报错!
642 | println(orderId)
643 | // more code here ...
644 | }
645 | }
646 |
647 | // 含有参数的类
648 | class Order3 private(orderId: Long, price: Double) {
649 | // 定义一个辅助构造器
650 | def this(orderId: Long) {
651 | this(orderId, 0) // 调用主构造器
652 | println(orderId)
653 | // more code here ...
654 | }
655 | }
656 |
657 | object PrivateConstructorTests extends App {
658 | // val o = new Order // this won't compile
659 | val o22 = new Order22(10L) // 调用辅助构造器
660 |
661 | // val o3 = new Order3(1, 10.0) // 编译错误
662 | val o3 = new Order3(1) // 调用辅助构造器
663 | }
664 | ```
665 |
666 | ---
667 |
668 | ---
669 |
670 | ## 1.10 样本类Case Class
671 |
672 | 上面介绍了Scala中关于类的各种细节,不难发现,很多都跟Java的类似。
673 | 如果真按照上面的搞法,归根结底,还是Java Style。Scala不是很推崇Java Style。
674 | 而在真正使用Scala中的Class时,几乎都是使用Scala推崇的immutable编程,这样的话,无一例外的都使用`case class`:
675 | ```
676 | // 主构造器参数默认val修饰,所以生成的字段只有一个getter方法
677 | // 并且该字段的值不能修改,不可变
678 | scala> case class Person(name: String, age: Int)
679 | defined class Person
680 |
681 | scala> val p = Person("jack", 30)
682 | p: Person = Person(jack,30)
683 |
684 | scala> p.name
685 | res3: String = jack
686 |
687 | scala> p.age
688 | res4: Int = 30
689 |
690 | scala> p.name = "tony" // 字段值不可变
691 | :10: error: reassignment to val
692 | p.name = "tony"
693 | ^
694 |
695 | scala> val p2 = p.copy("tony") // immutable方式,copy后生成一个新对象
696 | p2: Person = Person(tony,30)
697 |
698 | scala> p2.name
699 | res5: String = tony
700 |
701 | ```
702 |
703 | ---
704 |
705 | ---
706 |
707 | References:
708 |
709 | [1]. 【Scala for the impatient chapter 5】
--------------------------------------------------------------------------------
/doc/12 Scala中的常用Monad类型实战.markdown:
--------------------------------------------------------------------------------
1 | # 12 Scala中的常用Monad类型实战
2 |
3 | 标签(空格分隔): 级别L2:资深类库设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | ## 12.1 类型 Option(串行中的无异常处理的场景时使用)
12 |
13 | 引入Option的目的到底解决什么问题,为什么用它来处理缺失值要比其他方法好,这是本节要说明的问题。
14 |
15 | ### 12.1.1 基本概念
16 |
17 | Java 开发者一般都遇到过 NullPointerException(其他语言也有类似的东西), 通常这是由于调用了某个对象的方法,但是该对象却为null ,这并不是开发者所希望发生的,代码也只能用丑陋的`try{}catch`去捕捉这种异常。
18 |
19 | 值 null 通常被滥用来表示一个缺失的值。
20 |
21 | Scala 试图通过摆脱 null 来解决这个问题,并提供自己的类型来表示一个值是可选的(有值或无值), 这就是 Option[A] 特质。
22 |
23 | Option[A] 是一个类型为 A 的可选值的容器: 如果值存在, Option[A] 就是一个 Some[A] ,如果不存在, Option[A] 就是对象 None 。
24 |
25 | 在类型层面上指出一个值是否存在,使用你的代码的开发者(也包括你自己)就会被编译器强制去处理这种可能性, 而不能依赖值存在的偶然性。
26 |
27 | Option 是强制的!不要使用 null 来表示一个值是缺失的。
28 |
29 |
30 | ----------
31 |
32 |
33 | ### 12.1.2 创建 Option
34 |
35 | 通常,你可以直接实例化 Some 样本类来创建一个 Option 。
36 |
37 | ```
38 | val greeting: Option[String] = Some("Hello world")
39 |
40 | // 或者
41 | val greeting = Some("Hello world") // 类型自动推断出为Option[String]
42 | ```
43 |
44 | 或者,在知道值缺失的情况下,直接使用 None 对象:
45 |
46 | ```
47 | val greeting: Option[String] = None
48 | ```
49 |
50 | 然而,在实际工作中,你不可避免的要去操作一些 Java 库, 或者是其他将 null 作为缺失值的JVM 语言的代码。 为此, Option 伴生对象提供了一个工厂方法(apply),可以根据给定的参数创建相应的 Option :
51 |
52 | ```
53 | val absentGreeting: Option[String] = Option(null) // absentGreeting will be None
54 | // 等价于显示调用伴生对象的工厂方法
55 | val absentGreeting: Option[String] = Option.apply(null)
56 |
57 | val presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")
58 | ```
59 |
60 | **综上所述,明确知道值的就用Some,明确不知道值的就用None,不确定的就用Option**。
61 |
62 | ----------
63 |
64 | ### 12.1.3 Option的正确使用
65 |
66 | 在使用Option时,主要有三种不同的使用场景:
67 |
68 | 1 对Option的数据只处理Some的情况,而None的情况不需要处理。
69 | (**推荐使用方式是**,把Option当做集合的方式使用,具体可以用for,foreach,map,flatMap等)
70 | 例如:
71 | ```
72 | // 获得一个User对象
73 | val user1: Option[User] = UserRepository.findById(1)
74 |
75 | // 如果user不为空,就打印他的first name
76 | if (user1.isDefined) {
77 | println(user1.get.firstName) // will print "John"
78 | } // 显然,为空就不做任何处理
79 |
80 | // 上面的这种使用方式极不推荐,啰嗦的Java Style
81 | // 推荐的方式是当做一个只包含一个元素的集合来处理
82 | user1.foreach { u => println(u.firstName) } // 如果为空不做任何事,否则打印出user的first name
83 |
84 | ```
85 |
86 | 2 对Option的数据,Some和None的情况都要处理,但是**处理他们的逻辑是一样的**。
87 | (这种情况其实就是获得Some里面的值,None的情况指定默认值。**推荐使用方式是**,使用getOrElse方法)
88 | 例如:
89 | ```
90 | val age: Option[Int] = Option(30) // 这里指定是有值,如果这个age从别的地方获得,可能为None
91 | // 但是获得的值进行处理的逻辑一样
92 | age.getOrElse(30) // 真实年龄值和默认值都是为了给使用年龄的地方提供一个整数供使用
93 |
94 | ```
95 |
96 | 3 对Option的数据,Some和None的情况都要处理,但是**处理他们的逻辑不一样**。
97 | (**推荐使用方式是**,模式匹配)
98 | 例如:
99 | ```
100 | UserRepository.findById(1) match { // 从UserRepository中获得一个User
101 | case Some(user) =>
102 | // 如果User存在,那么就对该User进行字段的更新
103 | val updateUser = user.copy(firstName = "Jack")
104 | UserRepository.update(updateUser)
105 | // 再返回更新后的user
106 | updateUser
107 |
108 | case None =>
109 | // 如果不存在,就Insert一个新的User
110 | val newUser = User(3, "Tony", "G", 30, Some("male"))
111 | UserRepository.insert(newUser)
112 | // 再返回新增的User
113 | newUser
114 | }
115 |
116 | ```
117 |
118 | ----------
119 |
120 | 下面具体介绍Option的各种细节:
121 |
122 | ### 12.1.4 使用 Option
123 |
124 | 目前为止,所有的这些都很简洁,不过该怎么使用 Option 呢?是时候开始举些无聊的例子了。
125 |
126 | 想象一下,你正在为某个创业公司工作,要做的第一件事情就是实现一个用户的存储库, 要求能够通过唯一的用户 ID 来查找他们。 有时候请求会带来无效的 ID,这种情况,查找方法就需要返回 Option[User] 类型的数据。 一个假想的实现可能是:
127 | ```
128 | case class User(
129 | id: Int,
130 | firstName: String,
131 | lastName: String,
132 | age: Int,
133 | gender: Option[String]
134 | )
135 |
136 | object UserRepository {
137 | private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")),
138 | 2 -> User(2, "Johanna", "Doe", 30, None))
139 |
140 | def findById(id: Int): Option[User] = users.get(id)
141 |
142 | def findAll = users.values
143 | }
144 | ```
145 | 现在,假设从 UserRepository 接收到一个 Option[User] 实例,并需要拿它做点什么,该怎么办呢?
146 |
147 | 一个办法就是通过 isDefined 方法来检查它是否有值。 如果有,你就可以用 get 方法来获取该值(**不推荐这种使用方式**):
148 | ```
149 | val user1 = UserRepository.findById(1)
150 |
151 | if (user1.isDefined) {
152 | println(user1.get.firstName) // will print "John"
153 | }
154 | ```
155 |
156 | 这和 Guava 库 中的 Optional 使用方法类似。 不过这种使用方式太过笨重,更重要的是,使用 get 之前, 你可能会忘记用 isDefined 做检查,这会导致运行期出现异常。 这样一来,相对于 null ,使用 Option 并没有什么优势。
157 |
158 | 你应该尽可能远离这种访问方式!
159 |
160 |
161 | ----------
162 |
163 |
164 | ### 12.1.5 提供一个默认值(用getOrElse获取Option类型里面的值)
165 |
166 | 很多时候,在值不存在时,需要进行回退,或者提供一个默认值。 Scala 为 Option 提供了 getOrElse 方法,以应对这种情况:
167 | ```
168 | val user = User(2, "Johanna", "Doe", 30, None)
169 |
170 | println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"
171 | ```
172 |
173 | 请注意,作为 getOrElse 参数的默认值是一个 传名参数(by name的方式) ,这意味着,只有当这个 Option 确实是 None 时,传名参数才会被求值。 因此,没必要担心创建默认值的代价,它只有在需要时才会发生。
174 |
175 |
176 | ----------
177 |
178 |
179 | ### 12.1.6 模式匹配(对Option类型的Some和None分别有不同的逻辑处理)
180 |
181 | Some 是一个样本类(case class),可以出现在模式匹配表达式或者其他允许模式出现的地方。 上面的例子可以用模式匹配来重写:
182 | ```
183 | val user = User(2, "Johanna", "Doe", 30, None)
184 |
185 | user.gender match {
186 | case Some(gender) => println("Gender: " + gender)
187 | case None => println("Gender: not specified")
188 | }
189 | ```
190 | 或者,你想删除重复的 println 语句,并重点突出模式匹配表达式的使用:
191 | ```
192 | val user = User(2, "Johanna", "Doe", 30, None)
193 |
194 | val gender = user.gender match {
195 | case Some(gender) => gender
196 | case None => "not specified"
197 | }
198 |
199 | println("Gender: " + gender)
200 | ```
201 | 对于获取Option里面的值,你可能已经发现用模式匹配处理 Option 实例是非常啰嗦的,这也是它**非惯用法**的原因。 所以,即使你很喜欢模式匹配,也尽量用其他方法吧。
202 |
203 | 不过在 Option 上使用模式确实是有一个相当优雅的方式, 在下面的 for 语句一节中,你就会学到。
204 |
205 | 但是对于Option类型的Some和None分别有不同的逻辑处理时,**推荐使用模式匹配**。例如:
206 | ```
207 | UserRepository.findById(1) match { // 从UserRepository中获得一个User
208 | case Some(user) =>
209 | // 如果User存在,那么就对该User进行字段的更新
210 | val updateUser = user.copy(firstName = "Jack")
211 | UserRepository.update(updateUser)
212 | // 再返回更新后的user
213 | updateUser
214 |
215 | case None =>
216 | // 如果不存在,就Insert一个新的User
217 | val newUser = User(3, "Tony", "G", 30, Some("male"))
218 | UserRepository.insert(newUser)
219 | // 再返回新增的User
220 | newUser
221 | }
222 |
223 | ```
224 |
225 |
226 | ----------
227 |
228 |
229 | ### 12.1.7 作为集合的 Option
230 |
231 | 到目前为止,你还没有看见过优雅使用 Option 的方式吧。下面这个就是了。
232 |
233 | 前文我提到过, Option 是类型 A 的容器,更确切地说,你可以把它看作是某种集合, 这个特殊的集合要么只包含一个元素,要么就什么元素都没有。
234 |
235 | 虽然在类型层次上, Option 并不是 Scala 的集合类型, 但,凡是你觉得 Scala 集合好用的方法, Option 也有, 你甚至可以将其转换成一个集合,比如说 List 。
236 |
237 | 那么这又能让你做什么呢?
238 |
239 | #### 12.1.7.1 执行一个副作用
240 |
241 | 如果想在 Option 值存在的时候执行某个副作用,foreach 方法就派上用场了:
242 | ```
243 | UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"
244 | ```
245 | 如果这个 Option 是一个 Some ,传递给 foreach 的函数就会被调用一次,且只有一次; 如果是 None ,那它就不会被调用。
246 |
247 |
248 | ----------
249 |
250 |
251 | #### 12.1.7.2 执行映射
252 |
253 | Option 表现的像集合,最棒的一点是, 你可以用它来进行函数式编程,就像处理列表、集合那样。
254 |
255 | 正如你可以将 List[A] 映射到 List[B] 一样,你也可以映射 Option[A] 到 Option[B]: 如果 Option[A] 实例是 Some[A] 类型,那映射结果就是 Some[B] 类型;否则,就是 None 。
256 |
257 | 如果将 Option 和 List 做对比 ,那 None 就相当于一个空列表: 当你映射一个空的 List[A] ,会得到一个空的 List[B] , 而映射一个是 None 的 Option[A] 时,得到的 Option[B] 也是 None 。
258 |
259 | 让我们得到一个可能不存在的用户的年龄:
260 | ```
261 | val age = UserRepository.findById(1).map(_.age) // age is Some(32)
262 | ```
263 |
264 |
265 | ----------
266 |
267 |
268 | #### 12.1.7.3 Option 与 flatMap
269 |
270 | 也可以在 gender 上做 map 操作:
271 | ```
272 | val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]
273 | ```
274 | 所生成的 gender 类型是 Option[Option[String]] 。这是为什么呢?
275 |
276 | 这样想:你有一个装有 User 的 Option 容器,在容器里面,你将 User 映射到 Option[String] ( User 类上的属性 gender 是 Option[String] 类型的)。 得到的必然是嵌套的 Option。
277 |
278 | 既然可以 flatMap 一个 List[List[A]] 到 List[B] , 也可以 flatMap 一个 Option[Option[A]] 到 Option[B] ,这没有任何问题: Option 提供了 flatMap 方法。
279 | ```
280 | val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")
281 |
282 | val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None
283 |
284 | val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None
285 | ```
286 | 现在结果就变成了 Option[String] 类型, 如果 user 和 gender 都有值,那结果就会是 Some 类型,反之,就得到一个 None 。
287 |
288 | 要理解这是什么原理,让我们看看当 flatMap 一个 List[List[A] 时,会发生什么? (要记得, Option 就像一个集合,比如列表)
289 | ```
290 | val names: List[List[String]] =
291 | List(List("John", "Johanna", "Daniel"), List(), List("Doe", "Westheide"))
292 |
293 | names.map(_.map(_.toUpperCase))
294 | // results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))
295 |
296 | names.flatMap(_.map(_.toUpperCase))
297 | // results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")
298 | ```
299 | 如果我们使用 flatMap ,内部列表中的所有元素会被转换成一个扁平的字符串列表。 显然,如果内部列表是空的,则不会有任何东西留下。
300 |
301 | 现在回到 Option 类型,如果映射一个由 Option 组成的列表呢?
302 | ```
303 | val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
304 |
305 | names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
306 |
307 | names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")
308 | ```
309 | 如果只是 map ,那结果类型还是 List[Option[String]] 。 而使用 flatMap 时,内部集合的元素就会被放到一个扁平的列表里: 任何一个 Some[String] 里的元素都会被解包,放入结果集中; 而原列表中的 None 值由于不包含任何元素,就直接被过滤出去了。
310 |
311 | 记住这一点,然后再去看看 faltMap 在 Option 身上做了什么。
312 |
313 |
314 | ----------
315 |
316 |
317 | #### 12.1.7.4 过滤 Option
318 |
319 | 也可以像过滤列表那样过滤 Option: 如果选项包含有值,而且传递给 filter 的谓词函数返回真, filter 会返回 Some 实例。 否则(即选项没有值,或者谓词函数返回假值),返回值为 None 。
320 | ```
321 | UserRepository.findById(1).filter(_.age > 30) // None, because age is <= 30
322 |
323 | UserRepository.findById(2).filter(_.age > 30) // Some(user), because age is > 30
324 |
325 | UserRepository.findById(3).filter(_.age > 30) // None, because user is already None
326 | ```
327 |
328 |
329 | ----------
330 |
331 |
332 | ### 12.1.8 for 语句
333 |
334 | 现在,你已经知道 Option 可以被当作集合来看待,并且有 map 、 flatMap 、 filter 这样的方法。 可能你也在想 Option 是否能够用在 for 语句中,答案是肯定的。 而且,用 for 语句来处理 Option 是可读性最好的方式,尤其是当你有多个 map 、flatMap 、filter 调用的时候。 如果只是一个简单的 map 调用,那 for 语句可能有点繁琐。
335 |
336 | 假如我们想得到一个用户的性别,可以这样使用 for 语句:
337 | ```
338 | for {
339 | user <- UserRepository.findById(1)
340 | gender <- user.gender
341 | } yield gender // results in Some("male")
342 | ```
343 | 可能你已经知道,这样的 for 语句等同于嵌套的 flatMap 调用。 如果 UserRepository.findById 返回 None,或者 gender 是 None , 那这个 for 语句的结果就是 None 。 不过这个例子里, gender 含有值,所以返回结果是 Some 类型的。
344 |
345 | 如果我们想返回所有用户的性别(当然,如果用户设置了性别),可以遍历用户,yield 其性别:
346 | ```
347 | for {
348 | user <- UserRepository.findAll
349 | gender <- user.gender
350 | } yield gender
351 | // result in List("male")
352 | ```
353 |
354 |
355 | ----------
356 |
357 |
358 | #### 12.1.8.1 在生成器左侧使用
359 |
360 | for 语句中生成器的左侧也是一个模式。 这意味着也可以在 for 语句中使用包含选项的模式。
361 |
362 | 重写之前的例子:
363 | ```
364 | for {
365 | User(_, _, _, _, Some(gender)) <- UserRepository.findAll
366 | } yield gender
367 | ```
368 | 在生成器左侧使用 Some 模式就可以在结果集中排除掉值为 None 的元素。
369 |
370 |
371 | ----------
372 |
373 |
374 | #### 12.1.8.2 链接 Option
375 |
376 | Option 还可以被链接使用,这有点像偏函数的链接: 在 Option 实例上调用 orElse 方法,并将另一个 Option 实例作为传名参数传递给它。 如果一个 Option 是 None , orElse 方法会返回传名参数的值,否则,就直接返回这个 Option。
377 |
378 | 一个很好的使用案例是资源查找:对多个不同的地方按优先级进行搜索。 下面的例子中,我们首先搜索 config 文件夹,并调用 orElse 方法,以传递备用目录:
379 | ```
380 | case class Resource(content: String)
381 |
382 | val resourceFromConfigDir: Option[Resource] = None
383 |
384 | val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
385 |
386 | val resource = resourceFromConfigDir orElse resourceFromClasspath
387 | ```
388 | 如果想链接多个选项,而不仅仅是两个,使用 orElse 会非常合适。 不过,如果只是想在值缺失的情况下提供一个默认值,那还是使用 getOrElse 吧。
389 |
390 | >总结
391 | 在这一节里,你学到了有关 Option 的所有知识, 这有利于你理解别人的代码,也有利于你写出更可读,更函数式的代码。
392 | 这一节最重要的一点是:列表、集合、映射、Option,以及之后你会见到的其他数据类型, 它们都有一个非常统一的使用方式,这种使用方式既强大又优雅。
393 | 下一节,你将学习 Scala 错误处理的惯用法。
394 |
395 |
396 | ----------
397 |
398 |
399 | ----------
400 |
401 |
402 | ## 12.2 Try 与错误处理(串行中有异常处理时使用)
403 |
404 | 当你在尝试一门新的语言时,可能不会过于关注程序出错的问题, 但当真的去创造可用的代码时,就不能再忽视代码中的可能产生的错误和异常了。 鉴于各种各样的原因,人们往往低估了语言对错误处理支持程度的重要性。
405 |
406 | 事实会表明,Scala 能够很优雅的处理此类问题, 这一部分,我会介绍 Scala 基于 Try 的错误处理机制,以及这背后的原因。 我将使用一个在 Scala 2.10 新引入的特性,该特性向 2.9.3 兼容, 因此,请确保你的 Scala 版本不低于 2.9.3。
407 |
408 |
409 | ----------
410 |
411 |
412 | ### 12.2.1 异常的抛出和捕获
413 |
414 | 在介绍 Scala 错误处理的惯用法之前,我们先看看其他语言(如,Java,Ruby)的错误处理机制。 和这些语言类似,Scala 也允许你抛出异常:
415 | ```
416 | case class Customer(age: Int)
417 |
418 | class Cigarettes
419 |
420 | case class UnderAgeException(message: String) extends Exception(message)
421 |
422 | def buyCigarettes(customer: Customer): Cigarettes =
423 | if (customer.age < 16)
424 | throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
425 | else new Cigarettes
426 | ```
427 |
428 | 被抛出的异常能够以类似 Java 中的方式被捕获,虽然是使用偏函数来指定要处理的异常类型。 此外,Scala 的 try/catch 是表达式(返回一个值),因此下面的代码会返回异常的消息(**不推荐的方式**):
429 |
430 | ```
431 | val youngCustomer = Customer(15)
432 |
433 | try {
434 | buyCigarettes(youngCustomer)
435 | "Yo, here are your cancer sticks! Happy smokin'!"
436 | } catch {
437 | case UnderAgeException(msg) => msg
438 | }
439 | ```
440 |
441 |
442 | ----------
443 |
444 |
445 | ### 12.2.2 函数式的错误处理
446 |
447 | 现在,如果代码中到处是上面的异常处理代码,那它很快就会变得丑陋无比,和函数式程序设计非常不搭。 对于高并发应用来说,这也是一个很差劲的解决方式,比如, 假设需要处理在其他线程执行的 actor 所引发的异常,显然你不能用捕获异常这种处理方式, 你可能会想到其他解决方案,例如去接收一个表示错误情况的消息。
448 |
449 | 一般来说,在 Scala 中,好的做法是通过从函数里返回一个合适的值来通知人们程序出错了。 别担心,我们不会回到 C 中那种需要使用按约定进行检查的错误编码的错误处理。 相反,Scala 使用一个特定的类型来表示可能会导致异常的计算,这个类型就是 Try。
450 |
451 |
452 | ----------
453 |
454 |
455 | #### 12.2.2.1 Try 的语义
456 |
457 | 解释 Try 最好的方式是将它与上一章所讲的 Option 作对比。
458 |
459 | Option[A] 是一个可能有值也可能没值的容器;
460 |
461 | Try[A] 则表示一种计算: 这种计算在成功的情况下,返回类型为 A 的值,在出错的情况下,返回 Throwable 。
462 | 这种可以容纳错误的容器可以很轻易的在并发执行的程序之间传递。
463 |
464 |
465 | ----------
466 |
467 |
468 | #### 12.2.2.2 Try 有两个子类型:
469 |
470 | - Success[A]:代表成功的计算。
471 | - 封装了 Throwable 的 Failure[A]:代表出了错的计算。
472 |
473 | 如果知道一个计算可能导致错误,我们可以简单的使用 Try[A] 作为函数的返回类型。 这使得出错的可能性变得很明确,而且强制客户端以某种方式处理出错的可能。
474 |
475 | 假设,需要实现一个简单的网页爬取器:用户能够输入想爬取的网页 URL, 程序就需要去分析 URL 输入,并从中创建一个 java.net.URL :
476 |
477 | ```
478 | import scala.util.Try
479 | import java.net.URL
480 | def parseURL(url: String): Try[URL] = Try(new URL(url))
481 | ```
482 |
483 | 正如你所看到的,函数返回类型为 Try[URL]: 如果给定的 url 语法正确,这将是 Success[URL], 否则, URL 构造器会引发 MalformedURLException ,从而返回值变成 Failure[URL] 类型。
484 |
485 | 上例中,我们还用了 Try 伴生对象里的 apply 工厂方法,这个方法接受一个类型为 A 的 传名参数, 这意味着, new URL(url) 是在 Try 的 apply 方法里执行的。
486 |
487 | apply 方法不会捕获任何非致命的异常,仅仅返回一个包含相关异常的 Failure 实例。
488 |
489 | 因此, parseURL("http://danielwestheide.com") 会返回一个 Success[URL] ,包含了解析后的网址, 而 parseULR("garbage") 将返回一个含有 MalformedURLException 的 Failure[URL]。
490 |
491 |
492 | ----------
493 |
494 |
495 | #### 12.2.2.3 使用 Try
496 |
497 | 使用 Try 与使用 Option 非常相似,在这里你看不到太多新的东西。
498 |
499 | 你可以调用 isSuccess 方法来检查一个 Try 是否成功,然后通过 get 方法获取它的值, 但是,这种方式的使用并不多见,因为你可以用 getOrElse 方法给 Try 提供一个默认值:
500 | ```
501 | val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
502 | ```
503 | 如果用户提供的 URL 格式不正确,我们就使用 DuckDuckGo 的 URL 作为备用。
504 |
505 |
506 | ----------
507 |
508 |
509 | `链式操作`
510 |
511 | Try 最重要的特征是,它也支持高阶函数,就像 Option 一样。 在下面的示例中,你将看到,在 Try 上也进行链式操作,捕获可能发生的异常,而且代码可读性不错。
512 |
513 | `Mapping 和 Flat Mapping`
514 |
515 | 将一个是 Success[A] 的 Try[A] 映射到 Try[B] 会得到 Success[B] 。 如果它是 Failure[A] ,就会得到 Failure[B] ,而且包含的异常和 Failure[A] 一样。
516 | ```
517 | parseURL("http://danielwestheide.com").map(_.getProtocol)
518 | // results in Success("http")
519 |
520 | parseURL("garbage").map(_.getProtocol)
521 | // results in Failure(java.net.MalformedURLException: no protocol: garbage)
522 |
523 | ```
524 | 如果链接多个 map 操作,会产生嵌套的 Try 结构,这并不是我们想要的。 考虑下面这个返回输入流的方法:
525 | ```
526 | import java.io.InputStream
527 |
528 | def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] =
529 | parseURL(url).map { u =>
530 | Try(u.openConnection()).map(conn => Try(conn.getInputStream))
531 | }
532 | ```
533 | 由于每个传递给 map 的匿名函数都返回 Try,因此返回类型就变成了 Try[Try[Try[InputStream]]] 。
534 |
535 | 这时候, flatMap 就派上用场了。 Try[A] 上的 flatMap 方法接受一个映射函数,这个函数类型是 (A) => Try[B]。 如果我们的 Try[A] 已经是 Failure[A] 了,那么里面的异常就直接被封装成 Failure[B] 返回, 否则, flatMap 将 Success[A] 里面的值解包出来,并通过映射函数将其映射到 Try[B] 。
536 |
537 | 这意味着,我们可以通过链接任意个 flatMap 调用来创建一条操作管道,将值封装在 Success 里一层层的传递。
538 |
539 | 现在让我们用 flatMap 来重写先前的例子:
540 | ```
541 | def inputStreamForURL(url: String): Try[InputStream] =
542 | parseURL(url).flatMap { u =>
543 | Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
544 | }
545 |
546 | ```
547 | 这样,我们就得到了一个 Try[InputStream], 它可以是一个 Failure,包含了在 flatMap 过程中可能出现的异常; 也可以是一个 Success,包含了最后的结果。
548 |
549 |
550 | ----------
551 |
552 |
553 | `过滤器和 foreach`
554 |
555 | 当然,你也可以对 Try 进行过滤,或者调用 foreach ,既然已经学过 Option,对于这两个方法也不会陌生。
556 |
557 | 当一个 Try 已经是 Failure 了,或者传递给它的谓词函数返回假值,filter 就返回 Failure (如果是谓词函数返回假值,那 Failure 里包含的异常是 NoSuchException ), 否则的话, filter 就返回原本的那个 Success ,什么都不会变:
558 | ```
559 | def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == "http")
560 |
561 | parseHttpURL("http://apache.openmirror.de") // results in a Success[URL]
562 |
563 | parseHttpURL("ftp://mirror.netcologne.de/apache.org") // results in a Failure[URL]
564 |
565 | ```
566 | 当一个 Try 是 Success 时, foreach 允许你在被包含的元素上执行副作用, 这种情况下,传递给 foreach 的函数只会执行一次,毕竟 Try 里面只有一个元素:
567 | ```
568 | parseHttpURL("http://danielwestheide.com").foreach(println)
569 | ```
570 | 当 Try 是 Failure 时, foreach 不会执行,返回 Unit 类型。
571 |
572 |
573 | ----------
574 |
575 |
576 | `for 语句中的 Try`
577 |
578 | 既然 Try 支持 flatMap 、 map 、 filter ,能够使用 for 语句也是理所当然的事情, 而且这种情况下的代码更可读。 为了证明这一点,我们来实现一个返回给定 URL 的网页内容的函数:
579 | ```
580 | import scala.io.Source
581 |
582 | def getURLContent(url: String): Try[Iterator[String]] =
583 | for {
584 | url <- parseURL(url)
585 | connection <- Try(url.openConnection())
586 | is <- Try(connection.getInputStream)
587 | source = Source.fromInputStream(is)
588 | } yield source.getLines()
589 | ```
590 | 这个方法中,有三个可能会出错的地方,但都被 Try 给涵盖了。 第一个是我们已经实现的 parseURL 方法, 只有当它是一个 Success[URL] 时,我们才会尝试打开连接,从中创建一个新的 InputStream 。 如果这两步都成功了,我们就 yield 出网页内容,得到的结果是 Try[Iterator[String]] 。
591 |
592 | 当然,你可以使用 Source#fromURL 简化这个代码,并且,这个代码最后没有关闭输入流, 这都是为了保持例子的简单性,专注于要讲述的主题。
593 |
594 | >在这个例子中,Source#fromURL可以这样用:
595 | ```
596 | import scala.io.Source
597 | def getURLContent(url: String): Try[Iterator[String]] =
598 | for {
599 | url <- parseURL(url)
600 | source = Source.fromURL(url)
601 | } yield source.getLines()
602 | ```
603 | 用 is.close() 可以关闭输入流。
604 |
605 |
606 | ----------
607 |
608 |
609 | `模式匹配`(**处理Success和Failure逻辑不同的场景,类比Option的场景区分**)
610 |
611 | 代码往往需要知道一个 Try 实例是 Success 还是 Failure,这时候,你应该想到模式匹配, 也幸好, Success 和 Failure 都是样例类。
612 |
613 | 接着上面的例子,如果网页内容能顺利提取到,我们就展示它,否则,打印一个错误信息:
614 | ```
615 | import scala.util.Success
616 | import scala.util.Failure
617 | getURLContent("http://danielwestheide.com/foobar") match {
618 | case Success(lines) => lines.foreach(println)
619 | case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
620 | }
621 | ```
622 |
623 |
624 | ----------
625 |
626 |
627 | `从故障中恢复`
628 |
629 | 如果想在失败的情况下执行某种动作,没必要去使用 getOrElse, 一个更好的选择是 recover ,它接受一个偏函数,并返回另一个 Try。 如果 recover 是在 Success 实例上调用的,那么就直接返回这个实例,否则就调用偏函数。 如果偏函数为给定的 Failure 定义了处理动作, recover 会返回 Success ,里面包含偏函数运行得出的结果。
630 |
631 | 下面是应用了 recover 的代码:
632 | ```
633 | import java.net.MalformedURLException
634 | import java.io.FileNotFoundException
635 | val content = getURLContent("garbage") recover {
636 | case e: FileNotFoundException => Iterator("Requested page does not exist")
637 | case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
638 | case _ => Iterator("An unexpected error has occurred. We are so sorry!")
639 | }
640 | ```
641 | 现在,我们可以在返回值 content 上安全的使用 get 方法了,因为它一定是一个 Success。 调用 content.get.foreach(println) 会打印 Please make sure to enter a valid URL。
642 |
643 |
644 | ----------
645 |
646 |
647 | 总结
648 |
649 | Scala 的错误处理和其他范式的编程语言有很大的不同。 Try 类型可以让你将可能会出错的计算封装在一个容器里,并优雅的去处理计算得到的值。 并且可以像操作集合和 Option 那样统一的去操作 Try。
650 |
651 | Try 还有其他很多重要的方法,鉴于篇幅限制,这一章并没有全部列出,比如 orElse 方法, transform 和 recoverWith 也都值得去看。
652 |
653 | 下一节,我们会探讨 Either,另外一种可以代表计算的类型,但它的可使用范围要比 Try 大的多。
654 |
655 | ----------
656 |
657 |
658 | ## 12.3 类型 Either
659 |
660 | 上一章介绍了 Try,它用函数式风格来处理程序错误。 这一章我们介绍一个和 Try 相似的类型 - Either, 学习如何去使用它,什么时候去使用它,以及它有什么缺点。
661 |
662 | 不过首先得知道一件事情: 在写作这篇文章的时候,Either 有一些设计缺陷,很多人都在争论到底要不要使用它。 既然如此,为什么还要学习它呢? 因为,在理解 Try 这个错综复杂的类型之前,不是所有人都会在代码中使用 Try 风格的异常处理。 其次,Try 不能完全替代 Either,它只是 Either 用来处理异常的一个特殊用法。 Try 和 Either 互相补充,各自侧重于不同的使用场景。
663 |
664 | 因此,尽管 Either 有缺陷,在某些情况下,它依旧是非常合适的选择。
665 |
666 |
667 | ----------
668 |
669 |
670 | ### 12.3.1 Either 语义
671 |
672 | Either 也是一个容器类型,但不同于 Try、Option,它需要两个类型参数: Either[A, B] 要么包含一个类型为 A 的实例,要么包含一个类型为 B 的实例。 这和 Tuple2[A, B] 不一样, Tuple2[A, B] 是两者都要包含。
673 |
674 | Either 只有两个子类型: Left、 Right, 如果 Either[A, B] 对象包含的是 A 的实例,那它就是 Left 实例,否则就是 Right 实例。
675 |
676 | 在语义上,Either 并没有指定哪个子类型代表错误,哪个代表成功, 毕竟,它是一种通用的类型,适用于可能会出现两种结果的场景。 而异常处理只不过是其一种常见的使用场景而已, 不过,按照约定,处理异常时,Left 代表出错的情况,Right 代表成功的情况。
677 |
678 |
679 | ----------
680 |
681 |
682 | ### 12.3.2 创建 Either
683 |
684 | 创建 Either 实例非常容易,Left 和 Right 都是样例类。 要是想实现一个 “坚如磐石” 的互联网审查程序,可以直接这么做:
685 | ```
686 | import scala.io.Source
687 | import java.net.URL
688 |
689 | def getContent(url: URL): Either[String, Source] =
690 | if(url.getHost.contains("google"))
691 | Left("Requested URL is blocked for the good of the people!")
692 | else
693 | Right(Source.fromURL(url))
694 | ```
695 | 调用 getContent(new URL("http://danielwestheide.com")) 会得到一个封装有 scala.io.Source 实例的 Right, 传入 new URL("https://plus.google.com") 会得到一个含有 String 的 Left。
696 |
697 |
698 | ----------
699 |
700 |
701 | ### 12.3.3 Either 用法
702 |
703 | Either 基本的使用方法和 Option、Try 一样: 调用 isLeft (或 isRight )方法询问一个 Either,判断它是 Left 值,还是 Right 值。 可以使用模式匹配,这是最方便也是最为熟悉的一种方法:
704 | ```
705 | getContent(new URL("http://google.com")) match {
706 | case Left(msg) => println(msg)
707 | case Right(source) => source.getLines.foreach(println)
708 | }
709 | ```
710 |
711 | **立场**
712 |
713 | 你不能,至少不能直接像 Option、Try 那样把 Either 当作一个集合来使用, 因为 Either 是 无偏(unbiased) 的。
714 |
715 | Try 偏向 Success: map 、 flatMap 以及其他一些方法都假设 Try 对象是一个 Success 实例, 如果是 Failure,那这些方法不做任何事情,直接将这个 Failure 返回。
716 |
717 | 但 Either 不做任何假设,这意味着首先你要选择一个立场,假设它是 Left 还是 Right, 然后在这个假设的前提下拿它去做你想做的事情。 调用 left 或 right 方法,就能得到 Either 的 LeftProjection 或 RightProjection实例, 这就是 Either 的 立场(Projection) ,它们是对 Either 的一个左偏向的或右偏向的封装。
718 |
719 | **映射**
720 |
721 | 一旦有了 Projection,就可以调用 map :
722 | ```
723 | val content: Either[String, Iterator[String]] =
724 | getContent(new URL("http://danielwestheide.com")).right.map(_.getLines())
725 | // content is a Right containing the lines from the Source returned by getContent
726 |
727 | val moreContent: Either[String, Iterator[String]] =
728 | getContent(new URL("http://google.com")).right.map(_.getLines)
729 | // moreContent is a Left, as already returned by getContent
730 |
731 | // content: Either[String,Iterator[String]] = Right(non-empty iterator)
732 | // moreContent: Either[String,Iterator[String]] = Left(Requested URL is blocked for the good of the people!)
733 | ```
734 | 这个例子中,无论 Either[String, Source] 是 Left 还是 Right, 它都会被映射到 Either[String, Iterator[String]] 。 如果,它是一个 Right 值,这个值就会被 _.getLines() 转换; 如果,它是一个 Left 值,就直接返回这个值,什么都不会改变。
735 |
736 | LeftProjection也是类似的:
737 | ```
738 | val content: Either[Iterator[String], Source] =
739 | getContent(new URL("http://danielwestheide.com")).left.map(Iterator(_))
740 | // content is the Right containing a Source, as already returned by getContent
741 |
742 | val moreContent: Either[Iterator[String], Source] =
743 | getContent(new URL("http://google.com")).left.map(Iterator(_))
744 | // moreContent is a Left containing the msg returned by getContent in an Iterator
745 |
746 | // content: Either[Iterator[String],scala.io.Source] = Right(non-empty iterator)
747 | // moreContent: Either[Iterator[String],scala.io.Source] = Left(non-empty iterator)
748 | ```
749 | 现在,如果 Either 是个 Left 值,里面的值会被转换;如果是 Right 值,就维持原样。 两种情况下,返回类型都是 Either[Iterator[String, Source] 。
750 |
751 | 请注意, map 方法是定义在 Projection 上的,而不是 Either, 但其返回类型是 Either,而不是 Projection。
752 |
753 | 可以看到,Either 和其他你知道的容器类型之所以不一样,就是因为它的无偏性。 接下来你会发现,在特定情况下,这会产生更多的麻烦。 而且,如果你想在一个 Either 上多次调用 map 、 flatMap 这样的方法, 你总需要做 Projection,去选择一个立场。
754 | Flat Mapping
755 |
756 | Projection 也支持 flat mapping,避免了嵌套使用 map 所造成的令人费解的类型结构。
757 |
758 | 假设我们想计算两篇文章的平均行数,下面的代码可以解决这个 “富有挑战性” 的问题:
759 | ```
760 | val part5 = new URL("http://t.co/UR1aalX4")
761 | val part6 = new URL("http://t.co/6wlKwTmu")
762 | val content = getContent(part5).right.map(a =>
763 | getContent(part6).right.map(b =>
764 | (a.getLines().size + b.getLines().size) / 2))
765 |
766 | // => content: Product with Serializable with scala.util.Either[String,Product with Serializable with scala.util.Either[String,Int]] = Right(Right(537))
767 | ```
768 | 运行上面的代码,会得到什么? 会得到一个类型为 Either[String, Either[String, Int]] 的玩意儿。 当然,你可以调用 joinRight 方法来使得这个结果 扁平化(flatten) 。
769 |
770 | 不过我们可以直接避免这种嵌套结构的产生, 如果在最外层的 RightProjection 上调用 flatMap 函数,而不是 map , 得到的结果会更好看些,因为里层 Either 的值被解包了:
771 | ```
772 | val content = getContent(part5).right.flatMap(a =>
773 | getContent(part6).right.map(b =>
774 | (a.getLines().size + b.getLines().size) / 2))
775 | // => content: scala.util.Either[String,Int] = Right(537)
776 | ```
777 | 现在, content 值类型变成了 Either[String, Int] ,处理它相对来说就很容易了。
778 |
779 | **for 语句**
780 |
781 | 说到 for 语句,想必现在,你应该已经爱上它在不同类型上的一致性表现了。 在 for 语句中,也能够使用 Either 的 Projection,但遗憾的是,这样做需要一些丑陋的变通。
782 |
783 | 假设用 for 语句重写上面的例子:
784 | ```
785 | def averageLineCount(url1: URL, url2: URL): Either[String, Int] =
786 | for {
787 | source1 <- getContent(url1).right
788 | source2 <- getContent(url2).right
789 | } yield (source1.getLines().size + source2.getLines().size) / 2
790 | ```
791 | 这个代码还不是太坏,毕竟只需要额外调用 left 、 right 。
792 |
793 | 但是你不觉得 yield 语句太长了吗?现在,我就把它移到值定义块中:
794 | ```
795 | def averageLineCountWontCompile(url1: URL, url2: URL): Either[String, Int] =
796 | for {
797 | source1 <- getContent(url1).right
798 | source2 <- getContent(url2).right
799 | lines1 = source1.getLines().size
800 | lines2 = source2.getLines().size
801 | } yield (lines1 + lines2) / 2
802 | ```
803 |
804 | 试着去编译它,然后你会发现无法编译!如果我们把 for 语法糖去掉,原因可能会清晰些。 展开上面的代码得到:
805 | ```
806 | def averageLineCountDesugaredWontCompile(url1: URL, url2: URL): Either[String, Int] =
807 | getContent(url1).right.flatMap { source1 =>
808 | getContent(url2).right.map { source2 =>
809 | val lines1 = source1.getLines().size
810 | val lines2 = source2.getLines().size
811 | (lines1, lines2)
812 | }.map { case (x, y) => x + y / 2 }
813 | }
814 | ```
815 | 问题在于,在 for 语句中追加新的值定义会在前一个 map 调用上自动引入另一个 map 调用, 前一个 map 调用返回的是 Either 类型,不是 RightProjection 类型, 而 Scala 并没有在 Either 上定义 map 函数,因此编译时会出错。
816 |
817 | 这就是 Either 丑陋的一面。要解决这个例子中的问题,可以不添加新的值定义。 但有些情况,就必须得添加,这时候可以将值封装成 Either 来解决这个问题:
818 | ```
819 | def averageLineCount(url1: URL, url2: URL): Either[String, Int] =
820 | for {
821 | source1 <- getContent(url1).right
822 | source2 <- getContent(url2).right
823 | lines1 <- Right(source1.getLines().size).right
824 | lines2 <- Right(source2.getLines().size).right
825 | } yield (lines1 + lines2) / 2
826 | ```
827 | 认识到这些设计缺陷是非常重要的,这不会影响 Either 的可用性,但如果不知道发生了什么,它会让你感到非常头痛。
828 |
829 | **其他方法**
830 |
831 | Projection 还有其他有用的方法:
832 |
833 | 可以在 Either 的某个 Projection 上调用 toOption 方法,将其转换成 Option。
834 |
835 | 假如,你有一个类型为 Either[A, B] 的实例 e , e.right.toOption 会返回一个 Option[B] 。 如果 e 是一个 Right 值,那这个 Option[B] 会是 Some 类型, 如果 e 是一个 Left 值,那 Option[B] 就会是 None 。 调用 e.left.toOption 也会有相应的结果。
836 |
837 | 还可以用 toSeq 方法将 Either 转换为序列。
838 |
839 | **Fold 函数**
840 |
841 | 如果想变换一个 Either(不论它是 Left 值还是 right 值),可以使用定义在 Either 上的 fold 方法。 这个方法接受两个返回相同类型的变换函数, 当这个 Either 是 Left 值时,第一个函数会被调用;否则,第二个函数会被调用。
842 |
843 | 为了说明这一点,我们用 fold 重写之前的一个例子:
844 | ```
845 | val content: Iterator[String] =
846 | getContent(new URL("http://danielwestheide.com")).fold(Iterator(_), _.getLines())
847 |
848 | val moreContent: Iterator[String] =
849 | getContent(new URL("http://google.com")).fold(Iterator(_), _.getLines())
850 | ```
851 | 这个示例中,我们把 Either[String, String] 变换成了 Iterator[String] 。 当然,你也可以在变换函数里返回一个新的 Either,或者是只执行副作用。 fold 是一个可以用来替代模式匹配的好方法。
852 |
853 | **何时使用 Either**
854 |
855 | 知道了 Either 的用法和应该注意的事项,我们来看看一些特殊的用例。
856 |
857 | 错误处理
858 |
859 | 可以用 Either 来处理异常,就像 Try 一样。 不过 Either 有一个优势:可以使用更为具体的错误类型,而 Try 只能用 Throwable 。 (这表明 Either 在处理自定义的错误时是个不错的选择) 不过,需要实现一个方法,将这个功能委托给 scala.util.control 包中的 Exception 对象:
860 | ```
861 | import scala.util.control.Exception.catching
862 |
863 | def handling[Ex <: Throwable, T](exType: Class[Ex])(block: => T): Either[Ex, T] =
864 | catching(exType).either(block).asInstanceOf[Either[Ex, T]]
865 | ```
866 | 这么做的原因是,虽然 scala.util.Exception 提供的方法允许你捕获某些类型的异常, 但编译期产生的类型总是 Throwable ,因此需要使用 asInstanceOf 方法强制转换。
867 |
868 | 有了这个方法,就可以把期望要处理的异常类型,放在 Either 里了:
869 | ```
870 | import java.net.MalformedURLException
871 |
872 | def parseURL(url: String): Either[MalformedURLException, URL] =
873 | handling(classOf[MalformedURLException])(new URL(url))
874 | ```
875 | handling 的第二个参数 block 中可能还会有其他产生错误的情形, 而且并不是所有情形都会抛出异常。 这种情况下,没必要为了捕获异常而人为抛出异常,相反,只需定义你自己的错误类型,最好是样例类, 并在错误情况发生时返回一个封装了这个类型实例的 Left。
876 |
877 | 下面是一个例子:
878 | ```
879 | case class Customer(age: Int)
880 | class Cigarettes
881 | case class UnderAgeFailure(age: Int, required: Int)
882 | def buyCigarettes(customer: Customer): Either[UnderAgeFailure, Cigarettes] =
883 | if (customer.age < 16) Left(UnderAgeFailure(customer.age, 16))
884 | else Right(new Cigarettes)
885 | ```
886 | 应该避免使用 Either 来封装意料之外的异常, 使用 Try 来做这种事情会更好,至少它没有 Either 这样那样的缺陷。
887 |
888 | 处理集合
889 |
890 | 有些时候,当按顺序依次处理一个集合时,里面的某个元素产生了意料之外的结果, 但是这时程序不应该直接引发异常,因为这样会使得剩下的元素无法处理。 Either 也非常适用于这种情况。
891 |
892 | 假设,在我们 “行业标准般的” Web 审查系统里,使用了某种黑名单:
893 | ```
894 | type Citizen = String
895 | case class BlackListedResource(url: URL, visitors: Set[Citizen])
896 |
897 | val blacklist = List(
898 | BlackListedResource(new URL("https://google.com"), Set("John Doe", "Johanna Doe")),
899 | BlackListedResource(new URL("http://yahoo.com"), Set.empty),
900 | BlackListedResource(new URL("https://maps.google.com"), Set("John Doe")),
901 | BlackListedResource(new URL("http://plus.google.com"), Set.empty)
902 | )
903 | ```
904 | BlackListedResource 表示黑名单里的网站 URL,外加试图访问这个网址的公民集合。
905 |
906 | 现在我们想处理这个黑名单,为了标识 “有问题” 的公民,比如说那些试图访问被屏蔽网站的人。 同时,我们想确定可疑的 Web 网站:如果没有一个公民试图去访问黑名单里的某一个网站, 那么就必须假定目标对象因为一些我们不知道的原因绕过了筛选器,需要对此进行调查。
907 |
908 | 下面的代码展示了该如何处理黑名单的:
909 | ```
910 | al checkedBlacklist: List[Either[URL, Set[Citizen]]] =
911 | blacklist.map(resource =>
912 | if (resource.visitors.isEmpty) Left(resource.url)
913 | else Right(resource.visitors))
914 | ```
915 | 我们创建了一个 Either 序列,其中 Left 实例代表可疑的 URL, Right 是问题市民的集合。 识别问题公民和可疑网站变得非常简单。
916 | ```
917 | val suspiciousResources = checkedBlacklist.flatMap(_.left.toOption)
918 | val problemCitizens = checkedBlacklist.flatMap(_.right.toOption).flatten.toSet
919 | ```
920 |
921 | Either 非常适用于这种比异常处理更为普通的使用场景。
922 |
923 | **总结**
924 |
925 | 目前为止,你应该已经学会了怎么使用 Either,认识到它的缺陷,以及知道该在什么时候用它。 鉴于 Either 的缺陷,使用不使用它,全都取决于你。 其实在实践中,你会注意到,有了 Try 之后,Either 不会出现那么多糟糕的使用情形。
926 |
927 | 不管怎样,分清楚它带来的利与弊总没有坏处。
928 |
929 | ----------
930 |
931 |
932 | ----------
933 |
934 |
935 | ## 12.4 类型 Future(并行中有无异常时都使用Future)
936 |
937 | 作为一个对 Scala 充满热情的开发者,你应该已经听说过 Scala 处理并发的能力,或许你就是被这个吸引来的。 相较于大多数编程语言低级的并发 API,Scala 提供的方法可以让人们更好的理解并发以及编写良构的并发程序。
938 |
939 | 本章的主题 Future 就是这种方法的两大基石之一。(另一个是 actor) 我会解释 Future 的优点,以及它的函数式特征。
940 |
941 | 如果你想动手试试接下来的例子,请确保 Scala 版本不低于 2.9.3, Future 在 2.10.0 版本中引入,并向后兼容到 2.9.3,最初,它是 Akka 库的一部分(API略有不同)。
942 |
943 | ### 12.4.1 顺序执行的劣势
944 |
945 | 假设你想准备一杯卡布奇诺,你可以一个接一个的执行以下步骤:
946 |
947 | - 研磨所需的咖啡豆
948 | - 加热一些水
949 | - 用研磨好的咖啡豆和热水制做一杯咖啡
950 | - 打奶泡
951 | - 结合咖啡和奶泡做成卡布奇诺
952 |
953 | 转换成 Scala 代码,可能会是这样:
954 | ```
955 | import scala.util.Try
956 |
957 | // Some type aliases, just for getting more meaningful method signatures:
958 | type CoffeeBeans = String
959 | type GroundCoffee = String
960 | case class Water(temperature: Int)
961 | type Milk = String
962 | type FrothedMilk = String
963 | type Espresso = String
964 | type Cappuccino = String
965 |
966 | // dummy implementations of the individual steps:
967 | def grind(beans: CoffeeBeans): GroundCoffee = s"ground coffee of $beans"
968 | def heatWater(water: Water): Water = water.copy(temperature = 85)
969 | def frothMilk(milk: Milk): FrothedMilk = s"frothed $milk"
970 | def brew(coffee: GroundCoffee, heatedWater: Water): Espresso = "espresso"
971 | def combine(espresso: Espresso, frothedMilk: FrothedMilk): Cappuccino = "cappuccino"
972 |
973 | // some exceptions for things that might go wrong in the individual steps
974 | // (we'll need some of them later, use the others when experimenting with the code):
975 | case class GrindingException(msg: String) extends Exception(msg)
976 | case class FrothingException(msg: String) extends Exception(msg)
977 | case class WaterBoilingException(msg: String) extends Exception(msg)
978 | case class BrewingException(msg: String) extends Exception(msg)
979 |
980 | // going through these steps sequentially:
981 | def prepareCappuccino(): Try[Cappuccino] = for {
982 | ground <- Try(grind("arabica beans"))
983 | water <- Try(heatWater(Water(25)))
984 | espresso <- Try(brew(ground, water))
985 | foam <- Try(frothMilk("milk"))
986 | } yield combine(espresso, foam)
987 | ```
988 |
989 | 这样做有几个优点:
990 | 可以很轻易的弄清楚事情的步骤,一目了然,而且不会混淆。(毕竟没有上下文切换) 不好的一面是,大部分时间,你的大脑和身体都处于等待的状态: 在等待研磨咖啡豆时,你完全不能做任何事情,只有当这一步完成后,你才能开始烧水。 这显然是在浪费时间,所以你可能想一次开始多个步骤,让它们同时执行, 一旦水烧开,咖啡豆也磨好了,你可以制做咖啡了,这期间,打奶泡也可以开始了。
991 |
992 | 这和编写软件没什么不同。 一个 Web 服务器可以用来处理和响应请求的线程只有那么多, 不能因为要等待数据库查询或其他 HTTP 服务调用的结果而阻塞了这些可贵的线程。 相反,一个异步编程模型和非阻塞 IO 会更合适, 这样的话,当一个请求处理在等待数据库查询结果时,处理这个请求的线程也能够为其他请求服务。
993 |
994 | >"I heard you like callbacks, so I put a callback in your callback!"
995 |
996 | 在并发家族里,你应该已经知道 nodejs 这个很酷的家伙,nodejs 完全通过回调来通信, 不幸的是,这很容易导致回调中包含回调的回调,这简直是一团糟,代码难以阅读和调试。
997 |
998 | Scala 的 Future 也允许回调,但它提供了更好的选择,所以你不怎么需要它。
999 |
1000 | >"I know Futures, and they are completely useless!"
1001 |
1002 | 也许你知道些其他的 Future 实现,最引人注目的是 Java 提供的那个。 但是对于 Java 的 Future,你只能去查看它是否已经完成,或者阻塞线程直到其结束。 简而言之,Java 的 Future 几乎没有用,而且用起来绝对不会让人开心。
1003 |
1004 | 如果你认为 Scala 的 Future 也是这样,那大错特错了!
1005 |
1006 |
1007 | ----------
1008 |
1009 |
1010 | ### 12.4.2 Future 语义
1011 |
1012 | scala.concurrent 包里的 Future[T] 是一个容器类型,代表一种返回值类型为 T 的计算。 计算可能会出错,也可能会超时;从而,当一个 future 完成时,它可能会包含异常,而不是你期望的那个值。
1013 |
1014 | Future 只能写一次: 当一个 future 完成后,它就不能再被改变了。 同时,Future 只提供了读取计算值的接口,写入计算值的任务交给了 Promise,这样,API 层面上会有一个清晰的界限。 这篇文章里,我们主要关注前者,下一章会介绍 Promise 的使用。
1015 |
1016 | ### 12.4.3 使用 Future
1017 |
1018 | Future 有多种使用方式,我将通过重写 “卡布奇诺” 这个例子来说明。
1019 |
1020 | 首先,**所有可以并行执行的函数,应该返回一个 Future**(需要并行执行的函数应该返回Future类型):
1021 | ```
1022 | import scala.concurrent.future
1023 | import scala.concurrent.Future
1024 | import scala.concurrent.ExecutionContext.Implicits.global
1025 | import scala.concurrent.duration._
1026 | import scala.util.Random
1027 |
1028 | def grind(beans: CoffeeBeans): Future[GroundCoffee] = Future {
1029 | println("start grinding...")
1030 | Thread.sleep(Random.nextInt(2000))
1031 | if (beans == "baked beans") throw GrindingException("are you joking?")
1032 | println("finished grinding...")
1033 | s"ground coffee of $beans"
1034 | }
1035 |
1036 | def heatWater(water: Water): Future[Water] = Future {
1037 | println("heating the water now")
1038 | Thread.sleep(Random.nextInt(2000))
1039 | println("hot, it's hot!")
1040 | water.copy(temperature = 85)
1041 | }
1042 |
1043 | def frothMilk(milk: Milk): Future[FrothedMilk] = Future {
1044 | println("milk frothing system engaged!")
1045 | Thread.sleep(Random.nextInt(2000))
1046 | println("shutting down milk frothing system")
1047 | s"frothed $milk"
1048 | }
1049 |
1050 | def brew(coffee: GroundCoffee, heatedWater: Water): Future[Espresso] = Future {
1051 | println("happy brewing :)")
1052 | Thread.sleep(Random.nextInt(2000))
1053 | println("it's brewed!")
1054 | "espresso"
1055 | }
1056 | ```
1057 | 上面的代码有几处需要解释。
1058 |
1059 | 首先是 Future 伴生对象里的 apply 方法需要两个参数:
1060 | ```
1061 | object Future {
1062 | def apply[T](body: => T)(implicit execctx: ExecutionContext): Future[T]
1063 | }
1064 | ```
1065 | 要异步执行的计算通过传名参数 body 传入。 第二个参数是一个隐式参数,隐式参数是说,函数调用时,如果作用域中存在一个匹配的隐式值,就无需显示指定这个参数。 ExecutionContext 可以执行一个 Future,可以把它看作是一个线程池,是绝大部分 Future API 的隐式参数。
1066 | ```
1067 | import scala.concurrent.ExecutionContext.Implicits.global
1068 | ```
1069 | ⬆语句引入了一个全局的执行上下文,确保了隐式值的存在。 这时候,只需要一个单元素列表,可以用大括号来代替小括号。 调用 future 方法时,经常使用这种形式,使得它看起来像是一种语言特性,而不是一个普通方法的调用。
1070 |
1071 | 这个例子没有大量计算,所以用随机休眠来模拟以说明问题, 而且,为了更清晰的说明并发代码的执行顺序,还在“计算”之前和之后打印了些东西。
1072 |
1073 | 计算会在 Future 创建后的某个不确定时间点上由 ExecutionContext 给其分配的某个线程中执行。
1074 |
1075 | ### 12.4.3 回调(Call back)(极不推荐的使用方式)
1076 |
1077 | 对于一些简单的问题,使用回调就能很好解决。 Future 的回调是偏函数,你可以把回调传递给 Future 的 onSuccess 方法, 如果这个 Future 成功完成,这个回调就会执行,并把 Future 的返回值作为参数输入:
1078 | ```
1079 | grind("arabica beans").onSuccess { case ground =>
1080 | println("okay, got my ground coffee")
1081 | }
1082 | ```
1083 | 类似的,也可以在 onFailure 上注册回调,只不过它是在 Future 失败时调用,其输入是一个 Throwable。
1084 |
1085 | 通常的做法是将两个回调结合在一起以更好的处理 Future:在 onComplete 方法上注册回调,回调的输入是一个 Try。
1086 |
1087 | import scala.util.{Success, Failure}
1088 | grind("baked beans").onComplete {
1089 | case Success(ground) => println(s"got my $ground")
1090 | case Failure(ex) => println("This grinder needs a replacement, seriously!")
1091 | }
1092 | 传递给 grind 的是 “baked beans”,因此 grind 方法会产生异常,进而导致 Future 中的计算失败。
1093 |
1094 | ### 12.4.4 Future 组合
1095 |
1096 | 当嵌套使用 Future 时,回调就变得比较烦人。 不过,你也没必要这么做,因为 Future 是可组合的,这是它真正发挥威力的时候!
1097 |
1098 | 你一定已经注意到,之前讨论过的所有容器类型都可以进行 map 、 flatMap 操作,也可以用在 for 语句中。 作为一种容器类型,Future 支持这些操作也不足为奇!
1099 |
1100 | 真正的问题是,在还没有完成的计算上执行这些操作意味这什么,如何去理解它们?
1101 |
1102 | ### 12.4.5 Map 操作
1103 |
1104 | Scala 让 “时间旅行” 成为可能! 假设想在水加热后就去检查它的温度, 可以通过将 Future[Water] 映射到 Future[Boolean] 来完成这件事情:
1105 | ```
1106 | val tempreatureOkay: Future[Boolean] = heatWater(Water(25)) map { water =>
1107 | println("we're in the future!")
1108 | (80 to 85) contains (water.temperature)
1109 | }
1110 | ```
1111 | tempreatureOkay 最终会包含水温的结果。 你可以去改变 heatWater 的实现来让它抛出异常(比如说,加热器爆炸了), 然后等待 “we're in the future!” 出现在显示屏上,不过你永远等不到。
1112 |
1113 | 写传递给 map 的函数时,你就处在未来(或者说可能的未来)。 一旦 Future[Water] 实例成功完成,这个函数就会执行,只不过,该函数所在的时间线可能不是你现在所处的这个。 如果 Future[Water] 失败,传递给 map 的函数中的事情永远不会发生,调用 map 的结果将是一个失败的 Future[Boolean]。
1114 |
1115 | ### 12.4.6 FlatMap 操作
1116 |
1117 | 如果一个 Future 的计算依赖于另一个 Future 的结果,那需要求救于 flatMap 以避免 Future 的嵌套。
1118 |
1119 | 假设,测量水温的线程需要一些时间,那你可能想异步的去检查水温是否 OK。 比如,有一个函数,接受一个 Water ,并返回 Future[Boolean] :
1120 | ```
1121 | def temperatureOkay(water: Water): Future[Boolean] = Future {
1122 | (80 to 85) contains (water.temperature)
1123 | }
1124 | ```
1125 | 使用 flatMap(而不是 map)得到一个 Future[Boolean],而不是 Future[Future[Boolean]]:
1126 | ```
1127 | val nestedFuture: Future[Future[Boolean]] = heatWater(Water(25)) map {
1128 | water => temperatureOkay(water)
1129 | }
1130 |
1131 | val flatFuture: Future[Boolean] = heatWater(Water(25)) flatMap {
1132 | water => temperatureOkay(water)
1133 | }
1134 | ```
1135 | 同样,映射只会发生在 Future[Water] 成功完成情况下。
1136 |
1137 | ### 12.4.7 for 语句
1138 |
1139 | 除了调用 flatMap ,也可以写成 for 语句。上面的例子可以重写成:
1140 | ```
1141 | val acceptable: Future[Boolean] = for {
1142 | heatedWater <- heatWater(Water(25))
1143 | okay <- temperatureOkay(heatedWater)
1144 | } yield okay
1145 | ```
1146 | 如果有多个可以并行执行的计算,则需要特别注意,要先在 for 语句外面创建好对应的 Futures。
1147 | ```
1148 | def prepareCappuccinoSequentially(): Future[Cappuccino] =
1149 | // 串行执行
1150 | for {
1151 | ground <- grind("arabica beans")
1152 | water <- heatWater(Water(25))
1153 | foam <- frothMilk("milk")
1154 | espresso <- brew(ground, water)
1155 | } yield combine(espresso, foam)
1156 | ```
1157 | 这看起来很漂亮,但要知道,for 语句只不过是 flatMap 嵌套调用的语法糖。 这意味着,只有当 Future[GroundCoffee] 成功完成后, heatWater 才会创建 Future[Water]。 你可以查看函数运行时打印出来的东西来验证这个说法。
1158 |
1159 | 因此,要确保在 for 语句之前实例化所有相互独立的 Futures:
1160 | ```
1161 | def prepareCappuccino(): Future[Cappuccino] = {
1162 | // for之外的部分并行执行
1163 | val groundCoffee = grind("arabica beans")
1164 | val heatedWater = heatWater(Water(20))
1165 | val frothedMilk = frothMilk("milk")
1166 | for {
1167 | // for 里面的全部串行执行
1168 | ground <- groundCoffee
1169 | water <- heatedWater
1170 | foam <- frothedMilk
1171 | espresso <- brew(ground, water)
1172 | } yield combine(espresso, foam)
1173 | }
1174 | ```
1175 | 在 for 语句之前,三个 Future 在创建之后就开始各自独立的运行,显示屏的输出是不确定的。 唯一能确定的是 “happy brewing” 总是出现在后面, 因为该输出所在的函数 brew 是在其他两个函数执行完毕后才开始执行的。 也因为此,可以在 for 语句里面直接调用它,当然,前提是前面的 Future 都成功完成。
1176 |
1177 | ### 12.4.8 失败偏向的 Future
1178 |
1179 | 你可能会发现 Future[T] 是成功偏向的,允许你使用 map、flatMap、filter 等。
1180 |
1181 | 但是,有时候可能处理事情出错的情况。 调用 Future[T] 上的 failed 方法,会得到一个失败偏向的 Future,类型是 Future[Throwable]。 之后就可以映射这个 Future[Throwable],在失败的情况下执行 mapping 函数。
1182 |
1183 | >总结
1184 | 你已经见过 Future 了,而且它的前途看起来很光明! 因为它是一个可组合、可函数式使用的容器类型,这让我们的工作变得异常舒服。
1185 | 调用 future 方法可以轻易将阻塞执行的代码变成并发执行,但是,代码最好原本就是非阻塞的。 为了实现它,我们还需要 Promise 来完成 Future,这就是下一章的主题。
1186 |
1187 |
1188 | ----------
1189 |
1190 |
1191 | ----------
1192 |
1193 | ## 12.5 实战中的 Promise 和 Future
1194 |
1195 | 上一章介绍了 Future 类型,以及如何用它来编写高可读性、高组合性的异步执行代码。
1196 |
1197 | Future 只是整个谜团的一部分: 它是一个只读类型,允许你使用它计算得到的值,或者处理计算中出现的错误。 但是在这之前,必须得有一种方法把这个值放进去。 这一章里,你将会看到如何通过 Promise 类型来达到这个目的。
1198 |
1199 | ### 12.5.1 类型 Promise
1200 |
1201 | 之前,我们把一段顺序执行的代码块传递给了 scala.concurrent 里的 future 方法, 并且在作用域中给出了一个 ExecutionContext,它神奇地异步调用代码块,返回一个 Future 类型的结果。
1202 |
1203 | 虽然这种获得 Future 的方式很简单,但还有其他的方法来创建 Future 实例,并填充它,这就是 Promise。 **Promise 允许你在 Future 里放入一个值**,不过只能做一次,Future 一旦完成,就不能更改了。
1204 |
1205 | 一个 Future 实例总是和一个(也只能是一个)Promise 实例关联在一起。 如果你在 REPL 里调用 future 方法,你会发现返回的也是一个 Promise:
1206 | ```
1207 |
1208 | scala> import concurrent.Future
1209 | import concurrent.Future
1210 |
1211 | scala> import concurrent.future
1212 | import concurrent.future
1213 |
1214 | scala> import concurrent.ExecutionContext.Implicits.global
1215 | import concurrent.ExecutionContext.Implicits.global
1216 |
1217 | scala> val f: Future[String] = future { "Hello World!" }
1218 | f: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@6d78f375
1219 |
1220 | ```
1221 | 你得到的对象是一个 DefaultPromise ,它实现了 Future 和 Promise 接口, 不过这就是具体的实现细节了(译注,有兴趣的读者可翻阅其实现的源码), 使用者只需要知道代码实现把 Future 和对应的 Promise 之间的联系分的很清晰。
1222 |
1223 | 这个小例子说明了:除了通过 Promise,没有其他方法可以完成一个 Future, future 方法也只是一个辅助函数,隐藏了具体的实现机制。
1224 |
1225 | 现在,让我们动动手,看看怎样直接使用 Promise 类型。
1226 |
1227 |
1228 | ----------
1229 |
1230 |
1231 | ### 12.5.2 给出承诺
1232 |
1233 | 当我们谈论起承诺能否被兑现时,一个很熟知的例子是那些政客的竞选诺言。
1234 |
1235 | 假设被推选的政客给他的投票者一个减税的承诺。 这可以用 Promise[TaxCut] 表示:
1236 | ```
1237 | scala> import concurrent.Promise
1238 | import concurrent.Promise
1239 |
1240 | scala> case class TaxCut(reduction: Int)
1241 | defined class TaxCut
1242 |
1243 | // either give the type as a type parameter to the factory method:
1244 | scala> val taxcut = Promise[TaxCut]()
1245 | taxcut: scala.concurrent.Promise[TaxCut] = scala.concurrent.impl.Promise$DefaultPromise@31368b99
1246 |
1247 | // or give the compiler a hint by specifying the type of your val:
1248 | scala> val taxcut2: Promise[TaxCut] = Promise()
1249 | taxcut2: scala.concurrent.Promise[TaxCut] = scala.concurrent.impl.Promise$DefaultPromise@4f6ee6e4
1250 |
1251 | ```
1252 |
1253 | 一旦创建了这个 Promise,就可以在它上面调用 future 方法来获取承诺的未来:
1254 |
1255 | ```
1256 | scala> val taxCutF: Future[TaxCut] = taxcut.future
1257 | taxCutF: scala.concurrent.Future[TaxCut] = scala.concurrent.impl.Promise$DefaultPromise@31368b99
1258 | ```
1259 | 返回的 Future 可能并不和 Promise 一样,但在同一个 Promise 上调用 future 方法总是返回同一个对象, 以确保 Promise 和 Future 之间一对一的关系。
1260 |
1261 |
1262 | ----------
1263 |
1264 |
1265 | ### 12.5.3 结束承诺
1266 |
1267 | 一旦给出了承诺,并告诉全世界会在不远的将来兑现它,那最好尽力去实现。 在 Scala 中,可以结束一个 Promise,无论成功还是失败。
1268 |
1269 | #### 12.5.3.1 兑现承诺
1270 |
1271 | 为了成功结束一个 Promise,你可以调用它的 success 方法,并传递一个大家期许的结果:
1272 | ```
1273 | taxcut.success(TaxCut(20))
1274 | ```
1275 | 这样做之后,Promise 就无法再写入其他值了,如果偏要再写,会产生异常。
1276 |
1277 | 此时,和 Promise 关联的 Future 也成功完成,注册的回调会开始执行, 或者说对这个 Future 进行了映射,那这个时候,映射函数也该执行了。
1278 |
1279 | 一般来说,Promise 的完成和对返回的 Future 的处理发生在不同的线程。 很可能你创建了 Promise,并立即返回和它关联的 Future 给调用者,而实际上,另外一个线程还在计算它。
1280 |
1281 | 为了说明这一点,我们拿减税来举个例子:
1282 | ```
1283 | object Government {
1284 | def redeemCampaignPledge(): Future[TaxCut] = {
1285 | val p = Promise[TaxCut]()
1286 | Future {
1287 | println("Starting the new legislative period.")
1288 | Thread.sleep(2000)
1289 | p.success(TaxCut(20))
1290 | println("We reduced the taxes! You must reelect us!!!!1111")
1291 | }
1292 | p.future
1293 | }
1294 | }
1295 | ```
1296 | 这个例子中使用了 Future 伴生对象,不过不要被它搞混淆了,这个例子的重点是:Promise 并不是在调用者的线程里完成的。
1297 |
1298 | 现在我们来兑现当初的竞选宣言,在 Future 上添加一个 onComplete 回调:
1299 | ```
1300 | import scala.util.{Success, Failure}
1301 |
1302 | val taxCutF: Future[TaxCut] = Government.redeemCampaignPledge()
1303 |
1304 | println("Now that they're elected, let's see if they remember their promises...")
1305 |
1306 | taxCutF.onComplete {
1307 | case Success(TaxCut(reduction)) =>
1308 | println(s"A miracle! They really cut our taxes by $reduction percentage points!")
1309 | case Failure(ex) =>
1310 | println(s"They broke their promises! Again! Because of a ${ex.getMessage}")
1311 | }
1312 | ```
1313 | 多次运行这个例子,会发现显示屏输出的结果顺序是不确定的,而且,最终回调函数会执行,进入成功的那个 case 。
1314 |
1315 | #### 12.5.3.2 违背诺言
1316 |
1317 | 政客习惯违背诺言,Scala 程序员有时候也只能这样做。 调用 failure 方法,传递一个异常,结束 Promise:
1318 | ```
1319 | case class LameExcuse(msg: String) extends Exception(msg)
1320 | object Government {
1321 | def redeemCampaignPledge(): Future[TaxCut] = {
1322 | val p = Promise[TaxCut]()
1323 | Future {
1324 | println("Starting the new legislative period.")
1325 | Thread.sleep(2000)
1326 | p.failure(LameExcuse("global economy crisis"))
1327 | println("We didn't fulfill our promises, but surely they'll understand.")
1328 | }
1329 | p.future
1330 | }
1331 | }
1332 | ```
1333 | 这个 redeemCampaignPledge 实现最终会违背承诺。 一旦用 failure 结束这个 Promise,也无法再次写入了,正如 success 方法一样。 相关联的 Future 也会以 Failure 收场。
1334 |
1335 | 如果已经有了一个 Try,那可以直接把它传递给 Promise 的 complete 方法,以此来结束这个它。 如果这个 Try 是一个 Success,关联的 Future 会成功完成,否则,就失败。
1336 |
1337 | ### 12.5.4 基于 Future 的编程实践
1338 |
1339 | 如果想使用基于 Future 的编程范式以增加应用的扩展性,那应用从下到上都必须被设计成非阻塞模式。 这意味着,基本上应用层所有的函数都应该是异步的,并且返回 Future。
1340 |
1341 | 当下,一个可能的使用场景是开发 Web 应用。 **流行的 Scala Web 框架,允许你将响应作为 Future[Response] 返回,而不是等到你完成响应再返回。 这个非常重要,因为它允许 Web 服务器用少量的线程处理更多的连接。 通过赋予服务器 Future[Response] 的能力,你可以最大化服务器线程池的利用率**。
1342 |
1343 | 而且,应用的服务可能需要多次调用数据库层以及(或者)某些外部服务, 这时候可以获取多个 Future,用 for 语句将它们组合成新的 Future,简单可读! 最终,Web 层再将这样的一个 Future 变成 Future[Response]。
1344 |
1345 | 但是该怎样在实践中实现这些呢?需要考虑三种不同的场景:
1346 |
1347 | #### 12.5.4.1 非阻塞IO
1348 |
1349 | 应用很可能涉及到大量的 IO 操作。比如,可能需要和数据库交互,还可能作为客户端去调用其他的 Web 服务。
1350 |
1351 | 如果是这样,可以使用一些基于 Java 非阻塞 IO 实现的库,也可以直接或通过 Netty 这样的库来使用 Java 的 NIO API。 这样的库可以用定量的线程池处理大量的连接。
1352 |
1353 | 但如果是想开发这样的一个库,直接和 Promise 打交道更为合适。
1354 |
1355 | #### 12.5.4.2 阻塞 IO
1356 |
1357 | 有时候,并没有基于 NIO 的库可用。比如,Java 世界里大多数的数据库驱动都是使用阻塞 IO。 在 Web 应用中,如果用这样的驱动发起大量访问数据库的调用,要记得这些调用是发生在服务器线程里的。 为了避免这个问题,可以将所有需要和数据库交互的代码都放入 future 代码块里,就像这样:
1358 | ```
1359 | // get back a Future[ResultSet] or something similar:
1360 | Future {
1361 | queryDB(query)
1362 | }
1363 | ```
1364 | 到现在为止,我们都是使用隐式可用的全局 ExecutionContext 来执行这些代码块。 通常,更好的方式是创建一个专用的 ExecutionContext 放在数据库层里。 可以从 Java的 ExecutorService 来它,这也意味着,可以异步的调整线程池来执行数据库调用,应用的其他部分不受影响。
1365 | ```
1366 | import java.util.concurrent.Executors
1367 | import concurrent.ExecutionContext
1368 | val executorService = Executors.newFixedThreadPool(4)
1369 | val executionContext = ExecutionContext.fromExecutorService(executorService)
1370 | ```
1371 | #### 12.5.4.3 长时间运行的计算
1372 |
1373 | 取决于应用的本质特点,一个应用偶尔还会调用一些长时间运行的任务,它们完全不涉及 IO(CPU 密集的任务)。 这些任务也不应该在服务器线程中执行,因此需要将它们变成 Future:
1374 | ```
1375 | Future {
1376 | longRunningComputation(data, moreData)
1377 | }
1378 | ```
1379 | 同样,最好有一些专属的 ExecutionContext 来处理这些 CPU 密集的计算。 怎样调整这些线程池大小取决于应用的特征,这些已经超过了本文的范围。
1380 |
1381 | >总结
1382 | 这一章里,我们学习了 Promise - 基于 Future 的并发范式的可写组件,以及怎样用它来完成一个 Future; 同时,还给出了一些在实践中使用它们的建议。
1383 |
1384 |
1385 | ----------
1386 |
1387 |
1388 | ----------
1389 |
1390 | References:
1391 | [1]. http://windor.gitbooks.io/beginners-guide-to-scala/content/index.html
--------------------------------------------------------------------------------
/doc/13 Scala中的异步编程之 Future.markdown:
--------------------------------------------------------------------------------
1 | # 13 Scala中的异步编程之 Future
2 |
3 | 标签(空格分隔): 级别L2:资深类库设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | Scala中的异步编程主要通过`Future`、`Promise`、`Async Library`及`Future Frameworks`中的任一个来实现。
12 | 本篇主要通过具体的示例来展示Scala中异步编程的方方面面。
13 |
14 | ## 13.1 Future实战
15 |
16 | Future[T]是 scala.concurrent 包里的一个容器类型,代表一种返回值类型为 T 的计算。
17 | 这个计算是异步执行的,当一个 Future 计算完成时,如果计算成功,则代表在未来某一个时刻可以获取计算的值。
18 | 如果计算出错,则Future里的不再是值而是包含一个异常。
19 | 在使用 Future 时,只是从里面获取值或异常,所以 Future 只提供了读取计算结果的接口,而把结果写入到 Future 中的任务就交给了 Promise[T] 。
20 |
21 | ### 13.1.1 如何创建一个Future【Future computations】
22 |
23 | ```
24 | /**
25 | * [Example 1]:How to start a future computation in an example.
26 | */
27 | object FuturesComputation extends App {
28 |
29 | // we first import the contents of the scala.concurrent package
30 | import scala.concurrent._
31 |
32 | // we then import the global execution context from the Implicits object.
33 | // This makes sure that the future computations execute on global-the default
34 | // execution context you can use in most cases:
35 | import ExecutionContext.Implicits.global
36 |
37 |
38 | /**
39 | * The order, in which the log method calls(in the future computation and the main thread)
40 | * execute, is nondeterministic(random).
41 | */
42 | Future {
43 | log(s"the future is here")
44 | }
45 |
46 | log(s"the future is coming")
47 |
48 | // to sleep 1 second in order to Future can finish and then exit main thread.
49 | Thread.sleep(1000)
50 | }
51 |
52 | ```
53 |
54 |
55 | ----------
56 |
57 | ### 13.1.2 通过轮询(polling)的方式获取值【不推荐的使用方式】
58 |
59 | ```
60 | /**
61 | * [Example 2]: poll future value until it is completed
62 | *
63 | * In this example, we used polling to obtain the value of the future.
64 | * Polling的方式就是每隔一段时间就会询问一次异步代码块是否结束,没有结束,值就为None。
65 | * 如果结束,就可以获得异步代码块的返回值。
66 | *
67 | * Polling在Future中就是通过isCompleted方法和value实现的
68 | */
69 | object FuturesDataType extends App {
70 | import scala.concurrent._
71 | import ExecutionContext.Implicits.global
72 | import scala.io.Source
73 |
74 | val buildFile: Future[String] = Future {
75 | val f = Source.fromFile("build.sbt")
76 | try f.getLines().mkString("\n") finally f.close()
77 | }
78 |
79 | log(s"started reading build file asynchronously")
80 |
81 | // The Future singleton object's polling methods(such as isCompleted)
82 | // are non-blocking, but they are also nondeterministic.
83 | log(s"status: ${buildFile.isCompleted}")
84 |
85 | log(s"status: ${buildFile.value}") // print None
86 |
87 | Thread.sleep(250)
88 | log(s"status: ${buildFile.isCompleted}")
89 | log(s"status: ${buildFile.value}")
90 | }
91 | ```
92 |
93 | ----------
94 |
95 | ### 13.1.3 Future中的回调(callbacks)【代替上面轮询的方式】
96 |
97 | ```
98 | /**
99 | * [Example 3] [Future callbacks]
100 | * A callback is a function that is called once its arguments become available.
101 | *
102 | * 我们真正需要的不是不停的询问是否异步代码块是否执行结束,而是留下一个方法,
103 | * 等异步代码块结束后能够自动调用这个方法来返回结果。
104 | * 这个就是Future中的callbacks方法干的事儿。
105 | *
106 | * Scala Future callbacks: foreach, onSuccess[deprecated after 2.11]
107 | * The foreach method only accepts callbacks that handle values from
108 | * a successfully completed future.
109 | *
110 | * 通过foreach method 只能处理Future成功后的情况
111 | */
112 | object FuturesCallbacks extends App {
113 | import scala.concurrent._
114 | import ExecutionContext.Implicits.global
115 | import scala.io.Source
116 |
117 | def getUrlSpec: Future[Seq[String]] = Future {
118 | val f = Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt")
119 | try f.getLines().toList finally f.close()
120 | }
121 |
122 | val urlSpec: Future[Seq[String]] = getUrlSpec
123 |
124 | /**
125 | * collect 方法示例:得到 (4, 16, 36, 64, 100)
126 | * 写法1:(1 to 10) filter (_%2==0) map (x=>x*x)
127 | * 写法2:for(x<-1 to 10 if x%2==0) yield x*x
128 | * 写法3:(1 to 10) collect { case x if x%2==0 => x*x }
129 | *
130 | * @param lines
131 | * @param word
132 | * @return
133 | */
134 | def find(lines: Seq[String], word: String) = lines.zipWithIndex collect {
135 | case (line, n) if line.contains(word) => (n, line)
136 | } mkString "\n"
137 |
138 | // we install a callback to the future using the foreach method.
139 | // the equivalent of foreach method is called onSuccess, but might
140 | // be deprecated after Scala 2.11.
141 | urlSpec foreach {
142 | lines => log(s"Found occurrences of 'telnet'\n${find(lines, "telnet")}\n")
143 | }
144 |
145 | // The foreach method only accepts callbacks that handle values from
146 | // a successfully completed future
147 | urlSpec foreach {
148 | // 2 but the log statement in the callback can be called much later.
149 | lines => log(s"Found occurrences of 'password'\n${find(lines, "password")}\n")
150 | }
151 |
152 | // 1 The log statement in the main thread immediately executes after
153 | // the callback is registered
154 | log("callbacks installed, continuing with other work")
155 |
156 | Thread.sleep(2000)
157 | }
158 | ```
159 |
160 | ----------
161 |
162 | ### 13.1.4 Future中的异常处理
163 |
164 | ```
165 | /**
166 | * [Example 4] [Futures and exceptions]
167 | * Just to handle failure.
168 | *
169 | * How to handle failures in asynchronous computations
170 | * we need another method to install failure callbacks, this method is called failed.
171 | *
172 | * 假定Future失败后的处理,通过failed method.
173 | */
174 | object FuturesFailure extends App {
175 | import scala.concurrent._
176 | import ExecutionContext.Implicits.global
177 | import scala.io.Source
178 |
179 | val urlSpec: Future[String] = Future {
180 | Source.fromURL("http://www.w3.org/non-existent-url-spec.txt").mkString
181 | }
182 |
183 | // The failed method is a failure callbacks that handle exception from a failure future
184 | urlSpec.failed foreach {
185 | case t => log(s"exception occurred - $t")
186 | }
187 |
188 | Thread.sleep(5000)
189 | }
190 | ```
191 |
192 |
193 | ----------
194 |
195 |
196 | ### 13.1.5 同时处理成功或异常的Future
197 |
198 | ```
199 | /**
200 | * [Example 5]
201 | * Using the Try type for sometimes we want to subscribe to both
202 | * successes and failures in the same callback
203 | *
204 | * Future 在异步的情况下处理成功或失败
205 | * 同时处理成功和失败的情况, 通过onComplete method处理。
206 | */
207 | object FuturesExceptions extends App {
208 | import scala.concurrent._
209 | import ExecutionContext.Implicits.global
210 | import scala.io.Source
211 |
212 | val file = Future { Source.fromFile(".gitignore-SAMPLE").getLines.mkString("\n") }
213 |
214 | file foreach { // 异步调用: 只处理成功的情况,忽略失败的情况
215 | text => log(text)
216 | }
217 |
218 | file.failed foreach { // 异步调用: 只处理失败的情况,忽略成功的情况
219 | case fnfe: java.io.FileNotFoundException => log(s"Cannot find file - $fnfe")
220 | case t => log(s"Failed due to $t")
221 | }
222 |
223 | import scala.util.{Try, Success, Failure}
224 |
225 | // Using the Try type for sometimes we want to subscribe to both successes
226 | // and failures in the same callback
227 | // The callback is onComplete method
228 | file onComplete { // 异步调用: 同时处理成功和失败的情况
229 | case Success(text) => log(text)
230 | case Failure(t) => log(s"Failed due to $t")
231 | }
232 | }
233 | ```
234 |
235 |
236 | ----------
237 |
238 | ### 13.1.6 Future VS Try
239 |
240 | Future是异步的处理计算,并返回成功值或异常
241 | Try是同步的处理计算,并返回成功值或异常
242 |
243 | ```
244 | /**
245 | * The Try[T] objects are immutable objects used synchronously;
246 | * unlike futures, they contain a value or an exception from the moment they are created.
247 | * They are more akin to collections than to futures.
248 | *
249 | * Try 只能在同步的情况下处理成功或失败
250 | */
251 | object FuturesTry extends App {
252 | import scala.util._
253 |
254 | val threadName: Try[String] = Try(Thread.currentThread.getName)
255 | val someText: Try[String] = Try("Try objects are created synchronously")
256 |
257 | val message: Try[String] = for {
258 | tn <- threadName
259 | st <- someText
260 | } yield s"$st, t = $tn"
261 |
262 | message match {
263 | case Success(msg) => log(msg)
264 | case Failure(error) => log(s"There should be no $error here.")
265 | }
266 | }
267 | ```
268 |
269 |
270 | ----------
271 |
272 | ### 13.1.7 Future的计算不能捕获致命异常
273 |
274 | Future在异步计算过程中,可能会出现异常情况,则其返回值就是一个异常。
275 | 但是如果出现致命异常(InterruptedException)时,不能捕获而导致程序中断。
276 |
277 | ```
278 | /**
279 | * Future computations do not catch fatal errors.
280 | */
281 | object FuturesNonFatal extends App {
282 | import scala.concurrent._
283 | import ExecutionContext.Implicits.global
284 |
285 | val f = Future { throw new InterruptedException } // fatal exception
286 | val g = Future { throw new IllegalArgumentException } // non fatal exception
287 | f.failed foreach { case t => log(s"error - $t") }
288 | g.failed foreach { case t => log(s"error - $t") }
289 |
290 | Thread.sleep(1000)
291 |
292 | /**
293 | * run result:
294 | * java.lang.InterruptedException
295 | * at org.learningconcurrency.ch4.FuturesNonFatal$$anonfun$17.apply(Futures.scala:153)
296 | * at org.learningconcurrency.ch4.FuturesNonFatal$$anonfun$17.apply(Futures.scala:153)
297 | * at scala.concurrent.impl.Future$PromiseCompletingRunnable.liftedTree1$1(Future.scala:24)
298 | * at scala.concurrent.impl.Future$PromiseCompletingRunnable.run(Future.scala:24)
299 | * at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121)
300 | * at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
301 | * at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
302 | * at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
303 | * at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
304 | * ForkJoinPool-1-worker-5: error - java.lang.IllegalArgumentException
305 | *
306 | * Summary: Future computations do not catch fatal errors.
307 | */
308 | }
309 | ```
310 |
311 |
312 | ----------
313 |
314 | ### 13.1.8 Future 中的 map
315 |
316 | ```
317 | /**
318 | * map就是一个Functor,用于把 A => B
319 | * 例如:List[Int] => List[String]
320 | */
321 | object FuturesMap extends App {
322 | import scala.concurrent._
323 | import ExecutionContext.Implicits.global
324 | import scala.io.Source
325 | import scala.util.Success
326 |
327 | val buildFile: Future[Iterator[String]] = Future { Source.fromFile("build.sbt").getLines }
328 | // 通过map,Future[Iterator[String]] => Future[String] 的转变,找出最长的一行
329 | val longestBuildLine: Future[String] = buildFile.map(lines => lines.maxBy(_.length))
330 |
331 | val gitignoreFile = Future { Source.fromFile(".gitignore-SAMPLE").getLines }
332 | val longestGitignoreLine = for (lines <- gitignoreFile) yield lines.maxBy(_.length)
333 |
334 | longestBuildLine onComplete {
335 | case Success(line) => log(s"the longest line is '$line'")
336 | }
337 |
338 | longestGitignoreLine.failed foreach {
339 | case t => log(s"no longest line, because ${t.getMessage}")
340 | }
341 | }
342 | ```
343 |
344 |
345 | ----------
346 |
347 | ### 13.1.9 组合多个Future 【不推荐的方式】
348 |
349 | ```
350 | object FuturesFlatMapRaw extends App {
351 | import scala.concurrent._
352 | import ExecutionContext.Implicits.global
353 | import scala.io.Source
354 |
355 | val netiquette = Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
356 | val urlSpec = Future { Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString }
357 |
358 | // 可读性差
359 | val answer = netiquette.flatMap { nettext =>
360 | urlSpec.map { urltext =>
361 | "First, read this: " + nettext + ". Now, try this: " + urltext
362 | }
363 | }
364 |
365 | answer foreach {
366 | case contents => log(contents)
367 | }
368 | }
369 | ```
370 |
371 |
372 | ----------
373 |
374 | ### 13.1.10 通过 for 表达式来组合多个 Future 【推荐】
375 |
376 | - **异步** 的方式使用 for 表达式
377 |
378 | ```
379 | /**
380 | * Prefer for-comprehensions to using flatMap directly to make
381 | * programs more concise and understandable.
382 | */
383 | object FuturesFlatMap extends App {
384 | import scala.concurrent._
385 | import ExecutionContext.Implicits.global
386 | import scala.io.Source
387 |
388 | val netiquette = Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
389 | val urlSpec = Future {
390 | Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString
391 | }
392 |
393 | // 可读性好
394 | val answer = for {
395 | nettext <- netiquette
396 | urltext <- urlSpec
397 | } yield {
398 | "First of all, read this: " + nettext + " Once you're done, try this: " + urltext
399 | }
400 |
401 | answer foreach {
402 | case contents => log(contents)
403 | }
404 |
405 | }
406 | ```
407 |
408 |
409 | ----------
410 |
411 | - **顺序** 的方式使用 for 表达式
412 |
413 | ```
414 | /**
415 | * The nettext value is extracted from the first future.
416 | * Only after the first future is completed, the second future computation starts
417 | */
418 | object FuturesDifferentFlatMap extends App {
419 | import scala.concurrent._
420 | import ExecutionContext.Implicits.global
421 | import scala.io.Source
422 |
423 | val answer = for {
424 | nettext <- Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
425 | urltext <- Future { Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString }
426 | } yield {
427 | "First of all, read this: " + nettext + " Once you're done, try this: " + urltext
428 | }
429 |
430 | answer foreach {
431 | case contents => log(contents)
432 | }
433 |
434 | }
435 | ```
436 |
437 |
438 | ----------
439 |
440 |
441 | ### 13.1.11 通过 scala-async library来组合多个 Future
442 |
443 | - **异步** 的方式使用 scala-async library
444 |
445 | ```
446 | object FuturesFlatMap extends App {
447 | import scala.concurrent._
448 | import ExecutionContext.Implicits.global
449 | import scala.io.Source
450 | import scala.async.Async.{async, await}
451 |
452 | val netiquette = Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
453 | val urlSpec = Future {
454 | Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString
455 | }
456 |
457 | // 异步方式
458 | val answer = async {
459 | "First of all, read this: " + await(netiquette) +
460 | " Once you're done, try this: " + await(urlSpec)
461 | }
462 |
463 | answer foreach {
464 | case contents => log(contents)
465 | }
466 |
467 | }
468 | ```
469 |
470 | ----------
471 |
472 | - **同步** 的方式使用 scala-async library
473 |
474 | ```
475 | object FuturesFlatMap extends App {
476 | import scala.concurrent._
477 | import ExecutionContext.Implicits.global
478 | import scala.io.Source
479 | import scala.async.Async.{async, await}
480 |
481 | // 同步方式
482 | val answer = async {
483 | "First of all, read this: " +
484 | await(Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }) +
485 | " Once you're done, try this: " +
486 | await(Future {
487 | Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString
488 | })
489 | }
490 |
491 | answer foreach {
492 | case contents => log(contents)
493 | }
494 |
495 | }
496 | ```
497 |
498 |
499 | ----------
500 |
501 | ### 13.1.12 对异常的 Future 进行恢复
502 |
503 | ```
504 | /**
505 | * So far, we have only considered future combinators that work with successful futures.
506 | * When any of the input futures fails or the computation in the combinator throws an
507 | * exception, the resulting future fails with the same exception.
508 | *
509 | * In some situations, we want to handle the exception in the future in the same way as
510 | * we handle exception with a try-catch block in sequential programming.
511 | *
512 | * A combinator that is helpful in these situations is called recover.
513 | * Using the recover combinator on the Future to provide a default operation
514 | * if anything fails.
515 | */
516 | object FuturesRecover extends App {
517 | import scala.concurrent._
518 | import ExecutionContext.Implicits.global
519 | import scala.io.Source
520 |
521 | val netiquetteUrl = "http://www.ietf.org/rfc/rfc1855.doc"
522 | val netiquette = Future { Source.fromURL(netiquetteUrl).mkString } recover {
523 | case f: java.io.FileNotFoundException =>
524 | "Dear boss, thank you for your e-mail." +
525 | "You might be interested to know that ftp links " +
526 | "can also point to regular files we keep on our servers."
527 | }
528 |
529 | netiquette foreach {
530 | case contents => log(contents)
531 | }
532 |
533 | }
534 | ```
535 |
536 |
537 | ----------
538 |
539 | ### 13.1.13 Future中的常用方法
540 |
541 | #### 13.1.13.1 reduce 处理Future列表(List[Future[A]])
542 |
543 | ```
544 | /**
545 | * reduce方法用于对一个Future列表进行fold计算,其中,初始值为第一个计算完成的Future的值。
546 | */
547 | object FuturesReduce extends App {
548 | import scala.concurrent._
549 | import ExecutionContext.Implicits.global
550 |
551 | // 理想情况: 列表中的Future全部都成功
552 | val squares: Seq[Future[Int]] = for (i <- 0 until 10) yield Future { i * i }
553 | // reduce: 完成 Seq[Future[Int]] => Future[Int] 的转换
554 | val sumOfSquares: Future[Int] = Future.reduce(squares)(_ + _)
555 |
556 | sumOfSquares foreach {
557 | case sum => log(s"Sum of squares = $sum")
558 | }
559 |
560 | // 现实情况就是:不可能所有Future都是成功的
561 | val futureWithException = List(Future( 1 + 2), Future(throw new RuntimeException("hello")))
562 | val reduceFutures = Future.reduce(futureWithException)(_ + _)
563 |
564 | // 不会被执行
565 | reduceFutures foreach { num => // 只处理成功的情况
566 | log(s"plus with exception = $num")
567 | }
568 |
569 | // 被调用执行,其结果是列表中第一个最新计算完成的异常的Future
570 | reduceFutures.failed foreach { e => // 只处理失败的情况
571 | log(s"plus with exception = $e")
572 | }
573 |
574 | Thread.sleep(2000)
575 | }
576 | ```
577 |
578 |
579 | ----------
580 |
581 | #### 13.1.13.2 traverse
582 |
583 | ```
584 | /**
585 | * traverse方法用于 List[A] ==> Future[List[A]]的转换
586 | * sequence方法用于 List[Future[A]] ==> Future[List[A]]的转换
587 | *
588 | * {{{
589 | * http://qiita.com/mtoyoshi/items/297f6acdfe610440c719
590 | * }}}
591 | */
592 | object FuturesTraverse extends App {
593 |
594 | import scala.concurrent._
595 | import ExecutionContext.Implicits.global
596 |
597 | val list = List(1, 2, 3, 4, 5)
598 |
599 | // 这种方式会使用list的size个工作线程,这里是使用了5个线程
600 | val listFutures: List[Future[Int]] = list map (num => Future(num * num))
601 | listFutures.foreach { case future =>
602 | future foreach {
603 | num => log(s"num in future is $num")
604 | }
605 | }
606 |
607 | log("the message is printed by main thread!")
608 |
609 | // 这种方式只会使用一个工作线程
610 | val futureList: Future[List[Int]] = Future.traverse(list)(num => Future(num * num))
611 | futureList.foreach{ case listNum =>
612 | listNum foreach {
613 | num => log(s"num in list is $num")
614 | }
615 | }
616 | log("the message2 is printed by main thread!")
617 |
618 | }
619 | ```
620 |
621 |
622 | ----------
623 |
624 | ```
625 | /**
626 | * 如果只要列表中有一个Future是异常的,那么就不进行任何处理的情况,
627 | * 可以使用下面的例子的这种写法
628 | */
629 | object FuturesWithExceptionTraverse extends App {
630 |
631 | import scala.concurrent._
632 | import ExecutionContext.Implicits.global
633 | import scala.util.{Try, Success, Failure}
634 |
635 | val list = List(1, 2, 3, 4, 5)
636 |
637 | // 这种方式会使用list的size个工作线程,这里是使用了5个线程【不优雅,异常能够处理到】
638 | // 这种正常的Future和异常的Future组成的集合,都可以处理到,但是方式不太好
639 | val listFutures: List[Future[Int]] = list map {num =>
640 | if (num % 2 == 0) Future(throw new RuntimeException(s"hello$num"))
641 | else Future(num * num)
642 | }
643 |
644 | listFutures.foreach { case future =>
645 | future foreach {
646 | num => log(s"num in future is $num")
647 | }
648 | future.failed foreach {
649 | num => log(s"exception in future is $num")
650 | }
651 | }
652 |
653 | log("the message is printed by main thread!")
654 |
655 | // 这种方式只会使用一个工作线程【优雅,异常处理弱】
656 | // 这种方式是如果List中只要有一个异常的Future,最终的Future其实就是List中的第一个异常的Future
657 | // List[Future[Int]] => Future[List[Int]]
658 | val futureList: Future[List[Int]] = Future.traverse(list)(num =>
659 | if (num % 2 == 0)
660 | Future(throw new RuntimeException(s"hello$num"))
661 | else Future(num * num)
662 | )
663 |
664 | futureList.failed.foreach { case e =>
665 | log(s"exception in list is $e")
666 | }
667 |
668 | log("the message2 is printed by main thread!")
669 |
670 |
671 | /**
672 | * Future 里面的所有的call back,都是异步执行的。
673 | */
674 | }
675 | ```
676 |
677 |
678 | ----------
679 |
680 | #### 13.1.13.3 开发中的实用例子(sequence)
681 |
682 | ```
683 | /**
684 | * 一个List中的Future,有的是成功的,有的是异常的。
685 | * 普通的做法就是把 List[Future[A]] ==> Future[List[A]],
686 | * 但是在这个过程中,如果有一个异常的Future,成功的Future全部被忽略,
687 | * 最早确定失败的Future最为最后结果返回。
688 | *
689 | * 这显然不是我们需要的。我们在实际的开发中,需要的是把List中成功的Future进行计算,
690 | * 把失败的所有的Future进行异常输出
691 | */
692 | object FuturesWithException extends App {
693 |
694 | import scala.concurrent._
695 | import ExecutionContext.Implicits.global
696 | import scala.util.{Try, Success, Failure}
697 |
698 | log("this log is printed by main thread: begin")
699 |
700 | /**
701 | * List[Future[Int]] => List[Future[Try[Int]]]
702 | * @param f
703 | * @return
704 | */
705 | def future2FutureTry(f: Future[Int]): Future[Try[Int]] =
706 | f.map(Success(_)).recover { case e => Failure(e) }
707 |
708 | // 把异常的Future的异常信息输出,正常的Future进行结果的计算。
709 | // 那么如何做呢?
710 |
711 | // 第一种方式
712 | val list = List(1, 2, 3, 4, 5)
713 |
714 | // 首先生成一个 List[Future[Int]]
715 | val listFutures: List[Future[Int]] = list map { num =>
716 | if (num % 2 == 0) Future(throw new RuntimeException(s"hello$num"))
717 | else Future(num * num)
718 | }
719 |
720 | // 然后把 List[Future[Int]] => List[Future[Try[Int]]]
721 | val listFutureTry = listFutures.map(future2FutureTry)
722 |
723 | // 再把 List[Future[Try[Int]]] => Future[List[Try[Int]]]
724 | val futureListTry = Future.sequence(listFutureTry)
725 |
726 | // 最后分别处理成功的Future和异常的Future
727 | futureListTry.map(_.collect { case Success(num) => log(s"num in future is $num") })
728 | futureListTry.map(_.collect { case Failure(e) => log(s"exception in future is $e") })
729 |
730 | log("this log is printed by main thread: end")
731 | Thread.sleep(4000)
732 | }
733 | ```
734 |
735 |
736 | ----------
737 |
738 | 第二种方式:
739 |
740 | ```
741 | object FuturesWithException2 extends App {
742 |
743 | import scala.concurrent._
744 | import ExecutionContext.Implicits.global
745 | import scala.util.{Try, Success, Failure}
746 |
747 | log("this log is printed by main thread: begin")
748 |
749 | // 把异常的Future的异常信息输出,正常的Future进行结果的计算。
750 | // 那么如何做呢?
751 |
752 | // 第二种方式,直接一步到位,生成Future[List[Try[Int]]]
753 | val list = List(1, 2, 3, 4, 5)
754 |
755 | val futureListTry: Future[List[Try[Int]]] = Future.traverse(list){num =>
756 | val future = if (num % 2 == 0)
757 | Future(throw new RuntimeException(s"hello$num"))
758 | else Future(num * num)
759 |
760 | future.map(Success(_)).recover{case e => Failure(e)}
761 | }
762 |
763 | // 最后分别处理成功的Future和异常的Future
764 | futureListTry.map(_.collect { case Success(num) => log(s"num in future is $num") })
765 | futureListTry.map(_.collect { case Failure(e) => log(s"exception in future is $e") })
766 |
767 | log("this log is printed by main thread: end")
768 | Thread.sleep(4000)
769 | }
770 |
771 | ```
772 |
773 | ----------
774 |
775 |
776 | ----------
777 |
778 |
779 | References:
780 | [1]. learning concurrent programming in Scala
781 | [2]. http://qiita.com/mtoyoshi/items/297f6acdfe610440c719
--------------------------------------------------------------------------------
/doc/2 Scala中的对象.markdown:
--------------------------------------------------------------------------------
1 | # 2 Scala中的对象
2 |
3 | 标签(空格分隔): 级别A1:初级程序设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | 本篇通过一个问题引出下面的内容。
12 | 何时使用Scala的object语法?也就是何时使用object关键字定义一个对象?
13 | 答案就是:你需要某个类的单个实例时(单例对象),或者为一些值或方法提供一个namespace。就可以使用该语法。
14 |
15 |
16 | ## 2.1 用对象作为单例或存放Util方法
17 |
18 | Scala没有静态方法或静态字段,可以把这些方法或字段放在一个单例对象中。例如:
19 | ```
20 | // 单例对象(Singletons)
21 | object Accounts {
22 | private var lastNumber = 0
23 | def newUniqueNumber() = { lastNumber += 1; lastNumber }
24 | }
25 | ```
26 | 然后通过`对象.方法名`即可进行调用。
27 |
28 | 单例对象的用途:
29 | - 作为存放工具方法或常量的地方
30 | - 高效的共享单个不可变实例
31 | - 需要用单个实例来协调某个服务时
32 |
33 | ---
34 |
35 | ---
36 |
37 | ## 2.2 类可以拥有一个同名的伴生对象
38 |
39 | 在Java中,通常需要既有实例方法又有静态方法的类。在Scala中,你可以通过类和与类同名的伴生对象来达到同样的目的。
40 | ```
41 | class Account {
42 | val id = Account.newUniqueNumber()
43 | private var balance = 0.0
44 | def deposit(amount: Double) { balance += amount }
45 | //...
46 | }
47 |
48 | object Account { // The companion object
49 | private var lastNumber = 0
50 | private def newUniqueNumber() = { lastNumber += 1; lastNumber }
51 | }
52 |
53 | ```
54 | 类和它的伴生对象可以相互访问私有成员。类的伴生对象可以被访问,但并不在作用域当中。比如,Account类必须通过Account.newUniqueNumber()而不是直接用newUniqueNumber()来调用伴生对象的方法。
55 | 另外,它们必须存在于同一个文件中。
56 |
57 | ---
58 |
59 | ---
60 |
61 | ## 2.3 对象可以扩展类Class或特质Trait
62 |
63 | 一个object可以扩展类以及一个或多个Trait,其结果是一个扩展了指定类以及Trait的对象,同时拥有在对象定义中给出的所有特性。
64 |
65 | 一个有用的场景就是使用共享对象,就是一个对象提供若干方法,在任何地方都可以引入这个对象,并使用其提供的方法。
66 |
67 | 例如:
68 | ```
69 | abstract class UndoableAction(val description: String) {
70 | def undo(): Unit
71 | def redo(): Unit
72 | }
73 | ```
74 |
75 | 默认情况下可以是什么都不做。对于这个行为我们只需要一个实例即可。
76 |
77 | ```
78 | object DoNothingAction extends UndoableAction("Do nothing") {
79 | override def undo() {}
80 | override def redo() {}
81 | }
82 |
83 | ```
84 |
85 | DoNothingAction对象可以被所有需要这个缺省行为的地方共用。
86 |
87 | ```
88 | object Run extends App {
89 | val actions = Map("open" -> DoNothingAction, "save" -> DoNothingAction /*, ...*/ )
90 | }
91 |
92 | ```
93 |
94 | ---
95 |
96 | ---
97 |
98 | ## 2.4 对象的apply方法通常用来构造伴生类的新实例
99 |
100 | 我们通常会定义和使用对象的apply方法。apply方法被定义在伴生对象中,当做工厂方法来生产伴生类的对象。当遇到如下形式的表达式时,apply方法就会被调用:
101 |
102 | ```
103 | Object(参数1, ..., 参数N)
104 | ```
105 | 举例来说,Array对象定义了apply方法,让我们可以用下面这样的表达式来创建数组:
106 | ```
107 | Array("hello", "world")
108 | ```
109 |
110 | 为什么不使用构造器呢?对于嵌套表达式而言,省去new关键字会方便很多,例如:
111 | ```
112 | Array(Array(1, 7), Array(2, 9))
113 | ```
114 | >注意: Array(100)和new Array(100)的区别。
115 | Array(100)调用的是apply(100),输出一个元素(整数100)的Array[Int];
116 | new Array(100)调用的是构造器this(100),结果是Array[Nothing],包含了100个null元素。
117 |
118 | 一个使用apply方法的具体例子:
119 | ```
120 | // 对伴生类的主构造方法进行私有化,对外不可见,不能通过new的方式实例化Account对象
121 | class Account private (val id: Int, initialBalance: Double) {
122 | private var balance = initialBalance
123 | }
124 |
125 | // 由于伴生对象和伴生类之间可以互相访问私有成员,所以在伴生对象中是可以通过new的方式实例化Account对象的(也就是可以访问私有化的主构造方法)
126 | osbject Account {
127 | // 工厂方法生产伴生类的对象
128 | def apply(initialBalance: Double) =
129 | new Account(0, initialBalance)
130 | }
131 |
132 | object Run extends App {
133 | val acct = Account(1000.0) // 等价于Account.apply(1000.0)
134 | }
135 |
136 | ```
137 |
138 | ---
139 |
140 | ---
141 |
142 |
143 | ## 2.5 不显示定义main方法,可以使用扩展App特质的对象
144 |
145 | 扩展App特质的对象称为应用程序对象。每个Scala程序都必须从一个对象的main方法开始,这个方法的类型为`Array[String] => Unit`,表示接收一个字符串数组类型的参数,返回空值Unit。
146 |
147 | ```
148 | object Hello {
149 | def main(args: Array[String]) {
150 | println("Hello, World!")
151 | }
152 | }
153 | ```
154 | 除了每次都提供自己的main方法外,也可以通过一个对象扩展App特质,然后将程序代码放入构造器方法体内:
155 |
156 | ```
157 | object Hello extends App {
158 | println("Hello, World!")
159 | }
160 | ```
161 | 如果你需要命令行参数,则可以通过args属性得到:
162 | ```
163 | object Hello extends App {
164 | if (args.length > 0)
165 | println(s"Hello, ${args(0)}")
166 | else
167 | println("Hello, World!")
168 | }
169 | ```
170 |
171 | 如果你在调用该程序时设置了`scala.time`选项的话,程序退出时会显示执行的时间。
172 | ```
173 | $ scalac Hello.scala
174 | $ scala -Dscala.time Hello Jack
175 | Hello, Jack
176 |
177 | ```
178 | App特质扩展自另一个特质DelayedInit,编译器对该特质有特殊处理。所有带有该特质的类,其初始化方法都会被挪到delayedInit方法中。App特质的main方法捕获到命令行参数,调用delayedInit方法,并且还可以根据需要打印出执行时间。
179 |
180 | >说明,较早版本有一个Application的特质来达到同样的目的。那个特质是在静态初始化方法中执行程序动作,并不被即时编译器优化
181 |
182 | ---
183 |
184 | ---
185 |
186 | ## 2.6 扩展Enumeration对象来实现枚举
187 |
188 | Scala中并没有枚举类型。但是标准库提供了一个Enumeration的类,可以用于枚举场合。
189 | 定义一个扩展Enumeration类的对象并以Value方法调用初始化枚举中的所有可选值。例如:
190 | ```
191 | object TrafficLightColor extends Enumeration {
192 | val Red, Yellow, Green = Value
193 | }
194 | ```
195 | 在这里定义了三个字段: Red, Yellow和Green,然后用Value调用将它们初始化。这是如下代码的简写:
196 | ```
197 | val Red = Value
198 | val Yellow = Value
199 | val Green = Value
200 |
201 | ```
202 | 每次调用Value方法都返回内部类的新实例,该内部类也叫做Value。
203 | 也就是每一个字段都是Value的一个实例对象。
204 | 或者,你也可以向Value方法传入ID,名称,或两个参数都传:
205 | ```
206 | val Red = Value(0, "Stop") // ID默认为从0开始
207 | val Yellow = Value(10) // 名称为“Yellow”
208 | val Green = Value("Go") // ID为11
209 |
210 | ```
211 | >说明:枚举中的字段有两个值,一个ID,一个名称。
212 | ID的值从0开始,如果不指定,则ID在前一个枚举值的基础上加一。
213 | 名称不指定,缺省名称为字段名。
214 |
215 | 定义完成后,你就可以用TrafficLightColor.Red,TrafficLightColor.Yellow等来引用枚举值。枚举值的类型是TrafficLightColor.Value而不是TrafficLightColor--后者是拥有这些值的对象。有人推荐增加一个类型别名:
216 | ```
217 | object TrafficLightColor extends Enumeration {
218 | type TrafficLightColor = Value
219 | val Red, Yellow, Green = Value
220 | }
221 |
222 | ```
223 | 现在枚举的类型就变成了TrafficLightColor.TrafficLightColor,但仅当你使用import时这样做才有意义。
224 | 例如:
225 | ```
226 | import TrafficLightColor._
227 | def doWhat(color: TrafficLightColor) = {
228 | if (color == Red) "stop"
229 | else if (color == Yellow) "hurry up"
230 | else "go"
231 | }
232 |
233 | ```
234 | 枚举的ID可以通过id方法返回,比如: Red.id
235 | 对TrafficLightColor.values调用输出所有枚举值的集:
236 | ```
237 | for(c <- TrafficLightColor.values) println(c.id + ":" + c)
238 |
239 | ```
240 | 最后,你可以通过枚举的ID或名称来进行定位,以下两段代码都输出TrafficLightColor.Red对象:
241 | ```
242 | TrafficLightColor(0) // 将调用Enumeration.apply(0)
243 | TrafficLightColor.withName("Red")
244 | ```
245 |
246 | ---
247 |
248 | ---
249 |
250 | References:
251 |
252 | [1]. 【Scala for the impatient chapter 6】
--------------------------------------------------------------------------------
/doc/4 Scala中的特质Trait.markdown:
--------------------------------------------------------------------------------
1 | # 4 Scala中的特质Trait
2 |
3 | 标签(空格分隔): 级别L1:初级类库设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | 本章要点如下:
12 |
13 | - 类可以实现任意数量的特质Trait
14 | - 特质可以要求实现他们的类具备特定的字段,方法或父类
15 | - 特质可以提供方法和字段的具体实现
16 | - with多个特质的时候,注意其顺序--其方法被先执行的特质排在更后面
17 |
18 | ## 4.1 为什么没有多重继承,Scala通过特质如何解决多重继承问题
19 |
20 | 多重继承会引发菱形继承问题,既,两个父类继承自同一个基类,则子类中会包含两份父类中的内容,不合并重复内容会引起一些歧义,而合并重复内容又会导致类成员的内存布局不能简单的从父类复制。
21 |
22 | Scala中,在构造特质时,对其构造顺序进行限制来消除多重继承的问题。具体见下面篇幅的介绍。
23 |
24 | ## 4.2 当作接口使用的特质
25 |
26 | 当作接口使用的Trait,意味着这个Trait里面全是抽象方法:
27 | ```
28 | trait Logger {
29 | def log(msg: String) // 不需要abstract修饰,未被实现的方法就是抽象的
30 | }
31 | ```
32 | 通过子类来实现抽象方法:
33 | ```
34 | class ConsoleLogger extends Logger { // 用extends,而不是implements
35 | def log(msg: String) { // 子类实现一个抽象方法,不需要override修饰。重写父类的具体方法才需要override。
36 | println(msg)
37 | }
38 | }
39 | ```
40 |
41 | 如果需要混入多个特质时,用with来添加额外的特质:
42 | ```
43 | class ConsoleLogger2 extends Logger with Cloneable with Serializable {
44 | def log(msg: String) { println(msg) }
45 | }
46 | ```
47 | 在这里我们使用了Java类库的Cloneable和Serializable接口,仅仅是为了展示语法的需要。
48 | 所有Java的接口都可以在Scala中作为特质使用。
49 |
50 | >说明:在第一个特质前使用extends,而在所有其他特质前使用with看上去很奇怪。其实Scala并不是这样来解读的。在Scala中,Logger with Cloneable with Serializable 首先是一个整体,然后再由类来扩展。
51 |
52 | ---
53 |
54 | ---
55 |
56 | ## 4.3 带有具体实现的Trait
57 |
58 | 在Scala中,特质中的方法并不需要一定是抽象的。也可以是一个带有具体实现方法的特质:
59 | ```
60 | trait ConsoleLogger {
61 | def log(msg: String) { println(msg) } // 一个具体实现的方法
62 | }
63 | ```
64 | 使用上面的特质把日志信息打印出来。
65 | ```
66 | class Account() {
67 | var balance: Double = 0
68 | }
69 |
70 | class SavingsAccount extends Account with ConsoleLogger {
71 | def withdraw(amount: Double) {
72 | if (amount > balance) log("Insufficient funds")
73 | else balance -= balance
74 | }
75 | //...
76 | }
77 | ```
78 | 在Scala中,我们说ConsoleLogger提供的功能被混入到了SavingsAccount类中。
79 |
80 | ---
81 |
82 | ---
83 |
84 | ## 4.4 带有特质的对象【实例化一个对象的时候混入特质(一个或多个)】
85 |
86 | ```
87 | // 定义一个账户
88 | class Account() {
89 | var balance: Double = 0
90 | }
91 |
92 | // 一个空实现的Logger
93 | trait Logged {
94 | def log(msg: String) {}
95 | }
96 |
97 | trait ConsoleLogger extends Logged {
98 | override def log(msg: String) { println(s"Sub trait ConsoleLogger: $msg") }
99 | }
100 |
101 | trait ConsoleLogger2 {
102 | def log(msg: String) { println(s"ConsoleLogger2: $msg") }
103 | }
104 |
105 | trait FileLogger {
106 | def logFile(msg: String) { println(msg) }
107 | }
108 |
109 | class SavingsAccount extends Account with Logged {
110 | def withdraw(amount: Double) {
111 | if (amount > balance) log("Insufficient funds")
112 | else log("OK")
113 | }
114 | //...
115 | }
116 |
117 | object Run extends App {
118 | /**
119 | * 因为类SavingsAccount混入了Logged,并使用了特质提供的log方法。
120 | * 在实例化SavingsAccount的对象时,可以混入Logged特质的子类型。
121 | * 那么在调用这个对象所具有的特质方法log时,将会执行子类型中的log的方法。
122 | */
123 | val acct = new SavingsAccount with ConsoleLogger
124 | acct.withdraw(10)
125 |
126 | /**
127 | * 在实例化SavingsAccount的对象时,混入了一个非Logged特质的子类型,
128 | * 如果这个非子类型跟Logged特质存在同名的log方法时,编译出错:
129 | * inherits conflicting members
130 | * 反之,如果这个子类型跟Logged没有冲突的字段和方法,则可以混入
131 | */
132 | val acct2 = new SavingsAccount with ConsoleLogger2 // 这里会编译出错:inherits conflicting members
133 | acct2.withdraw(10)
134 |
135 | /**
136 | * 另一个对象也可以混入不同的特质
137 | */
138 | val acct3 = new SavingsAccount with FileLogger
139 | }
140 | ```
141 | >对上面例子的说明:
142 | 在实例化SavingsAccount对象时,可以混入该对象所具有的特质Logged的子类型ConsoleLogger。
143 | 那么在调用这个对象所具有的特质方法log时,将会执行子类型ConsoleLogger的log方法。
144 |
145 | ---
146 |
147 | ---
148 |
149 | ## 4.5 叠加在一起的特质
150 |
151 | 可以为类或对象添加多个相互调用的特质,调用将会从最后一个特质开始。
152 | 这个功能对需要分阶段加工处理某个值的场景很有用。
153 | ```
154 | class Account() {
155 | var balance: Double = 0
156 | }
157 |
158 | trait Logged {
159 | def log(msg: String) {}
160 | }
161 |
162 | class SavingsAccount extends Account with Logged {
163 | def withdraw(amount: Double) {
164 | if (amount > balance) log("Insufficient funds")
165 | else log("OK")
166 | }
167 | //...
168 | }
169 |
170 | // 单纯打印日志信息
171 | trait ConsoleLogger extends Logged {
172 | override def log(msg: String) { println(msg) }
173 | }
174 |
175 | // 为日志信息添加时间戳
176 | trait TimestampLogger extends Logged {
177 | override def log(msg: String) {
178 | super.log(new java.util.Date() + " " + msg)
179 | }
180 | }
181 |
182 | // 截断冗长的日志信息
183 | trait ShortLogger extends Logged {
184 | val maxLength = 15
185 | override def log(msg: String) {
186 | super.log(
187 | if (msg.length <= maxLength) msg
188 | else msg.substring(0, maxLength - 3) + "...")
189 | }
190 | }
191 |
192 | object Run extends App {
193 |
194 | /**
195 | * acct1对象在调用log方法时,首先会调用ShortLogger中的log方法,
196 | * ShortLogger中的log方法对日志进行处理后,把处理后的结果信息
197 | * 通过super.log方式传递给混入顺序的上一层TimestampLogger进行处理。
198 | * 同样,TimestampLogger的log方法处理完成后,再通过super.log方式
199 | * 传递给混入顺序的上一层ConsoleLogger处理,最后打印出日志信息。
200 | * 那为什么没有使用Logger中的log方法而是到ConsoleLogger中的log方法就停止了呢?
201 | * 因为ConsoleLogger中的log方法并没有继续使用super去调用上一个层级Logger中的方法。
202 | * 通过这样的一种机制能很好的避免多重继承的问题。
203 | */
204 | val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
205 | acct1.log("test") // Sun Dec 01 16:01:17 CST 2013 test
206 |
207 | // 线性化顺序也就是super被解析的顺序
208 | // SavingAccount -> TimestampLogger -> ShortLogger -> ConsoleLogger -> Logger -> Account
209 | val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger
210 | acct2.log("test") // Sun Dec 01 1...
211 | }
212 | ```
213 |
214 | ---
215 |
216 | ---
217 |
218 | ## 4.6 特质构造顺序
219 |
220 | 和类一样,特质也可以有构造器,由字段的初始化和其他特质体中的语句构成。
221 | ```
222 | trait FileLogger extends Logger {
223 | val out = new PrintWriter("app.log") // 特质构造器的一部分
224 | out.println(s"${new Date()}") // 特质构造器的一部分
225 |
226 | def log(msg: String) {
227 | out.println(msg)
228 | out.flush()
229 | }
230 | }
231 | ```
232 | 在实例化混入该特质的对象时,特质构造器部分的语句都会被执行。
233 | 以下面的例子说明:
234 | ```
235 | class SavingsAccount extends Account with ConsoleLogger with ShortLogger with TimestampLogger{}
236 | ```
237 | 构造器以如下的顺序执行:
238 |
239 | - 调用超类Account的构造器;
240 | - 特质构造器在超类Account构造器之后、类SavingsAccount构造器之前执行;
241 | - 特质由左到右被构造;每个特质当中,父特质先被构造;
242 | 首先,Logger(第一个Trait的父Trait),
243 | 然后,ConsoleLogger(第一个Trait),
244 | 第三,ShortLogger(第二个Trait,注意到它的父Trait已经被构造)
245 | 第四,TimestampLogger(第三个Trait,注意到它的父Trait已经被构造)
246 | - 如果多个特质共有一个父特质,父特质不会被重复构造
247 | - 所有特质被构造完毕,子类SavingsAccount被构造。
248 |
249 | ```
250 | class Account() {
251 | var balance: Double = 0
252 | }
253 |
254 | trait Logged {
255 | def log(msg: String) {} // 这是一个空实现的方法,而并非抽象方法
256 | }
257 |
258 | class SavingsAccount extends Account with Logged {
259 | def withdraw(amount: Double) {
260 | if (amount > balance) log("Insufficient funds")
261 | else log("OK")
262 | }
263 | //...
264 | }
265 |
266 | // 单纯打印日志信息
267 | trait ConsoleLogger extends Logged {
268 | override def log(msg: String) { println(msg) }
269 | }
270 |
271 | // 为日志信息添加时间戳
272 | trait TimestampLogger extends Logged {
273 | override def log(msg: String) {
274 | super.log(new java.util.Date() + " " + msg)
275 | }
276 | }
277 |
278 | // 截断冗长的日志信息
279 | trait ShortLogger extends Logged {
280 | val maxLength = 15
281 | override def log(msg: String) {
282 | super.log(
283 | if (msg.length <= maxLength) msg
284 | else msg.substring(0, maxLength - 3) + "...")
285 | }
286 | }
287 |
288 | class SavingsAccount extends Account with ConsoleLogger with ShortLogger with TimestampLogger{}
289 |
290 | Account(超类)
291 | Logger(第一个Trait的父Trait)
292 | ConsoleLogger(第一个Trait)
293 | ShortLogger(第二个Trait,注意到它的父Trait已经被构造)
294 | TimestampLogger(第三个Trait,注意到它的父Trait已经被构造)
295 | SavingAccount(子类)
296 |
297 | // 构造器顺序
298 | Account -> Logger -> ConsoleLogger -> ShortLogger -> TimestampLogger -> SavingAccount
299 | ```
300 | ---
301 |
302 | 线性化:
303 | 如果C extends C1 with C2 … with Cn,则lin( C ) = C ⪼ lin(Cn) ⪼ … ⪼ lin(C2) ⪼ lin(C1),在这里⪼的意思是“串接并去掉重复项,右侧胜出”。
304 |
305 | 例如:
306 | ```
307 | lin(SavingAccount)
308 | = SavingAccount ⪼ lin(TimestampLogger) ⪼ lim(ShortLogger) ⪼ lim(ConsoleLogger) ⪼ lim(Account)
309 | = SavingAccount ⪼ (TimestampLogger ⪼ Logger) ⪼ (ShortLogger ⪼ Logger) ⪼ (ConsoleLogger ⪼ Logger) ⪼ lim(Account)
310 | = SavingAccount ⪼ TimestampLogger ⪼ ShortLogger ⪼ ConsoleLogger ⪼ Logger ⪼ Account
311 |
312 | // 线性化顺序
313 | SavingAccount -> TimestampLogger -> ShortLogger -> ConsoleLogger -> Logger -> Account
314 | ```
315 | 线性化给出了在特质中super被解析的顺序。
316 |
317 | 例如:
318 |
319 | - 在TimestampLogger中调用super会执行ShortLogger中的方法,
320 | - 而在ShortLogger中调用会执行ConsoleLogger中的方法。
321 |
322 | > **构造器的顺序是类的线性化的反向**。
323 |
324 | ---
325 |
326 | ---
327 |
328 | ## 4.7 在特质中重写抽象方法(在特质中实现抽象方法)
329 |
330 | 我们实现一个提供抽象方法的特质:
331 | ```
332 | trait Logger {
333 | def log(msg: String) // 抽象方法
334 | }
335 |
336 | ```
337 | 再通过一个具体的时间戳特质来扩展上面的Logger特质,即实现其抽象方法log。
338 |
339 | ```
340 | // 此种实现编译出错,因为super.log调用了一个抽象方法,编译器将super.log调用标记为错误。
341 | trait TimestampLogger extends Logger {
342 | override def log(msg: String) { // 重写抽象方法
343 | super.log(new java.util.Date() + " " + msg) // super.log定义了吗?
344 | }
345 | }
346 | ```
347 | 根据正常的继承规则,这个调用肯定是错误的---Logger.log方法没用实现。但实际上,我们没法知道哪个log方法最终被调用---这取决于特质被混入的顺序。
348 |
349 | 所以,这种情况TimestampLogger依旧被当作是抽象的---它需要混入一个具体的log方法。
350 | 因此,需要用**abstract override**修饰TimestampLogger中的log方法:
351 | ```
352 | trait TimestampLogger extends Logger {
353 | abstract override def log(msg: String) { // 重写抽象方法
354 | super.log(new java.util.Date() + " " + msg)
355 | }
356 | }
357 | ```
358 | 非抽象类和特质扩展TimestampLogger时,要求需要实现log方法。
359 | ```
360 | class Account extends TimestampLogger // 编译错误,需要实现log方法
361 | ```
362 |
363 | ---
364 |
365 | ---
366 |
367 | ## 4.8 当做富接口使用的特质
368 | 特质可以包含大量工具方法,而这些工具方法可以依赖一些抽象方法来实现。
369 | >例如:scala中的Iterator特质就利用抽象的next和hasNext方法定义了很多方法。
370 |
371 | 丰富的Logger特质,提供大量的工具方法:
372 | ```
373 | class Account() {
374 | var balance: Double = 0
375 | }
376 |
377 | // Scala中经常使用抽象方法和具体方法相结合的方式
378 | trait Logger {
379 | def log(msg: String) // 定义一个抽象方法
380 | // 其他的工具方法都依赖于上面的抽象方法log
381 | def trace(msg: String) = log(s"[TRACE]: $msg") // 把抽象方法和具体方法结合在一起的例子
382 | def debug(msg: String) = log(s"[DEBUG]: $msg")
383 | def info(msg: String) = log(s"[INFO]: $msg")
384 | def warn(msg: String) = log(s"[WARN]: $msg")
385 | def error(msg: String) = log(s"[ERROR]: $msg")
386 | }
387 |
388 | class SavingsAccount extends Account with Logger {
389 | // 任意使用Logger中的其他工具方法,比如error方法
390 | def withdraw(amount: Double) {
391 | if (amount > balance) error("Insufficient funds")
392 | else log("OK")
393 | }
394 | // 子类在使用这个特质时,需要实现抽象的的log方法。
395 | override def log(msg: String) = println(msg)
396 | }
397 | ```
398 |
399 | ---
400 |
401 | ---
402 |
403 | ## 4.9 特质中的具体字段
404 | 特质中的字段可以是具体的,也可以是抽象的。如果给出了初始值,字段就是具体的。
405 | ```
406 | trait ShortLogger extends Logged {
407 | val maxLength = 15 // 一个具体的字段(给出了初始值)
408 | }
409 |
410 | ```
411 |
412 | 混入了该特质(ShortLogger)的类会自动获得特质的具体字段(maxLength)。
413 | 通常,对于特质中的每一个具体字段,使用该特质的类都会获得一个字段与之对应。
414 | 但这些字段不是被继承的,而是被简单加入到了子类当中。这个很细微的区别非常重要,通过实际例子来展示说明:
415 | ```
416 | class Account {
417 | var balance = 0.0 // 父类中的普通字段
418 | }
419 |
420 | class SavingsAccount extends Account with ShortLogger {
421 |
422 | var interest = 0.0 // 子类中的普通字段
423 |
424 | def withdraw(amount: Double) {
425 | if(amount > balance) log("Insufficient funds")
426 | else ...
427 | }
428 | }
429 |
430 | ```
431 | SavingsAccount类按照正常的方式继承了这个字段。SavingsAccount对象由所有超类的字段以及任何子类中定义的字段构成。则SavingsAccount对象的字段构成如下图:
432 |
433 | |---------|
434 | balance <-- 从父类继承下来的字段
435 | |---------|
436 | interest <-- SavingsAccount类中的普通字段
437 | maxLength <-- 来自特质中的字段被放置在子类中
438 | |---------|
439 |
440 | 在JVM中,一个类只能扩展一个父类,因此来自特质的字段不能以相同的方式继承。由于这个限制,maxLength被直接加到了SavingsAccount类中,跟interest字段并排在一起。
441 |
442 | >从特质中通过混入的方式获得的具体字段都自动成为该类自己的字段,这种字段等同于在类中自己定义的普通字段。
443 | 如果特质ShortLogger中的字段和父类Account中的字段有重复的话,编译出错,字段冲突。
444 |
445 | ---
446 |
447 | ---
448 |
449 | ## 4.10 特质中的抽象字段
450 | 特质中没有被初始化的字段就是抽象字段,抽象字段必须在子类中被具体化(实现)。
451 | 带有抽象字段的特质的例子:
452 | ```
453 | trait ShortLogger extends Logged {
454 |
455 | var maxLength: Int // 一个抽象的字段(没有被初始化)
456 |
457 | // 方法的实现中使用了上面的抽象字段
458 | override def log(msg: String) {
459 | super.log(
460 | if(msg.length <= maxLength) msg
461 | else msg.take(maxLength - 3) + "..."
462 | )
463 | }
464 | }
465 |
466 | // 在具体类中使用ShortLogger特质时,必须要实现其抽象字段maxLength
467 | class SavingsAccount extends Account with ShortLogger {
468 | var maxLength = 20 // 具体化特质ShortLogger中的抽象字段maxLength
469 | }
470 |
471 | ```
472 |
473 | ---
474 |
475 | ---
476 |
477 | ## 4.11 初始化特质中的字段
478 |
479 | 每个特质只有一个无参数的构造器。
480 | >特质与类唯一的差别:
481 | - 特质没有带参数的构造器,而是只有一个无参构造器
482 |
483 | 由于特质Trait只有一个无参构造器,那么在需要向特质指定参数的情况下就无法实现:
484 | 比如,以一个文件日志生成器Trait为例来说明:
485 | ```
486 | // 向特质FileLogger传入一个指定的参数,用于指定日志文件
487 | val acct = new SavingsAccount with FileLogger("myapp.log") // 错误:特质不能使用构造器参数
488 | ```
489 |
490 | 对于这个局限其解决方法是,可以考虑使用抽象字段来存放文件名:
491 | ```
492 | trait FileLogger extends Logger {
493 | val filename: String // 用抽象字段来存放文件名,抽象字段可以用val/var修饰
494 | val out = new PrintStream(filename)
495 | def log(msg: String) { out.println(msg); out.flush() }
496 | }
497 | ```
498 | 然后再直接使用特质FileLogger:
499 | ```
500 | // 错误的使用方式,混入了FileLogger的SavingsAccount类实例
501 | val acct = new SavingsAccount with FileLogger {
502 | val filename = "myapp.log"
503 | }
504 | ```
505 | 但是这样却是行不通的。问题来自于构造顺序。FileLogger的构造器会先于子类构造器执行,这里的子类是混入了FileLogger的SavingsAccount类实例。在构造FileLogger时,就会抛出一个空指针异常,子类的构造器根本就不会执行。
506 |
507 | 这个问题的解决方法之一是使用**提前定义**这个语法:
508 | ```
509 | // 一个扩展了SavingsAccount且混入了FileLogger的匿名类实例
510 | val acct = new { // new之后的提前定义块
511 | val filename = "myapp.log"
512 | } with SavingsAccount with FileLogger
513 | ```
514 | 这段“恶心”代码解决了直接使用时的构造顺序的问题。提前定义发生在常规的构造序列之前。
515 | 在FileLogger被构造时,filename已经被初始化了。
516 |
517 | 如果你需要在类中做同样的事情,代码如下:
518 | ```
519 | class SavingsAccount extends { // extends之后是提前定义块
520 | val filename = "myapp.log"
521 | } with Account with FileLogger {
522 | ... // SavingsAccount的实现
523 | }
524 | ```
525 |
526 | 另外一个方法是使用**懒值**,因为懒值在真正的初次使用时才被初始化:
527 | ```
528 | trait FileLogger extends Logger {
529 | val filename: String
530 | lazy val out = new PrintStream(filename)
531 | def log(msg: String) { out.println(msg) } // 真正使用log方法时,out才被初始化,而这个时候filename已经早就被初始化完了。
532 | }
533 | ```
534 | 如此一来,out字段不会再抛出空指针异常。在使用out字段时,filename也已经初始化了。
535 |
536 | **但是使用懒值不高效,因为懒值在每次使用前都会检查是否已经初始化。**
537 |
538 | ---
539 |
540 | ---
541 |
542 | ## 4.12 扩展一个类的特质(Trait extends Class)
543 |
544 | 通常特质可以扩展另一特质,由特质组成的继承层级也比较常见。
545 | 但是不太常见的一种用法也是存在的,那就是通过一个特质去扩展一个类。这个类会自动成为所有混入该特质的类的超类。例如:
546 |
547 | ```
548 | trait LoggedException extends Exception with Logged {
549 | // 定义一个log方法来记录异常信息
550 | def log() {
551 | log(getMessage()) // 调用从Exception超类继承下来的getMessage()方法。
552 | }
553 | }
554 | ```
555 |
556 | 创建一个混入该特质LoggedException的类UnhappyException:
557 | ```
558 | class UnhappyException extends LoggedException {
559 | override def getMessage = "arggh!"
560 | }
561 | ```
562 | 特质的超类Exception自动成为了混入了LoggedException特质的UnhappyException的超类。
563 |
564 | Scala并不允许多继承。
565 | 那么这样一来,如果UnhappyException原先已经扩展了一个类了该如何处理?
566 | 只要已经扩展的类是特质超类的一个子类就可以。
567 | ```
568 | // UnhappyException扩展的类IOException是特质LoggedException的超类Exception的一个子类
569 | class UnhappyException extends IOException with LoggedException // OK
570 | ```
571 |
572 | 如果类扩展自一个不相关的类,那么就不可能混入这个特质了。
573 |
574 | ```
575 | // UnhappyFrame扩展的类JFrame不是特质LoggedException的超类Exception的子类,错误!
576 | class UnhappyFrame extends JFrame with LoggedException // Error!!!
577 | ```
578 | 我们无发同时将JFrame和Exception作为父类。
579 |
580 | ---
581 |
582 | ---
583 |
584 | ## 4.13 特质中的自身类型(self type)【级别L2: 资深类库设计者】
585 |
586 | 当特质扩展一个类时,编译器能够确保的一件事就是所有混入该特质的类都认这个特质扩展类为超类。
587 | 通过例子再次解释上面的含义:
588 | ```
589 | // 定义3个类
590 | class Parent
591 | class Son extends Parent
592 | class D
593 |
594 | // 一个特质扩展一个类
595 | trait Logger extends Parent
596 |
597 | // 下面来使用这个Logger特质,分两种情况
598 | // 第一种情况:一个单纯的类
599 | class A extends Logger // 这种方式,显然Parent类成为了类A的父类
600 |
601 | // 第二种情况:还扩展了别的类的类
602 | class B extends Son with Logger // 类B扩展了别的类Son,但是这里有个限制就是Son必须是Parent的子类。我们这里满足
603 |
604 | // 如果是另外的情况:
605 | class C extends D with Logger // 错误,扩展的别的类D并不是Parent类的子类,禁止!
606 |
607 | // 综上所述:在使用一个扩展了类的特质时,或受到这个特质扩展的类的限制。
608 | // 特质除了使用扩展一个类的方式之外,就是在特质中使用自身类型self type。
609 |
610 | ```
611 |
612 | **自身类型**:
613 | 如果特质以 this: 类型type =>开始定义,那么这个特质就只能被混入type指定的类型的子类。
614 | ```
615 | trait LoggedException extends Logged {
616 | this: Exception =>
617 | def log() { log(getMessage()) }
618 | }
619 | ```
620 | 这里的特质LoggedException并不扩展Exception类,而是自身类型为Exception类型,意味着该特质只能被混入Exception的子类。这样指定了自身类型之后,可以调用自身类型的任何方法(这里调用了Exception类的getMessage方法)。因为我们知道this必定是一个Exception。
621 |
622 | 如果你把这个特质混入一个不符合自身类型要求的类,就会报错:
623 | ```
624 | val f = new JFrame with LoggedException // 错误:LoggedException的自身类型为Exception,而JFrame并不是Exception的子类。
625 |
626 | ```
627 |
628 | > 总结:带有自身类型的特质和带有超类型的特质很相似。两种情况都能确保混入该特质的类能够使用某个特定类型的特性。
629 | 在某些情况下,带有自身类型的特质比带有超类型的特质更灵活。
630 | 自身类型可以解决特质间的循环依赖问题(两个彼此需要的特质会产生循环依赖:TODO)。
631 |
632 | 自身类型还可以处理结构类型(structural type)——这种类型只给出了类必须拥有的方法,而不是类的名称。
633 | ```
634 | trait LoggedException extends Logged {
635 | this: {def getMessage(): String} =>
636 | def log() { log(getMessage()) }
637 | }
638 | ```
639 |
640 | 这个特质可以被混入任何拥有getmessage方法的类。
641 |
642 | ---
643 |
644 | ---
645 |
646 | ## 4.14 背后发生了什么
647 | Scala将特质翻译为JVM的类和接口。
648 |
649 | - 只有抽象方法的特质被简单的变成一个Java接口。
650 | ```
651 | trait Logger { // Scala 特质
652 | def log(msg: String)
653 | }
654 | ```
655 | 直接被翻译成:
656 | ```
657 | public interface Logger { // 生成Java接口
658 | void log(String msg);
659 | }
660 |
661 | ```
662 |
663 | - 带有具体方法的特质,Scala会创建出一个伴生类,伴生类里的静态方法存放特质的具体方法
664 | ```
665 | trait ConsoleLogger extends Logger {
666 | def log(msg: String) {
667 | println(msg)
668 | }
669 | }
670 |
671 | ```
672 | 被翻译成:
673 | ```
674 | public interface ConsoleLogger extends Logger{ // 生成Java接口
675 | void log (String msg);
676 | }
677 |
678 | public class ConsoleLogger$class { // 生成Java伴生类
679 | public static void log(ConsoleLogger self, String msg) {
680 | println(msg);
681 | }
682 | }
683 |
684 | ```
685 |
686 |
687 | ---
688 |
689 | ---
690 |
691 | References:
692 |
693 | [1]. 【Scala for the impatient chapter 10】
694 | [2].http://www.slideshare.net/jboner/pragmatic-real-world-scala-45-min-presentation
--------------------------------------------------------------------------------
/doc/5 Scala中的类型参数.markdown:
--------------------------------------------------------------------------------
1 | # 5 Scala中的类型参数
2 |
3 | 标签(空格分隔): 级别L2:资深类库设计者 深入学习Scala
4 |
5 | [TOC]
6 |
7 | ------------------------------------------------------------
8 |
9 | ------------------------------------------------------------
10 |
11 | Scala中的类型参数其表现形式,主要通过`泛型类`,`泛型Trait`和`泛型函数/方法`。
12 |
13 |
14 | ## 5.1 类型参数,参数类型,参数化的类型区别
15 |
16 | 以Array[T]为例:
17 |
18 | T就是`类型参数`,或者称为`参数类型`。`类型参数(type parameter)`和`参数类型(parameter type)`是一个意思,都是指T。
19 |
20 | Array[T]这个整体就称为`参数化的类型(parameterized type)`。
21 |
22 | ------------------------------------------------------------
23 |
24 | ------------------------------------------------------------
25 |
26 |
27 | ## 5.2 泛型类(带有一个或多个类型参数的类)
28 |
29 | `类class`,`特质trait`以及`函数/方法`可以带类型参数。在Scala中,用方括号来定义类型参数,例如:
30 | ```
31 | class Pair[T, S](val first: T, val second: S)
32 | ```
33 | 以上将定义一个带有两个类型参数T和S的类。这个类被称为参数化的类型Pair[T, S]。
34 |
35 | 在类的定义中,你可以用类型参数来定义`变量/常量`的类型,`方法参数`的类型以及`返回值`的类型。
36 |
37 | 带有一个或多个类型参数的类是泛型的。如果你把类型参数T,S替换成实际的类型,将得到一个具体的类,比如Pair[Int, String]。
38 |
39 | Scala具有类型推断能力,下面的代码是等价的:
40 | ```
41 | val p = new Pair[Int, String](10, "String")
42 |
43 | // 等价于
44 | val p = new Pair(10, "String") // Scala从构造方法的参数自动推断出这是一个Pair[Int, String]的类型
45 | ```
46 |
47 | ------------------------------------------------------------
48 |
49 | ------------------------------------------------------------
50 |
51 | ## 5.3 泛型函数(带有一个或多个类型参数的函数)
52 |
53 | 函数和方法也可以带类型参数。和泛型类一样,你需要把类型参数放在函数名之后。以下是一个简单的示例:
54 | ```
55 | // 返回数组的中间元素
56 | def getMiddle[T](a: Array[T]) = a(a.length / 2)
57 | ```
58 | 接着调用getMiddle方法:
59 | ```
60 | getMiddle(Array("hello", "world", "!")) // Scala自动推断出getMiddle[String]
61 |
62 | getMiddle[String](Array("hello", "world", "!")) // 手动指定类型参数
63 | ```
64 |
65 | ------------------------------------------------------------
66 |
67 | >小结:类型参数(参数类型)出现的位置在类名,特质名,方法名之后:
68 | ```
69 | class Pair[T] // 出现在类名之后:方括号中的T称为类型参数,而Pair[T]则被称为参数化的类型
70 | trait Pair[T] // 出现在特质名之后:方括号中的T称为类型参数,而Pair[T]则被称为参数化的类型
71 | def getMiddle[T] // 出现在方法名之后:方括号中的T称为类型参数,此种情况无参数化的类型
72 | ```
73 |
74 | ------------------------------------------------------------
75 |
76 | ------------------------------------------------------------
77 |
78 |
79 | ## 5.4 对类型参数的限定(对类型参数范围的指定)
80 |
81 | 对类型参数的限定就是对类型参数范围的指定,使这个类型参数有一个更明确的限定范围,从而在这个类型参数未被具体化的时候,也能够在一定程度上知道该类型参数所代表的类型的某些行为。主要通过下面三种方式来进行限定:
82 |
83 | `上界(upper bounds) A <: B`
84 | `下界 (lower bounds) B >: A`
85 | `上下文界定(context bounds) A : B`
86 |
87 | ### 5.4.1 上界(upper bounds)的使用场景
88 |
89 | 为类型参数指定上界进行限定后,就表明这个上界是这个类型参数代表的类型的父类,从而就可以确定这个类型参数代表的类型肯定有上界指定的类型中的行为。
90 | 例如:T <: Comparable[T] 说明,类型T肯定包含有Comparable中的方法。因为T是Comparable[T]的子类。
91 |
92 | 直接通过例子来说明为什么需要使用`上界upper bounds`。
93 | 考虑这样一个Pair类型,它要求它的两个参数类型相同:
94 | ```
95 | class Pair[T](val first: T, val second: T) {
96 | def smaller =
97 | if (first < second) first else second // 编译错误
98 | }
99 | ```
100 | 类型为T的值并不一定有`<`操作符或被叫作为`<`的方法。
101 | 为了解决T类型的值可以进行大小比较,我们可以为T类型添加一个`上界upper bounds` T<:Comparable[T]。也就是T必须为Comparable[T]的子类型。
102 | 上界Comparable[T]这个父类中提供了两个值进行大小比较的方法,作为这个上界的子类T,必然也就拥有了这个上界父类中的方法,所以可以进行比较。示例代码如下:
103 | ```
104 | class Pair[T <: Comparable[T]](val first: T, val second: T) {
105 | def smaller =
106 | if(first.compareTo(second) < 0) first else second // 编译成功
107 | }
108 | ```
109 | 这样一来,我们在实例化Pair[T]的时候,一定要保证T的实际类型必须是Comparable[T]的子类型。
110 |
111 | 例如:
112 | String是Comparable[String]的子类型,所以可以实例化Pair[String]。
113 | File **不**是Comparable[File]的子类型,所以**不**可以实例化Pair[File]。
114 | 如果你尝试实例化new Pair(4, 2),则会编译出错,因为Scala通过类型推断,自动推断出T=Int,界定T <: Comparable[T]无法满足。因为Int <: Comparable[Int]不满足,Int不是Comparable[Int]的子类。
115 |
116 | ------------------------------------------------------------
117 |
118 | ### 5.4.2 下界(lower bounds)的使用场景
119 |
120 | 下界的限定就是类型参数的类型必须等于下界或者是下界的父类。
121 | 例如:
122 | T >: List[Int],要求T必须等于下界List[Int]或者是List[Int]的父类。当T为Traverable[Int]时,那么使用T类型的地方,任何Traverable的子类都可以使用,比如Set[Int],尽管不是List[Int]的父类,但是也可以使用。
123 |
124 | 举例来说,假定我们要定义一个方法,用另一个值替换对偶的第一个参数。我们的对偶是不可变得,因此我们需要返回一个新的对偶。以下是我们的首次尝试:
125 | ```
126 | // 父类
127 | class Person(name: String){
128 | override def toString = s"hello, $name"
129 | }
130 | // 子类之一
131 | class Student(name: String) extends Person(name) {
132 | override def toString = s"hello, $name"
133 | }
134 | // 子类之二
135 | class Teacher(name: String) extends Person(name) {
136 | override def toString = s"hello, $name"
137 | }
138 | // 对偶类
139 | class Pair[T](val first: T, val second: T) {
140 | def replaceFirst(newFirst: T) = new Pair(newFirst, second)
141 | }
142 | // test类
143 | object Pair extends App {
144 |
145 | // 确定T为Student类型
146 | val s: Pair[Student] = new Pair(new Student("张三"), new Student("李四"))
147 |
148 | // 那么replaceFirst方法的参数类型也必须为Student类型
149 | val s1: Pair[Student] = s.replaceFirst(new Student("王五"))
150 |
151 | // replaceFirst方法的参数类型为Student的父类,编译错误,类型不匹配
152 | val p: Pair[Person] = s.replaceFirst(new Person("王五"))
153 |
154 | // replaceFirst方法的参数类型与Student类有共同父类,编译错误,类型不匹配
155 | val p1: Pair[Person] = s.replaceFirst(new Teacher("王麻子"))
156 | }
157 | ```
158 |
159 | 上面的Person类既然是Student类的父类,我们当然希望replaceFirst方法也可以应用于Student类的父类Person。改进如下:
160 |
161 | ```
162 | // 对偶类
163 | class Pair[T](val first: T, val second: T) {
164 | // 为了清晰起见,给返回的对偶也明确写上类型参数
165 | def replaceFirst[U >: T](newFirst: U) = new Pair[U](newFirst, second)
166 |
167 | /**
168 | // 你也可以对返回类型不明确指定类型参数,通过Scala自动类型推断
169 | def replaceFirst[U >: T](newFirst: U) = new Pair(newFirst, second)
170 | */
171 | }
172 | // test类
173 | object Pair extends App {
174 | val s: Pair[Student] = new Pair(new Student("张三"), new Student("李四")) // Student类型
175 |
176 | val s1: Pair[Student] = s.replaceFirst(new Student("王五")) // Student类型
177 |
178 | val p: Pair[Person] = s.replaceFirst(new Person("王五")) // 返回Student的父类型Person
179 |
180 | val p1: Pair[Person] = s.replaceFirst(new Teacher("王麻子")) // 返回Student的父类型Person
181 | }
182 | ```
183 |
184 | 通过这个改进,带来的额外的好处就是,Teacher类跟Student类没有任何关系,但是他们都有一个公共的父类Person。所以通过下界的限定,def replaceFirst[U >: T]不但可以接收Person类型,还可以接收Person的其他子类型参数。
185 |
186 | >注意: 如果不指定下界,那么这个replaceFirst方法返回的类型就是`newFirst: U`和`val second: T`的公共父类型。
187 | ```
188 | class Pair[T](val first: T, val second: T) {
189 | // 返回的类型为U和T的公共父类型
190 | def replaceFirst[U](newFirst: U) = new Pair(newFirst, second)
191 | }
192 | ```
193 |
194 | ------------------------------------------------------------
195 |
196 | ### 5.4.3 上下文界定(context bounds)
197 |
198 | 类型参数可以有一个形式为`T:M`的上下文界定,其中M是另一个`泛型类型`。它要求作用域中存在一个类型为`M[T]`的隐式值。
199 |
200 | ```
201 | // 上下文界定
202 | class Pair[T : Ordering]
203 | ```
204 | 上述定义要求必须存在一个类型为Ordeing[T]的隐式值。该隐式值会成为Pair类的一个字段,可以被用在该Pair类的带有隐式参数的方法中。这个带有隐式参数的方法会自动应用作用域内的类型为Ordeing[T]隐式值。以下是一个示例:
205 | ```
206 | class Pair[T : Ordering](val first: T, val second: T) {
207 | // 1. 编译后,Pair里面会有一个隐式的Ordering[T]的字段
208 | // 2. 定义一个带有隐式参数的方法,调用方法时,会自动获取Pair类的隐式字段Ordering[T]
209 | def smaller(implicit ord: Ordering[T]) =
210 | if (ord.compare(first, second) < 0 ) first else second
211 | }
212 | ```
213 |
214 | 如果我们new一个Pair(40, 2), 编译器将推断出我们需要一个Pair[Int]。由于Predef作用域中有一个类型为Ordering[Int]的隐式值,因此Int满足上下文界定。这个Ordering[Int]就成为该类的一个字段,被传入需要该值的方法(一个带有隐式参数的方法)当中。
215 | 如果你愿意,你也可以用Predef类的implicity方法获取该值:
216 | ```
217 | class Pair[T: Ordering](val first: T, val second: T) {
218 | def smaller =
219 | if(implicitly[Ordering[T]].compare(first, second) < 0) first else second
220 | }
221 | ```
222 |
223 | >说明:为什么要引人上下文限定?
224 |
225 | 上下文界定(Context Bound),是Scala为隐式参数引入的一种语法糖,使得隐式转换的编码更加简洁。
226 | 通过下面的例子说明:
227 | 首先引入一个泛型函数max,用于获取a和b的最大值。
228 | ```
229 | def max[T](a: T, b: T) = if (a > b) a else b
230 | ```
231 | 因为T是未知类型,只有在运行时才会确定真正的类型,因此调用a > b是不正确的,因为T不确定,也就不确定T是否有`>`这个方法实现。
232 |
233 | 通过引入类型隐式转换可以解决上面的问题,因为Ordering类型是可比较的,因此定义一个类型的隐式转换,将T转换为Ordering[T],所以只要执行上下文有这个隐式转换,就可以进行比较:
234 | ```
235 | // 实现一个带有隐式参数的max方法,在调用这个Max方法时,引入一个Ordering[T]的隐式值
236 | def max[T](a: T, b: T)(implicit t: Ordering[T]) =
237 | if (t.compare(a, b) < 0) b else a
238 | ```
239 |
240 | 上面的方法实现,对使用者来说,不够友好,最好把隐式参数的部分隐藏起来,于是通过Context Bound来省略隐式参数:
241 |
242 | ```
243 | // 1 推荐的做法,implicitly是在Predef.scala里定义的,它是一个特殊的方法,编译器会记录当前上下文里的隐式值,而这个方法则可以获得某种类型的隐式值。
244 | def max[T : Ordering](a: T, b: T) =
245 | if (implicitly(Ordering[T]).compare(a, b) < 0) b else a
246 |
247 | // 2 在内部定义函数并声明隐式参数
248 | def max[T : Ordering](a: T, b: T) = {
249 | def compare(implicit ev1: Ordering[T]) = ev1.compare(a, b)
250 | if(compare > 0) a else b
251 | }
252 | ```
253 |
254 | ------------------------------------------------------------
255 |
256 | ------------------------------------------------------------
257 | ## 5.5 ClassTag上下文限定(ClassTag context bounds)(最常用)
258 |
259 | ### 5.5.1 背景介绍(为什么要使用ClassTag)
260 |
261 | Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译的时候去掉,这个过程就称为类型擦除。泛型擦除是为了兼容jdk1.5之前的jvm,在这之前是不支持泛型的。
262 | 泛型在编写和编译的时候,不能确定具体的类型,但是虚拟机运行的时候必须要具体的类型,所以ClassTag会帮助我们存储这个运行时的类型,并通过反射把这个运行时的类型传递给虚拟机。
263 |
264 | ### 5.5.2 ClassTag
265 |
266 | scala在2.10里用用ClassTag替代了ClassManifest,原因是在路径依赖类型中,ClassManifest存在问题(ClassManifest已过期)。
267 | ClassTag[T]保存了被泛型擦除后的原始类型T,提供给运行时使用。
268 | ```
269 | scala> import scala.reflect.ClassTag
270 | import scala.reflect.ClassTag
271 |
272 | scala> def mkArray[T : ClassTag](elems: T*) = Array[T](elems: _*)
273 | mkArray: [T](elems: T*)(implicit evidence$1: scala.reflect.ClassTag[T])Array[T]
274 |
275 | scala> mkArray(1, 2)
276 | res0: Array[Int] = Array(1, 2)
277 |
278 | scala> mkArray("Jim", "Lucy")
279 | res1: Array[String] = Array(Jim, Lucy)
280 |
281 | ```
282 |
283 |
284 | ------------------------------------------------------------
285 |
286 | ------------------------------------------------------------
287 |
288 | ## 5.6 TypeTag上下文限定(TypeTag context bounds)
289 | scala在2.10里用TypeTag替代了Manifest,原因是在路径依赖类型中,Manifest存在问题(Manifest已过期)。实例代码如下:
290 |
291 | ```
292 | scala> class Foo{class Bar}
293 | defined class Foo
294 |
295 | scala> val f1 = new Foo;val b1 = new f1.Bar
296 | f1: Foo = Foo@48c4245d
297 | b1: f1.Bar = Foo$Bar@3df978b9
298 |
299 | scala> val f2 = new Foo;val b2 = new f2.Bar
300 | f2: Foo = Foo@5ac7aa18
301 | b2: f2.Bar = Foo$Bar@4cdd2c73
302 |
303 | // 使用Manifest的例子
304 | scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
305 | m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]
306 |
307 | scala> val ev1 = m(f1)(b1)
308 | ev1: Manifest[f1.Bar] = Foo@48c4245d.type#Foo$Bar
309 |
310 | scala> val ev2 = m(f2)(b2)
311 | ev2: Manifest[f2.Bar] = Foo@5ac7aa18.type#Foo$Bar
312 |
313 | scala> ev1 == ev2 // // they should be different, thus the result is wrong
314 | res3: Boolean = true
315 |
316 | // 使用TypeTag的例子
317 | scala> import scala.reflect.runtime.universe._
318 | import scala.reflect.runtime.universe._
319 |
320 | scala> def m2(f: Foo)(b: f.Bar)(implicit ev: TypeTag[f.Bar]) = ev
321 | m2: (f: Foo)(b: f.Bar)(implicit ev: reflect.runtime.universe.TypeTag[f.Bar])reflect.runtime.universe.TypeTag[f.Bar]
322 |
323 | scala> val ev3 = m2(f1)(b1)
324 | ev3: reflect.runtime.universe.TypeTag[f1.Bar] = TypeTag[f1.Bar]
325 |
326 | scala> val ev4 = m2(f2)(b2)
327 | ev4: reflect.runtime.universe.TypeTag[f2.Bar] = TypeTag[f2.Bar]
328 |
329 | scala> ev3 == ev4 // This is right!
330 | res4: Boolean = false
331 | ```
332 |
333 | TypeTag保存所有具体的类型
334 | ```
335 | import scala.reflect.runtime.universe._
336 |
337 | def paramInfo[T](x: T)(implicit tag: TypeTag[T]): Unit = {
338 | val targs = tag.tpe match { case TypeRef(_, _, args) => args }
339 | println(s"type of $x has type arguments $targs")
340 | }
341 |
342 | scala> paramInfo(42)
343 | type of 42 has type arguments List()
344 |
345 | scala> paramInfo(List(1, 2))
346 | type of List(1, 2) has type arguments List(Int)
347 | ```
348 |
349 | ------------------------------------------------------------
350 |
351 | ------------------------------------------------------------
352 |
353 | ## 5.7 多重限定
354 | Scala 多重界定分为以下几种:
355 | ```
356 | // 1. 只能有一个上界
357 | T <: A with B 类型参数T不能同时有多个上界或下界,不过可以有一个类型混入了多个Trait,T是A或B的子类
358 |
359 | // 2. 只能有一个下界
360 | T >: A with B A或B是T的子类,一般不用
361 |
362 | // 3. 可以同时有上界和下界
363 | T >: A <: B 必须先是下界,再跟着上界,顺序不能颠倒,A下是界,B是上界 ,A是B的子类
364 |
365 | // 4. 可以同时有多个上下文界定
366 | T: A : B 上下文界定,T必须同时满足存在A[T]和B[T]的隐试转换值
367 |
368 | // 5. 可以同时有多个视图界定,2.10后过期
369 | T <% A <% B 视图界定 T既可以转换成B也可以转换成A
370 | ```
371 |
372 | 类型不可以有多个上界或下界,如果想有多个上界或下界,这样语法是不正确的。
373 | spark中常用的就是`<:`
374 |
375 | ------------------------------------------------------------
376 |
377 | ------------------------------------------------------------
378 |
379 | ## 5.8 类型约束(对类型参数的约束)【级别L3: 专家类库设计者】
380 |
381 | 类型约束总共有三种关系可供使用:
382 | ```
383 | T =:= U
384 | T <:< U
385 | T <%< U 2.10之后已废弃
386 | ```
387 | 这些约束将会测试T是否等于U,是否为U的子类型,能否被视图(隐式)转换为U。`类型约束`对比`类型界定`的例子:
388 | ```
389 | // 类型界定
390 | class Pair[T <: Comparable[T]](val first : T, val second : T){
391 | def smaller = if (first.compareTo(second) < 0) first else second
392 | }
393 |
394 | // 类型约束,更严格
395 | class Pair[T](val first : T, val second : T)(implicit ev: T <:< Comparable[T] ) {
396 | def smaller = if (first.compareTo(second) < 0) first else second
397 | }
398 | ```
399 | 这个例子并没有看出类型约束相比于类型变量界定有和优势。其实类型约束比类型界定更严格。如果上面的例子有从T到Comparable[T]的隐式转换,那么类型界定可以,而类型约束却禁止。
400 |
401 | 下面再举出类型约束的两个用途。
402 | 第一,类型约束让你可以在泛型类中定义只能在特定条件下使用的方法:
403 | ```
404 | class Pair[T](val first : T, val second : T) {
405 | def smaller(implicit ev : T <:< Comparable[T] ) = if (first.compareTo(second) < 0) first else second
406 | }
407 | ```
408 | 构造Pair时,如果T的类型不是Comparable[T]的子类,则调用smaller方法时报错。
409 | 例如:构造Pair[File],File不是Comparable[T]的子类。那么调用smaller的时候才会报错。
410 |
411 | 一个更明显的例子是Option类的orNull方法,你可以构建任何类型的Option[T],但是Option里面的orNull方法有类型约束:
412 | ```
413 | @inline final def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse ev(null)
414 | ```
415 | 如果T的类型为值类型(AnyVal),那么调用orNull方法就会报错。因为Null是引用类型,它要求T必须是引用类型。
416 |
417 | 见Scala的继承图:
418 | 
419 |
420 | 例如:
421 | ```
422 | val m = Map("a"->1,"b"->2)
423 | val mOpt = m.get("c") // Option[Int]
424 | val mOrNull = mOpt.orNull // Compile error!
425 | ```
426 | 在和Java代码打交道时,orNull方法很有用,因为Java中通常用null表示缺少某值。不过这种做法并不适用于值类型,比如Int。因为orNull的实现带有约束Null <:< A,你仍然可以实例化Option[Int],只有不要调用orNull。
427 |
428 | 第二,类型约束的另一个用途是改进类型的推断:
429 | ```
430 | def firstLast[A, C <: Iterable[A]](it: C) = (it.head, it.last)
431 |
432 | firstLast(List(1,2,3)) // Compile error
433 | ```
434 | 这里编译错误,是因为推断出的类型参数[Nothing, List[Int]]不符合[A, C <: Iterable[A]]。为什么是Nothing?类型推断器单凭List(1,2,3)无法判断出A是什么,因为它是在同一个步骤中匹配到A和C的。要解决这个问题,首先匹配C,然后再匹配A:
435 | ```
436 | def firstLast[A, C](it: C)(implicit ev : C <:< Iterable[A]) = (it.head, it.last)
437 |
438 | firstLast(List(1,2,3)) // 正确
439 | ```
440 |
441 | ------------------------------------------------------------
442 |
443 | ------------------------------------------------------------
444 |
445 | ## 5.9 类型约束补充(Type Constraints)
446 |
447 | ### 5.9.1 基本概念
448 | 类型约束用于限定类型,现在有两种关系可供使用:
449 |
450 | A =:= B // 校验 A类型是否等于B类型
451 | A <:< B // 校验 A类型是否是B的子类型
452 |
453 | 2.10之前还有一个`A <%< B`类似于view bound,表示A可以当作B,即A隐式转换成B也满足。但在2.10里已经废弃这种写法。
454 |
455 | 这个看上去很像操作符的`=:=` 和 `<:<`,实际是一个类,它在Predef里定义:
456 |
457 | ```
458 | sealed abstract class =:=[From, To] extends (From => To) with Serializable
459 |
460 | sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
461 | ```
462 |
463 | 它定义了两个类型参数,所以可以使用中缀写法:`From <:< To`。
464 |
465 | ------------------------------------------------------------
466 |
467 | ### 5.9.2 如何使用
468 |
469 | 要使用这些类型约束,做法是提供一个隐式参数。比如:
470 | ```
471 | def firstLast[A, C](it: C)(implicit ev: C <:< Iterable[A]) = (it.head, it.last)
472 | ```
473 | 类型约束用在特定方法(specialized methods)的场景,所谓特定,是指方法只针对特定的类型参数才可以运行:
474 | ```
475 | def test[T](i:T)(implicit ev: T <:< java.io.Serializable) {
476 | print("OK")
477 | }
478 | ```
479 | ```
480 | scala> test("hi") // OK
481 | scala> test(2)
482 | :9: error: Cannot prove that Int <:< java.io.Serializable.
483 | ```
484 | 上面定义的test方法,在方法的第二个参数使用了一个隐式参数ev,它的类型是:`T<: import scala.reflect.runtime.universe._
510 | import scala.reflect.runtime.universe._
511 |
512 | scala> typeOf[List[_]] =:= typeOf[List[AnyRef]]
513 | res4: Boolean = false
514 |
515 | scala> typeOf[List[Int]] <:< typeOf[Iterable[Int]]
516 | res1: Boolean = true
517 | ```
518 | 上面的是方法调用:typ1.=:=(typ2),虽然效果都是证明类型关系,但不要混淆。
519 |
520 | ------------------------------------------------------------
521 |
522 | ### 5.9.4 <:与<:<的差异
523 | ```
524 | object A{
525 | def test[T <: java.io.Serializable](i:T) {}
526 | test(1) // 编译时报错
527 |
528 | def test2[T](i:T)(implicit ev: T <:< java.io.Serializable) {}
529 | test2(1) // 同样编译时报错
530 | }
531 | ```
532 | 两者的效果似乎一样,应该怎么选择?
533 | [stackoverflow的解释如下:](http://stackoverflow.com/questions/19829770/whats-different-between-and-in-scala)
534 |
535 | ```
536 | def foo[A, B <: A](a: A, b: B) = (a,b)
537 |
538 | scala> foo(1, List(1,2,3))
539 | res1: (Any, List[Int]) = (1,List(1, 2, 3))
540 | ```
541 |
542 | 传入第一个参数是Int类型,第二个参数是List[Int],显然这不符合 B <: A 的约束,编译器在做类型推导的时候,为了满足这个约束,会继续向上寻找父类型来匹配是否满足,于是在第一个参数被推导为Any类型的情况下,List[Int] 符合Any的子类型。
543 |
544 | ```
545 | def bar[A,B](a: A, b: B)(implicit ev: B <:< A) = (a,b)
546 |
547 | scala> bar(1,List(1,2,3))
548 | :9: error: Cannot prove that List[Int] <:< Int.
549 | ```
550 |
551 | 通过隐式参数ev来证明类型时,类型推断过程不会像上面那样再向上寻找可能满足的情况,而直接报错。
552 |
553 | 确实,在用 `<:` 声明类型约束的时候,不如用`<:<`更严格。除了上面的类型推导之外,还存在隐式转换的情况下:
554 | ```
555 | scala> def foo[B, A<:B] (a:A,b:B) = print("OK")
556 |
557 | scala> class A; class B;
558 |
559 | scala> implicit def a2b(a:A) = new B
560 |
561 | scala> foo(new A, new B) //存在A到B的隐式转换,也可满足
562 | OK
563 |
564 | scala> def bar[A,B](a:A,b:B)(implicit ev: A<: bar(new A, new B) //隐式转换并不管用
567 | :17: error: Cannot prove that A <:< B.
568 | ```
569 | >另外`<:`和`<:<`的位置:
570 | `<:` 用在类型参数的位置`def foo[B, A<:B] (a:A,b:B)`
571 | `<:<` 用在隐式参数类型的位置 `def bar[A,B](a:A,b:B)(implicit ev: A<: 并不是 List