Scala教程之:静态类型


Scala是静态类型的,它拥有一个强大的类型系统,静态地强制以安全、一致的方式使用抽象,我们通过下面几个特征来一一说明:

  • 泛类型
  • 型变
  • 类型上界
  • 类型下界
  • 内部类
  • 抽象类型
  • 复合类型
  • 自类型
  • 隐式参数
  • 隐式转换
  • 多态方法
  • 类型推断

通过这些特性,为安全可重用的编程抽象以及类型安全的扩展提供了强大的基础。

泛类型

和java一样,Scala也有泛型的概念,在scala里面泛型是使用方括号 [] 来接受类型参数的。通常使用字母A来作为参数标志符,当然你也可以使用其他任意的参数名称。

class Stack[A] {
  private var elements: List[A] = Nil
  def push(x: A) { elements = x :: elements }
  def peek: A = elements.head
  def pop(): A = {
    val currentTop = peek
    elements = elements.tail
    currentTop
  }
}

要使用一个泛类型,将一个具体的类型替换掉A即可。

val stack = new Stack[Int]
stack.push(1)
stack.push(2)
println(stack.pop)  // prints 2
println(stack.pop)  // prints 1

上面的例子中,实例对象接收整型值,如果该类型有子类型,子类型也是可以传入的。

class Fruit
class Apple extends Fruit
class Banana extends Fruit

val stack = new Stack[Fruit]
val apple = new Apple
val banana = new Banana

stack.push(apple)
stack.push(banana)

型变

型变主要是针对泛类型来说的,用来表示这种复杂类型的相关性。型变主要有协变,逆变和不变三种情况。在类型系统中使用型变允许我们在复杂类型之间建立直观的连接,而缺乏型变则会限制类抽象的重用性。

class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A]  // An invariant class

协变

协变使用+A来表示。对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 C 和 D,如果 C 是 D 的子类型,那么 List[C] 就是 List[D] 的子类型。 这允许我们使用泛型来创建非常有用和直观的子类型关系。

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

上面的例子中类型 Cat 和 Dog 都是 Animal 的子类型。那么
List[Cat]和List[Dog]都是List[Animal]的子类。

下面看下协变的应用:

object CovarianceTest extends App {
  def printAnimalNames(animals: List[Animal]): Unit = {
    animals.foreach { animal =>
      println(animal.name)
    }
  }

  val cats: List[Cat] = List(Cat("Whiskers"),Cat("Tom"))
  val dogs: List[Dog] = List(Dog("Fido"),Dog("Rex"))

  printAnimalNames(cats)
  // Whiskers
  // Tom

  printAnimalNames(dogs)
  // Fido
  // Rex
}

在上面的例子中,方法 printAnimalNames 将接受动物列表作为参数,并且逐行打印出它们的名称。 如果 List[A] 不是协变的,最后两个方法调用将不能编译,这将严重限制 printAnimalNames 方法的适用性。

逆变

逆变和协变相反,使用-A来表示。对于某个类 class Writer[-A] ,使 A 逆变意味着对于两种类型 C 和 D,如果 C 是 D 的子类型,那么 Writer[D] 是 Writer[C] 的子类型。

考虑一下的例子:

abstract class Printer[-A] {
  def print(value: A): Unit
}

我们定义两个printer:

class AnimalPrinter extends Printer[Animal] {
  def print(animal: Animal): Unit =
    println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
  def print(cat: Cat): Unit =
    println("The cat's name is: " + cat.name)
}

如果 Printer[Cat] 知道如何在控制台打印出任意 Cat,并且 Printer[Animal] 知道如何在控制台打印出任意 Animal,那么 Printer[Animal] 也应该知道如何打印出 Cat 就是合理的。 反向关系不适用,因为 Printer[Cat] 并不知道如何在控制台打印出任意 Animal。 因此,如果我们愿意,我们应该能够用 Printer[Animal] 替换 Printer[Cat],而使 Printer[A] 逆变允许我们做到这一点。

object ContravarianceTest extends App {
  val myCat: Cat = Cat("Boots")

  def printMyCat(printer: Printer[Cat]): Unit = {
    printer.print(myCat)
  }

  val catPrinter: Printer[Cat] = new CatPrinter
  val animalPrinter: Printer[Animal] = new AnimalPrinter

