了解Scala中的泛型和变量

296 阅读6分钟

阅读时间: 5 分钟

通用类是将一个类型作为参数的类。这意味着,一个类可以用于不同的类型,而实际上不需要多次写下它。这些对于集合类特别有用。

1.概述

在这篇博客中,我们将探讨Scala泛型在实现容器方面的优势。我们将研究Scala泛型如何提供类型安全,同时帮助我们坚持DRY原则

我们将经历编写泛型类和方法的步骤,并探索标准Scala库中可用的泛型类型。

2.泛型方法。

在编写接受泛型输入或产生泛型返回值的Scala方法时,这些方法本身可能是泛型的,也可能不是。

2.1.泛型声明语法。

声明泛型方法与声明泛型类非常相似。 我们仍然将类型参数放在方括号中。

而且,方法的返回类型也可以被参数化。

def middle[A](input: Seq[A]): A = input(input.size / 2)

这个方法接收一个包含选定类型的项目的Seq,并返回Seq中间的项目。

val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7)) 
val middleRabbit: Rabbit = middle[Rabbit](rabbits)

现在让我们来看看一个有多个类型参数的例子。

def itemsAt[A, B](index: Int, seq1: Seq[A], seq2: Seq[B]): (A, B) = (seq1(index), seq2(index))

这个方法接收两个Seq中的索引元素,并返回一个元组,与输入中使用的类型匹配。

val apples = List[Apple](Apple("gala"), Apple("pink lady"))
val items: (Rabbit, Apple) = itemsAt[Rabbit, Apple](1, rabbits, apples)

我们应该注意到,上面的例子并不适合生产,因为它们不能正确处理诸如空Seq这样的边缘情况。然而,它们展示了类型参数如何帮助我们强制执行参数和返回值的类型。

2.2.在非泛型方法中使用泛型类。

在使用泛型类时,我们并不总是写泛型方法。例如,一个接收两个任何类型的Lists并返回总长度的方法可以被声明为。

def totalSize(list1: List[_], list2: List[_]): Int

_(下划线)意味着List中的内容并不重要。这里没有类型参数,因此,调用者不需要为这个方法提供类型。

val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7))
val strings = List("a", "b")
val size: Int = totalSize(rabbits, strings)

在这个例子中,当函数被调用时,类型没有被提供给它。

3.上层类型界线

为了说明Scala的上层类型界限,让我们重新发明轮子,编写一个基本的但又是泛型的函数来寻找一个集合中的最大元素。下面是我们的第一次尝试。

def findMax[T](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)

尽管逻辑上似乎没有问题,但是*>=函数并没有定义在泛型T*上。

我们知道,Ordered[T]是这种比较函数的归宿。所以,**我们应该以某种方式告诉编译器,在这个例子中,T是*Ordered[T]***的一个子类型。

事实证明,Scala泛型中的上界类型可以为我们做到这一点。

def findMax[T <: Ordered[T]](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)

通过"T <:语法,我们表示Ordered[T]是类型参数T**的上位类型,也就是说,xs列表中的每个元素都应该是*Ordered[T]的子类型。这样,我们就可以对其使用>=*和其他比较函数。

4.类型下限

让我们举一个例子。

class Queue[+T](private val leading: List[T], trailing: List[T]) {
  def head(): T = // returns the first element
  def tail(): List[T] = // everything but the first element
  def enqueue(x: T): Queue[T] = // appending to the end
}

这里,我们试图用两个列表来表示一个队列。

现在,假设我们需要处理一个Queue[String]。因为我们希望Queue[String]Queue[Any] 的子类型,所以我们使用了共变类型注解*[+T]。*然而,现在 enqueue 方法无法编译。

covariant type T occurs in contravariant position in type T of value x

def enqueue(x: T): Queue[T] = new Queue(leading, x :: trailing)

类型参数*[+T]*是协变的,但我们把它用在了协变的位置(函数参数),编译器对此提出了抱怨。

解决这个问题的一个方法是使用下限类型。

def enqueue[U >: T](x: U): Queue[U] = new Queue(leading, x :: trailing)

