印度朋友手把手教你学Scala(8):特质


/**
 * 谨献给我最爱的YoYo 
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-8-traits/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-13
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

特质

这是关于Scala系列教程的第八章。点击这里可查看整个系列。

目录

  • 特质简介
  • 一个基本的特质 - 语法解释
  • Java的抽象变量概念
  • 抽象变量的类型注解
  • 混合抽象和具体成员
  • Java语法区别
  • 扩展特质
  • with关键字
  • 混入类的组成
  • 结论

特质简介

对于Scala,特质就像样本类那样,是一个新的概念。它们补充了OOP中已有的特性。

理解特质一个更好的方式是把它和Java语言的特性进行比较。

它们类似于接口,但它们可以有方法的实现,它们像抽象类一样工作,除了特质没有构造函数外。

最好的方式是通过例子来了解他们。所以,让我们来动手吧。

一个基本的特质 - 语法解释

声明一个特质,以trait关键字开始,然后是特质名称,后跟特质的主体/内容。

trait Book {

  val id : Int
  val name : String
  val isbn : Long

}

Java的抽象变量概念

如果你是Java后端开发,那么上面的例子可能会让你觉得困惑。在java中,没有抽象变量这样的东西。

在Java只有方法和类是抽象的,变量不行。

由于我们没有抽象变量,所以也不能覆盖变量。

Scala在这方面与众不同,即为什么它有抽象变量,其原因与两个函数式编程概念有关。一个是统一访问原则,我们之前在类时看到过的,而另一个是参照透明度

由于这些概念很复杂,他们需要各自额外的博客来解释,我们会在以后进行探讨。

由于Scala努力使用统一访问原则来看待方法和值,即一切都是值类型,所以我们也就有了抽象变量。

抽象变量的类型注解

抽象变量应该对它们的进行类型注解,不管它们是在特质中还是在抽象类中。当我解释Scala类时我应该解释这个概念,但由于特质也使用它们,我想我应该在这里进行解释。

如果我们没有提到类型,那么会得到如下错误。

记住,Scala有局部类型推理,所以我们需要明确地注释类型。

对于方法,方案稍有不同。我们可以声明一个方法,如下。

trait Book {

  def noExplicitTypeAnnotation

}

它是一个有效的语法,并且没有编译错误。

注意这里有一些细微的差别。def noExplicitTypeAnnotation等同于def noExplicitTypeAnnotation() : Unit。类型Unit等效于Java中的void。它表示字面意义上没有元素。不要和Scala中存在的其他类型混淆,例如NothingNull,稍后我会介绍。

当应用于变量时,Unit类型没有意义,因此编译器会和它对待方法一样推断该类型。

然而,我们可以把Unit作为一个类型赋给一个变量。

  val id : Unit = Unit

  println(id)

它将会打印(),但用Unit注释变量类型是没有意义的。

混合抽象和具体成员

到目前为止,我们只看到抽象变量 实际上,我们会混合使用抽象变量,方法和/或具体变量/方法。

让我们看个例子。

trait Book {

    val id : Int
    val name : String
    val isbn : Long
    val price : Double
    // 具体的变量
    val category = "Uncategorized"

   // 具体的实现  
   def getTaxOnPrice : Double = {
     (price * 14)/100
   }

}

正如我们在方法教程中看到的,如果我们省略了=后面的部分,那么它就成了一个抽象方法。

请注意,类型推理适用于具体变量,因为它在编译时是已知的,并且类型注释是可选的。

Java语法区别

在Java中,我们有接口和抽象类,扩展抽象类/类和实现接口。

public class Child extends Root implements Interface1,Interface2 {  
}

Scala没有interface和implements这些关键字。

语法上有微妙的差异,此外还有一个新的关键字叫做with,我们将在下面探讨。

扩展特质

类似于Java,Scala有extends关键字,可用于扩展类和特质。

特质可以通过其他特质,抽象类,具体类和样本类来扩展。

特质继承特质

由于特质不能被实例化,可以不必实现抽象成员。

trait ScienceBook extends Book{

}

但如果我们需要实现任意一个具体成员,则需要override修饰符。

以上的正确版本将是。

trait ScienceBook extends Book{

  override val category: String = "Science Book"

}

抽象类继承特质

抽象类也可以继承特质。

适用于特质继承特质的相同原理也适用于此。

abstract class ScienceBook extends Book{

  override val category: String = "Science Book"

}

类继承特质

由于类是具体的,即可以创建实例,因此应该实现特质的抽象成员。

这个类正确的实现版本应该如下所示。

class ScienceBook extends Book{

  override val id: Int = 1000
  override val name: String = "A Brief History of Time"
  override val isbn: Long = 9783499605550l
  override val price: Double = 7.43

}

我们没有实现getTaxOnPrice方法和变量category,因为它们是具体成员。类型注释当然是可选的,它们存在于我的代码示例中,因为IDE Intellij自动生成了它们。

如果我们需要修改逻辑,当然可以覆盖它们的实现。

class ScienceBook extends Book{

  override val id: Int = 1000
  override val name: String = "A Brief History of Time"
  override val isbn: Long = 9783499605550l
  override val price: Double = 7.43

  override val category: String = "Science book"

  override def getTaxOnPrice : Double = {
    (price * 10)/100
  }

}

由于样本类继承是一个复杂的主题,我将在一个专门的教程中进行解释。

with关键字

因为在Scala中没有interface和implements这些关键字的概念,那如何同时继承特质和类呢?

在Java中,通常会这样做

public class Root extends Ex1 implements Intef1,Intef2 {  
}

对此,Scala则有一个新的关键字。

让我们来考虑另外一个叫做Product的抽象类。

abstract class Product {

  val prodID : Int
  val skuID : Int


}

现在既然一本书是一个产品,我们可以组合这些逻辑。

class ScienceBook extends Product with Book{

  override val id: Int = 1000
  override val name: String = "A Brief History of Time"
  override val isbn: Long = 9783499605550l
  override val price: Double = 7.43

  override val category: String = "Science book"

  override def getTaxOnPrice : Double = {
    (price * 10)/100
  }

  // 抽象类Product的成员
  override val prodID: Int = 20001504
  override val skuID: Int = 4574555
}

混入类的组成

现在想象一下,某些情况下,我们不需要扩展Product类,有些情况我们又需要扩展。

这是不可能用存在于Java中的OOP概念来解决的,因为我们需要声明ScienceBook类先前扩展了什么。

Scala有一些叫做混入(mixins)的东西,它们可以做出一个类组合的混合,我们可以选择扩展Product特性,而不修改它的原始类层次结构。

为了演示,我们需要对抽象类Product进行一点简单的修改,并将其改为特质。

trait Product {

  val prodID : Int
  val skuID : Int

}

接下来在实例声明期间,现在我们可以使用不同的语法扩展产品特性,如下所示。

  // 用混入扩展
  val scBook = new ScienceBook() with Product {
    override val prodID: Int = 1000
    override val skuID: Int = 2000
  }

  // 原始类实例
  val scBookWithoutProduct = new ScienceBook()

由于我们创建了一个实际的实例,所以需要重写抽象变量。

然而,原始的ScienceBook类在其逻辑上保持不变,即它不需要扩展Product特质。现在,通过一种非常整齐的方式,我们有了Book,ScienceBook,Product类和特质的功能。

关于理解这些混入,Scala文档文章写得也很好。

结论

相比于接口,特质与抽象类关系更为密切。主要区别是特质没有构造函数。每当你为OOP逻辑而需要一个构造函数,那么一个抽象类会更合适,对于其他情况特质则会更好。

关于特质,有很多复杂的主题,其中有两块,我会在后面的教程中特别说到,他们是:

1)特质线性化(Trait linearization) - 由于特质允许定义,可以继承多个特质,那么如何处理多重继承这个老问题?

2)Sealed特质 - Sealed关键字表示其他文件中的类不能扩展此特质,但远不止这些。

这就结束了这篇文章。我已经在这篇文章中涵盖了前言和性和更容易的主题,并计划在后面的文章中一次一个地涵盖高级主题。

敬请关注!^_^

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州