  printMyCat(catPrinter)
  printMyCat(animalPrinter)
}

不变

默认情况下,Scala中的泛型类是不变的。这意味着虽然Cat是Animal的子类,但是Container[Cat]和Container[Animal]之间没有任何关系。

类型上界

像T <: A这样声明的类型上界表示类型变量T应该是类型A的子类。下面是个例子:

abstract class Animal {
 def name: String
}

abstract class Pet extends Animal {}

class Cat extends Pet {
  override def name: String = "Cat"
}

class Dog extends Pet {
  override def name: String = "Dog"
}

class Lion extends Animal {
  override def name: String = "Lion"
}

class PetContainer[P <: Pet](p: P) {
  def pet: P = p
}

val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)

// this would not compile
val lionContainer = new PetContainer[Lion](new Lion)

类PetContainer接受一个必须是Pet子类的类型参数P。因为Dog和Cat都是Pet的子类,所以可以构造PetContainer[Dog]和PetContainer[Cat]。但在尝试构造PetContainer[Lion]的时候会得到下面的错误信息:

type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet]

这是因为Lion并不是Pet的子类。

类型下界

类型下界和类型上界相反,B >: A 表示类型参数 B 或抽象类型 B 是类型 A 的超类型。

下面看个它使用的例子:

trait Node[+B] {
  def prepend(elem: B): Node[B]
}

case class ListNode[+B](h: B,t: Node[B]) extends Node[B] {
  def prepend(elem: B): ListNode[B] = ListNode(elem,this)
  def head: B = h
  def tail: Node[B] = t
}

case class Nil[+B]() extends Node[B] {
  def prepend(elem: B): ListNode[B] = ListNode(elem,this)
}

这个例子会在编译的时候报错。因为方法 prepend 中的参数 elem 是协变的 B 类型。

在scala中函数的参数类型是逆变的,而返回类型是协变的。

要解决这个问题,我们需要将方法 prepend 的参数 elem 的型变翻转。 我们通过引入一个新的类型参数 U 来实现这一点,该参数具有 B 作为类型下界。

trait Node[+B] {
  def prepend[U >: B](elem: U): Node[U]
}

case class ListNode[+B](h: B,t: Node[B]) extends Node[B] {
  def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem,this)
  def head: B = h
  def tail: Node[B] = t
}

case class Nil[+B]() extends Node[B] {
  def prepend[U >: B](elem: U): ListNode[U] = ListNode(elem,this)
}

这样就可以了。

内部类

内部类就是class里面的class,在java里面,内部类被看成是外部类的成员。但是在scala中内部类是和外部类的对象进行绑定的。这意味着即使是同一个外部类的不同对象,其包含的内部类是不同类型的。 我们举个例子:

class Graph {
  class Node {
    var connectedNodes: List[Node] = Nil
    def connectTo(node: Node) {
      if (!connectedNodes.exists(node.equals)) {
        connectedNodes = node :: connectedNodes
      }
    }
  }
  var nodes: List[Node] = Nil
  def newNode: Node = {
    val res = new Node
    nodes = res :: nodes
    res
  }
}

这里connectedNodes 中存储的所有节点必须使用同一个 Graph 的实例对象的 newNode 方法来创建。

val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
val node3: graph1.Node = graph1.newNode
node1.connectTo(node2)
node3.connectTo(node1)

这里三个node都是graph1.Node类型。如果是非graph1.Node类型则无法编译成功。

val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
node1.connectTo(node2)      // legal
val graph2: Graph = new Graph
val node3: graph2.Node = graph2.newNode
node1.connectTo(node3)      // illegal!

那如果想达到和java中内部内中一样的效果,不区分路径该怎么办呢?使用Graph#Node即可。

