本章要点
- 单例类型可用于方法串接和带对象参数的方法。
- 类型投影对所有外部类型的对象都包含了其内部类的实例。
- 类型别名给类型指定一个短小的名称。
- 结构类型等效于“鸭子类型”。
- 存在类型为泛型类型的通配参数提供了统一形式。
- 使用自身类型来表明某特质对混入它的类或对象的类型要求。
- “蛋糕模式”用自身类型来实例依赖注入。
- 抽象类型必须在子类中被具体化。
- 高等类型带有本身为参数化类型的类型参数。
单例类型
给定任何引用v,你可以得到类型v.type,它有两个可能的值: v 和 null 。例如我们想让方法返回this,从而实现方法的串接调用:
class Document {
def setTitle(title: String) = {...; this}
def setAuthor(author: String) = {...; this}
...
}
// 级联使用
val article = new Document
article.setTitle("Whatever Floats Your Boat").setAuthor("Cay Horstmann")
不过,要是还有子类,问题就来了:
class Book extends Document {
def addChapter(chapter: String) = {...; this}
...
}
val book = new Book()
book.setTitle("Scala for the Impatient").addChapter("chapter1") // error
由于setTitle返回的是this, Scala将返回类型推断为Document。但Document并没有addChapter方法。
解决方法是声明setTitle的返回类型为this.type :
def setTitle(title: String): this.type = {...; this}
这样book.setTitle("...")返回类型就是book.type。
如果你想定义一个接受object实例作为参数的方法,你也可以使用单例类型。例如
object Title
class Document {
private var useNextArgAs: Any = null
def set(obj: Title.type): this.type = {useNextArgAs = obj; this}
def to(arg: String) = if (useNextArgAs == Title) title = arg; else ...
...
}
class Book extends Document {}
book.set(Title).to("Scala for the Impatient")
注意Title.type参数,你不能用:
def set(obj: Title) ... // error, Title代指的是单例对象,而不是类型
类型投影
在第5章的嵌套类中已经有过例子。http://blog.csdn.net/dwb1015/article/details/51706746
路径
形如: com.horstmann.impatient.chatter.Member 这样的表达式称之为路径。在最后的类型前,路径的所有组成部分都必须是“稳定的”,也就是说它必须指定到单个、有穷的范围。组成的部分必须是以下当中的一种:
- 包
- 对象
- val
- this、super、super[S]、C.this、C.super或C.super[S]
路径的组成部分不能是类。也不能是var。 例如:
val aly = new Network.Member // error Network是类
var chatter = new Network
val fred = new chatter.Member // error chatter不稳定
类型别名
对于复杂类型,你可以用type关键字创建一个简单的别名,这和C/C++的typedef相同。
class Book {
import scala.collection.mutable._
type Index = HashMap[String, (Int, Int)]
}
类型别名必须被嵌套在类或对象中。它不能出现在Scala文件的顶层。
结构类型
所谓的“结构类型”指的是一组关于抽象方法、字段和类型的规格说明,这些抽象方法、字段和类型是满足该规格的类型必须具备的。
def appendLines(target: { def append(str: String): Any }, lines: Iterable[String]) {
for (l <- lines) { target.append(l); target.append("\n") }
}
你可以对任何具备append方法的类的实例调用appendLines方法。这比定义一个有appendLines方法的特质更为灵活,因为你不能总是能够将该特质添加到使用的类上。
但是类型结构背后使用的是反射,而反射调用的开销比较大。因此,你应该只在需要抓住那些无法共享一个特质的类的共通行为的时候才使用结构类型。
复合类型
复合类型的定义形式如下:
T1 with T2 with T3 ...
在这里,要想成为该复合类型的实例,某个值必须满足每一个类型的要求才行。例如:
val image = new ArrayBuffer[java.awt.Shape with java.io.Serializable]
val rect = new Ractangle(5, 10, 20, 30)
image += rect // OK , Rectangle是Serializable的
image += new Area(rect) // error, Area是Shape但不是Serializable的
你可以把结构类型的声明添加到简单类型或复合类型。例如:
Shape with Serializable { def contains(p: Point): Boolean}
该类型必须既是一个Shape的子类型也是Serializable的子类型,并且还必须有一个带Point参数的contains方法。
从技术上讲,
// 结构类型
{ def append(str: String): Any }
// 是如下代码的简写
AnyRef { def append(str: String): Any }
// 复合类型
Shape with Serializable
// 是如下代码的简写
Shape with Serializable {}
中置类型
中置类型是一个带有两个类型参数的类型,以“中置”语法表示,类型名称写在两个类型参数之间。例如:
String Map Int
// 而不是
Map[String, Int]
存在类型
存在类型被加入Scala是为了与Java的类型通配符兼容。存在类型的定义是在类型表达式之后跟上forSome{...},花括号中包含了type和val声明。例如:
Array[T] forSome {type T <: JComponent}
// 这与类型通配符效果相同
Array[_ <: JComponent]
Scala的类型通配符只不过是存在类型的语法糖。例如:
Array[_]
// 等同于
Array[T] forSome { type T }
Map[_, _]
// 等同于
Map[T, U] forSome { type T; type U }
你也可以在forSome代码块中使用val声明,因为val可以有自己的嵌套类型。例如:
n.Member forSome { val n: Network}
Scala类型系统
自身类型
在第十章 http://blog.csdn.net/dwb1015/article/details/51761510 已经有过介绍。
**注意: **自身类型不会自动继承, 你需要重复自身类型的声明:
trait ManagedException extends LoggedException {
this: Exception =>
...
}
依赖注入
在Scala中,你可以通过特质和自身类型达到一个简单的依赖注入的效果。
trait Logger {
def log(msg: String)
}
class ConsoleLogger(str: String) extends Logger {
def log(msg: String) = {
println("Console: " + msg)
}
}
class FileLogger(str: String) extends Logger {
def log(msg: String) = {
println("File: " + msg)
}
}
// 用户认证特质有一个对日志功能的依赖
trait Auth {
this: Logger =>
def login(id: String, password: String): Boolean
}
// 应用逻辑有赖于上面两个特质
trait App {
this: Logger with Auth =>
...
}
// 组装应用
object MyApp extends App with FileLogger("test.log") with MockAuth("users.txt")
像这样使用特质的组合有些别扭。毕竟,一个应用程序并非是认证器和文件日志器的合体。更自然的表示方式可能是通过实例变量来表示组件。
蛋糕模式给出了更好的设计。在这个模式当中,你对每个服务都提供一个组件特质,该特质包含:
- 任何所依赖的组件,以自身类型表述。
- 描述服务接口的特质。
- 一个抽象的val,该val将被初始化成服务的一个实例。
- 可以有选择性的包含服务接口的实现。
trait LoggerComponent {
trait Logger { ... }
val logger: Logger
class FileLogger(file: String) extends Logger { ... }
...
}
trait AuthComponent {
this: LoggerComponent => // 让我们可以访问日志器
trait Auth { ... }
val auth: Auth
class MockAuth(file: String) extends Auth { ... }
...
}
// 使用组件
object AppComponents extends LoggerComponent with AuthComponent {
val logger = new FileLogger("test.log")
val auth = new MockAuth("users.txt")
}
抽象类型
类或特质可以定义一个在子类中被具体化的抽象类型。例如:
trait Reader {
type Contents
def read(fileName: String): Contents
}
class StringReader extends Reader {
type Contents = String
def read(fileName: String) = Source.fromFile(filename, "UTF-8").mkString
}
class ImageReader extends Reader {
type Contents = BufferedImage
def read(fileName: String) = ImageIO.read(new File(fileName))
}
同样的效果可以通过类型参数来实现:
trait Reader[C] {
def read(fileName: String): C
}
class StringReader extends Reader[String] {
def read(fileName: String) = Source.fromFile(filename, "UTF-8").mkString
}
class ImageReader extends Reader[BufferedImage] {
def read(fileName: String) = ImageIO.read(new File(fileName))
}
那种方式更好呢?Scala经验法则:
- 如果类型是在类被实例化时给出,则使用类型参数。
- 如果类型是在子类中给出,则使用抽象类型。
抽象类型可以有类型界定,就和参数类型一样。例如:
trait Listener {
type Event <: java.util.EventObject
...
}
// 子类必须提供一个兼容的类型
trait ActionListener extends Listener {
type Event = java.awt.event.ActionEvent
}