scala学习笔记 - 高级类型(一)

本文介绍了Scala中的单例类型如何用于方法返回值,类型投影在处理嵌套类和细粒度类型时的作用,以及结构类型和复合类型的使用场景,展示了如何创建流畅接口和使用存在类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例类型

给定任何引用v,你可以得到类型v.type,它有两个可能的值:v和null,这听上去像是一个挺古怪的类型,但它在有些时候很有用。
首先,我们来看那种返回this的方法,通过这种方式你可以把方法调用串接起来:

class Document { 
  def setTitle(title: String) = { ... ; this } 
  def setAuthor(author: String) = { ... ; this )
  ...
}

然后,你就可以编写如下代码:

val s = new Document
doc.setTitle("aa").setAuthor("bb")

不过,要是你还有子类,问题就来了:

class Book extends Document { 
  def addChapter(chapter: String) = { ...; this }
  ...
}

va l book = new Book () 
book.setTitle("Scala for the Impatient").addChapter("ppp") // 错误

由于setTitle返回的是this,Scala将返回类型推断为Document,但Document并没有addChapter方法;
解决方法是声明白setTitle的返回类型为this.type:

def setTitle(title: String): this.type = { ... ; this }

这样一来,book.setTitle("...")的返回类型就是book.type,而由于book有一个addChapter方法,方法串接就能成功了。
如果你想要定义一个接受object实例作为参数的方法,也可以使用单例类;你可能会纳闷;你什么时候才会这样做呢,毕竟,如果只有一个实例,方法直接用它就好了,为什么还要调用者将它传入呢?
不过,有些人喜欢构造那种读起来像英文的“流利接口( fluent interface )”:

book set Title to "Scala for the Impatient"

上述代码将被解析成:

book.set(Title).to("Scala for the Impatient")

要让这段代码工作,set得是一个参数为单例Title的方法:

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 ...
}

注意Title.type参数,你不能用

def set(obj: Title) ... // 错误

因为Title指代的是单例对象,而不是类型。

类型投影

如下示例:

import scala.collection.mutable.ArrayBuffer 
class Network ( 
	class Member (val name: String){
		val contacts =new ArrayBuffer[Mernber]
	} 
	private val members = new ArrayBuffer[Member] 
	def join(name: String) = { 
		val m =new Member(name) 
		members += m 
		m
	}
}

每个网络实例都有它自己的Member类。举例来说,以下是两个网络:

val chatter = new Network 
val myFace = new Network

现在chatter.Member和myFace.Member是不同的类。
你不能将其中一个网络(Network)的成员(Member)添加到另一个网络:

val fred = chatter.join("Fred") // 类型为 chatter.Member
val barney = myFace.join("Barney") // 类型为myFace.Member
fred.contacts += barney // 错误

如果你不希望有这个约束,就应该把Member类直接挪到Network类之外。一个好
的地方可能是Network的伴生对象中。
如果你要的就是细粒度的类,只是偶尔想使用更为松散的定义,那么可以用“类
型投影”Network#Member,意思是“任何Network的Member”。

class Network { 
	class Member (val name: String) { 
		val contacts = new ArrayBuffer[Network#Member] 
	}
	...
}

如果你想要在程序中的某些地方但不是所有地方使用“每个对象自己的内部类”这个细粒度特性的话,可以按照上面的方式处理。
注意: 类似于Network#Member这样的类型投影并不会被当作“路径”,你也无法引人它。

路径

考虑如下类型:

com.horstmann.mpatient.chatter.Member

或者,如果我们将Member嵌套在伴生对象当中的话,

com.horstmann.mpatient.Network.Member

这样的表达式被称为路径。
在最后的类型之前,路径的所有组成部分都必须是“稳定的”,也就是说,它必须指定到单个、有穷的范围。 组成部分必须是以下当中的一种:

  • 对象
  • val
  • this、super、super[S]、C.this、C.super或C.super[S]
    路径组成部分不能是类,因为正如你看到的那样,嵌套的内部类并不是单个类型,而是给每个实例都留出了各自独立的一套类型。
    不仅如此,类型也不能是var。例如:
var chatter = new Network
...
val fred = new chatter.Member // 错误, chatter不稳定

由于你可能将一个不同的值赋给chatter,因此编译器无法对类型chatter.Member做出明确解读。
说明:在内部,编译器将所有嵌套的类型表达式a.b.c.T都翻译成类型投影a.b.c.type#T。举例来说,chatter.Member就成为chatter.type#Member,任何位于chatter.type单例中的Member。这不是你通常需
要担心的问题。不过,有时候你会看到关于类型a.b.c.type#T的报错信息。将它翻译回a.b.c.T即可。

类型别名

对于复杂类型,你可以用type关键字创建一个简单的别名,就像这样:

class Book { 
  import scala.collection.mutable._
  type Index = HashMap[String, (Int, Int)]
}

这样一来,你就可以用Book.Index而不是更笨重的类型scala.collection.mutable.HashMap[String, (Int, Int)]。
类型别名必须被嵌套在类或对象中。它不能出现在Scala文件的顶层。不过,在REPL中你可以在顶层声明type,因为REPL中的所有内容都隐式地包含在一个顶层对象当中。
说明:type关键字同样被用于那些在子类中被具体化的抽象类型,例如:

abstract class Reader { 
  type Contents 
  def read(fileName: String): Contents
}

结构类型

所谓的“结构类型”指的是一组关于抽象方法 、字段和类型的规格说明,这些抽象方法、字段和类型是满足该规格的类型必须具备的。举例来说,如下方法带有一个结构类型的参数:

def appendLines (target: { def append(str: String): Any },
    lines: Iterable [String] ) { 
  for (l <- lines) { target.append(1); target.append ("\n")}
}

你可以对任何具备append方法的类的实例调用appendLines方法,这比定义一个Appendable特质更为灵活,因为你可能并不总是能够将该特质添加到使用的类上。
Scala使用反射来调用target.append(…)。结构类型让你可以安全而方便地做这样的反射调用。
不过,相比常规方法调用,反射调用的开销要大得多。因此,你应该只在需要抓住那些无法共享一个特质的类的共通行为的时候才使用结构类型。

复合类型

复合类型的定义形式如下:
T1 with T2 with T3
其中,T1、T2、T3等是类型。要想成为该复合类型的实例,某个值必须满足每一个类型的要求才行,因此,这样的类型也被称作交集类型。
你可以用复合类型来操纵那些必须提供多个特质的值。例如:

val image = new ArrayBuffer[java.awt.Shape with java.io.Serializable]

你可以用for (s <-image) graphics.draw(s)来绘制这个image对象;你也可以序列化这个image对象,因为你知道所有元素都是可被序列化的。
当然了,你只能添加那些既是形状(Shape)也是可被序列化的对象:

val rect = new Rectangle(5, 10, 20, 30) 
image += rect //OK, Rectangle是Serializable的
image += new Area(rect) // 错误, Area是Shape但不是Serializable的

说明:当你有如下声明时,

trait ImageShape extends Shape with Serializable

这段代码意昧着ImageShape扩展自交集类型Shape with 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]

中置表示法在数学当中是很常见的,举例来说,$A × \times × B = {(a,b) | a ∈ \in A, b ∈ \in B}$指的是组件类型分别为A和B的对偶的集,在Scala 中,该类型被写作(A, B)。 如果你倾向于使用数学表示法,则可以这样来定义:

type x[A, B] = (A, B)

在此之后你就可以写String x Int而不是(String, Int)了。
所有中置类型操作符都拥有相同的优先级,和常规操作符一样,它们是左结合的,除非它们的名称以:结尾。例如:

String x Int x Int

述代码的意思是((String, Int), Int),该类型与(String, Int, Int)相似但不相同,后者不能在Scala中以中置表示法写出。
说明:中置类型的名称可以是任何操作符字符的序列(除单个*号外)。这个规则是为了避免与变长参数声明 T* 混淆。

存在类型

存在类型被加人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表示法允许我们使用更复杂的关系,而不仅限于类型通配符能表达的那些, 例如:

Map[T, U] forSome{type T; type U <: T}

你也可以在forSome代码块中使用val明,因为val可以有自己的嵌套类型。如下是一个示例:

n.Member forSome{ val n: Network }

就其自身而言,它并没有什么特别的用处,你完全可以用类型投影Network#Member。不过也有更复杂的情况:

def process[M <: n.Member forSome { val n: Network }](ml: M, m2: M) = (ml, m2)

该方法将会接受相同网络的成员,但拒绝那些来自不同网络的成员:

val chatter = new Network 
val myFace = new Network 
val fred = chatter.join("Fred") 
val wilma = chatter. Join("Wilma") 
val barney = myFace. join("Barney") 
process(fred, wilma) // OK 
process(fred, barney) // 错误

说明:要不带警告地使用存在类型,你必须引人scala.language.existentials或使用编译器选项-language:existentials

Scala类型系统

Scala语言参考给出了所有Scala类型的完整清单,如下:

类型语法
类或特质class C … , trait C …
元组类型(T1, T2, … Tn)
函数类型(T1, T2, … Tn) => T
带注解的类型T @A
参数化类型A[T1, T2, … Tn]
单例类型值.type
类型投影O#I
复合类型T1 with T2 with … Tn { 声明 }
中置类型T1 A T2
存在类型T forSome { type和val声明 }

还有一些类型是Scala编译器内部使用的。比如,方法类型表示为(T1 with T2 with … Tn)T,不带=>。偶尔会看到这样的类型。举例来说,当在REPL中键入如下代码时:

def square(x: Int) = x * x

它的晌应为:

square(x: Int)Int

这和

val triple = (x: Int) => 3 * x

不同,后者交出的是:

triple: Int => Int

也可以在方法后面跟一个_来将方法转成函数。比如:

square _ // 类型为: Int => Int

参考:快学scala(第二版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值