class Graph {
  class Node {
    var connectedNodes: List[Graph#Node] = Nil
    def connectTo(node: Graph#Node) {
      if (!connectedNodes.exists(node.equals)) {
        connectedNodes = node :: connectedNodes
      }
    }
  }
  var nodes: List[Node] = Nil
  def newNode: Node = {
    val res = new Node
    nodes = res :: nodes
    res
  }
}

抽象类型

抽象类型通常用T来表示,用在特质和抽象类中,表示实际类型可以由具体的实现类来确认:

trait Buffer {
  type T
  val element: T
}

通过抽象类来扩展这个特质后,就可以添加一个类型上边界来让抽象类型T变得更加具体。

abstract class SeqBuffer extends Buffer {
  type U
  type T <: Seq[U]
  def length = element.length
}

复合类型

复合类型很简单,表示多个类型的交集,用with来表示。

假设我们定义了两个traits:

trait Cloneable extends java.lang.Cloneable {
  override def clone(): Cloneable = {
    super.clone().asInstanceOf[Cloneable]
  }
}
trait Resetable {
  def reset: Unit
}

假如我们需要接受一个对象它即可以是Cloneable又可以是Resetable,那么可以这样定义:

def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {
  //...
}

自类型

自类型的意思是在一个trait中可以使用另外一个trait中定义的属性而不必去继承它。

要在特质中使用自类型,写一个标识符,跟上要混入的另一个特质,以及 =>(例如 someIdentifier: SomeOtherTrait =>)。

trait User {
  def username: String
}

trait Tweeter {
  this: User =>  // 重新赋予 this 的类型
  def tweet(tweetText: String) = println(s"$username: $tweetText")
}

class VerifiedTweeter(val username_ : String) extends Tweeter with User {  // 我们混入特质 User 因为 Tweeter 需要
	def username = s"real $username_"
}

val realBeyoncé = new VerifiedTweeter("Beyoncé")
realBeyoncé.tweet("Just spilled my glass of lemonade")  // 打印出 "real Beyoncé: Just spilled my glass of lemonade"

因为我们在特质 trait Tweeter 中定义了 this: User =>,现在变量 username 可以在 tweet 方法内使用。 这也意味着,由于 VerifiedTweeter 继承了 Tweeter,它还必须混入 User(使用 with User)。

隐式参数

隐式参数由 implicit 关键字标记,在方法调用的时候,scala会去尝试获取正确的隐式类型值。

Scala查找参数的位置有两个地方:

  • 首先查找可以直接访问的隐式定义和隐式参数。
  • 然后,它在所有伴生对象中查找与隐式候选类型相关的有隐式标记的成员。

下面的例子定义了两个隐式类型,stringMonoid和intMonoid。

abstract class Monoid[A] {
  def add(x: A,y: A): A
  def unit: A
}

object ImplicitTest {
  implicit val stringMonoid: Monoid[String] = new Monoid[String] {
    def add(x: String,y: String): String = x concat y
    def unit: String = ""
  }
  
  implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
    def add(x: Int,y: Int): Int = x + y
    def unit: Int = 0
  }
  
  def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
    if (xs.isEmpty) m.unit
    else m.add(xs.head,sum(xs.tail))
    
  def main(args: Array[String]): Unit = {
    println(sum(List(1,2,3)))       // uses IntMonoid implicitly
    println(sum(List("a","b","c"))) // uses StringMonoid implicitly
  }
}

在 main 方法中我们调用了 sum 方法两次,并且只传入参数 xs。 Scala 会在上例的上下文范围内寻找隐式值。 第一次调用 sum 方法的时候传入了一个 List[Int] 作为 xs 的值,这意味着此处类型 A 是 Int。 隐式参数列表 m 被省略了,因此 Scala 将查找类型为 Monoid[Int] 的隐式值。

intMonoid 是一个隐式定义,可以在main中直接访问。 并且它的类型也正确,因此它会被自动传递给 sum 方法。

第二次调用 sum 方法的时候传入一个 List[String],这意味着此处类型 A 是 String。 与查找 Int 型的隐式参数时类似,但这次会找到 stringMonoid,并自动将其作为 m 传入。

隐式转换

简单点讲,隐式转换就是当需要的时候,将某个类型S转换到另外一个类型T。这是通过定义隐式函数来确定的。

下面提供了一个隐式方法 List[A] => Ordered[List[A]] 的例子。

import scala.language.implicitConversions

implicit def list2ordered[A](x: List[A])
    (implicit elem2ordered: A => Ordered[A]): Ordered[List[A]] =
  new Ordered[List[A]] { 
    //replace with a more useful implementation
    def compare(that: List[A]): Int = 1
  }

如果需要Ordered[List[A]] 而你传入List[A]的时候,scala会自动去寻找隐式的类型转换方法。

下面是一个从scala.Int到java.lang.Integer的转换:

import scala.language.implicitConversions