在这里,我们要定义一个新的类型参数U,同时,"U >:T "的语法意味着U应该是U的一个超类型。T现在,我们可以使用enqueue方法了

val empty = new Queue[String](Nil, Nil)
val stringQ: Queue[String] = empty.enqueue("The answer")
val intQ: Queue[Any] = stringQ.enqueue(42)

当我们向Queue[String]中添加Int时,编译器会自动推断出离StringInt最近的超类型。 因此返回的类型是Queue[Any]

Variance定义了参数化类型的继承关系。它是所有关于子类型的。它使Scala集合更加类型安全。
Variance的类型:

  1. Covariant
  2. 不变体
  3. 同变体
class Stack[+A] // A covariant class
class Stack[-A] // A contravariant class
class Stack[A] // An invariant class

5.1 共变体

如果S是T的一个子类型,那么List[S]就是List[T]的一个子类型。

在Scala中,表示两个参数化类型之间协变关系的语法是在Type Parameter前加上 "+"符号。让我们借助于一个例子来理解协变关系。

abstract class Flower 
{
    def name: String
}
  
// Creating a sub-class Lily 
// of Flower 
case class Lily(name: String) extends Flower
  
// Creating a sub-class Carnation
// of Flower 
case class Carnation(name: String) extends Flower 
object Covariance extends App
{
      
    // Creating a method
    def FlowerNames(flowers: List[Flower]): Unit =
    {   
        flowers.foreach 
        {
            flower => println(flower.name)
        }
    }
      
    // Assigning names
    val lily: List[Lily] = List(Lily("White Lily"), 
                                Lily("Jersey Lily"))
    val carnations: List[Carnation] = List(Carnation("White carnations"),
                                           Carnation("Pink carnations"))
  
    // Print: names of lily 
    FlowerNames(lily)
  
    // Print: names of carnation 
    FlowerNames(carnations)
}

5.2 等价关系

如果S是T的一个子类型,那么List[T]就是List[S]的一个子类型。

在Scala中,表示两个参数化类型之间的共变关系的语法是在Type Parameter前加上"-"符号。让我们借助于一个例子来理解反变量。

abstract class Type[-T]{
  def typeName(): Unit
}

class SuperType extends Type[AnyVal]{
  override def typeName(): Unit = {
    print("\n\n SuperType \n\n")
  }
}

class SubType extends Type[Int]{
  override def typeName(): Unit = {
    print("\n\n SubType \n\n")
  }
}

class TypeCarer{
  def display(t: Type[Int]){
    t.typeName()
  }
}

object ScalaContravariance {

  def main(args: Array[String]) {
    val superType = new SuperType
    val subType = new SubType

    val typeCarer = new TypeCarer

    typeCarer.display(subType)
    typeCarer.display(superType)
  }

}

5.3 不变量

如果ST的一个子类型,那么List[S]List[T]就没有继承关系或子类型。这意味着两者是不相关的。
在Scala中,默认情况下,泛型类型具有非变异关系。如果我们不使用 "
+
"或**"-"**符号来定义Parameterized Types,那么它们就被称为Invariants。让我们借助于一个例子来理解反变量。

abstract class Animal {
  def name: String
}

case class Cat(name: String) extends Animal

case class Dog(name: String) extends Animal

class Container[A](value: A) {
  private var _value: A = value
  def getValue: A = _value
  def setValue(value: A): Unit = {
    _value = value
  }
}

object Invariance {

  val catContainer: Container[Cat] = new Container[Cat](Cat("tom"))

  // This won't compile
  val animalContainer: Container[Animal] = catContainer

}

6.总结

总结这篇博客,我们得出以下结论。

  • 通用类是以类型为参数的类,也就是说,一个类可以用于不同的类型,而不需要实际写下多次。
  • 泛型类的方法
  • 差异性定义了参数化类型的继承关系。
  • 各种类型的变异是。不变型、共变型和同变型。
  • 不变型。如果ST的一个子类型,那么**List[S]List[T]**不存在继承关系或子类型。
  • 不变量。如果S是T的一个子类型,那么List[S]就是List[T]的一个子类型。
  • 不变的。如果S是T的一个子类型,那么List[T]是List[S]的一个子类型。