implicit def int2Integer(x: Int) =
  java.lang.Integer.valueOf(x)

多态方法

Scala中多态是通过类型和值的参数化来实现的。 如下所示:

def listOfDuplicates[A](x: A,length: Int): List[A] = {
  if (length < 1)
    Nil
  else
    x :: listOfDuplicates(x,length - 1)
}
println(listOfDuplicates[Int](3,4))  // List(3,3,3)
println(listOfDuplicates("La",8))  // List(La,La,La)

上例中第一次调用方法时,我们显式地提供了类型参数 [Int]。 因此第一个参数必须是 Int 类型,并且返回类型为 List[Int]。

上例中第二次调用方法,表明并不总是需要显式提供类型参数。 编译器通常可以根据上下文或值参数的类型来推断。 在这个例子中,“La” 是一个 String,因此编译器知道 A 必须是 String。

类型推断

Scala 编译器通常可以推断出表达式的类型,因此你不必显式地声明它。

val businessName = "Montreux Jazz Café"

编译器可以发现 businessName 是 String 类型。

你也可以省略方法返回类型:

def squareOf(x: Int) = x * x

编译器可以推断出方法的返回类型为 Int,因此不需要明确地声明返回类型。

当调用 多态方法 或实例化 泛型类 时,也不必明确指定类型参数。 Scala 编译器将从上下文和实际方法的类型/构造函数参数的类型推断出缺失的类型参数。

看下面两个例子:

case class MyPair[A,B](x: A,y: B)
val p = MyPair(1,"scala") // type: MyPair[Int,String]

def id[T](x: T) = x
val q = id(1)              // type: Int

编译器使用传给 MyPair 参数的类型来推断出 A 和 B 的类型。对于 x 的类型同样如此。

更多教程请参考 flydean的博客

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


共收录Twitter的14款开源软件,第1页Twitter的Emoji表情 TwemojiTwemoji是Twitter开源的其完整的Emoji表情图片。开发者可以去GitHub下载完整的表情库,并把这些表情加入到自己的应用或网页中。使用示例:var i = 0;twemoji.parse(  ’emoji, m\u276
Java和Scala中关于==的区别Java:==比较两个变量本身的值,即两个对象在内存中的首地址;equals比较字符串中所包含的内容是否相同。publicstaticvoidmain(String[]args){​ Strings1="abc"; Strings2=newString("abc");​ System.out.println(s1==s2)
本篇内容主要讲解“Scala怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Scala怎么使用”吧!语法scala...
这篇文章主要介绍“Scala是一种什么语言”,在日常操作中,相信很多人在Scala是一种什么语言问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,
这篇文章主要介绍“Scala Trait怎么使用”,在日常操作中,相信很多人在Scala Trait怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,
这篇文章主要介绍“Scala类型检查与模式匹配怎么使用”,在日常操作中,相信很多人在Scala类型检查与模式匹配怎么使用问题上存在疑惑,小编查阅了各式资料,整理...
这篇文章主要介绍“scala中常用但不常见的符号有哪些”,在日常操作中,相信很多人在scala中常用但不常见的符号有哪些问题上存在疑惑,小编查阅了各式资料,整理...
本篇内容主要讲解“Scala基础知识有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Scala基础知识有哪些”...
本篇内容介绍了“scala基础知识点有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧...
本篇内容介绍了“Scala下划线怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧...
本篇内容主要讲解“Scala提取器怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Scala提取器怎么使用”...
这篇文章主要讲解了“Scala基础语法有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Scala基础语法有...
本篇内容主要讲解“Scala方法与函数怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Scala方法与函数怎...
这篇文章主要讲解了“scala条件控制与循环怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“scala条...
这篇文章主要介绍“scala函数怎么定义和调用”,在日常操作中,相信很多人在scala函数怎么定义和调用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操
这篇文章主要介绍“scala如何声明变量”,在日常操作中,相信很多人在scala如何声明变量问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对...
这篇文章主要讲解了“scala的Map和Tuple怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“scala的Ma...
这篇文章主要介绍“scala的隐式参数有什么作用”,在日常操作中,相信很多人在scala的隐式参数有什么作用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的...
本篇内容主要讲解“Scala怎么进行文件写操作”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Scala怎么进行文件...
这篇文章主要讲解了“Scala怎么声明数组”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Scala怎么声明数组...