印度朋友手把手教你学Scala(6):样本类


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

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

样本类

样本类和类非常相似,但样本类做了很多东西并且有一些简洁的功能。

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

目录

  • 声明样本类
  • 不用new关键字创建类
  • 访问变量
  • 默认和部分构造器参数
  • 不可变对象
  • 结构相等 - 自动生成equals
  • 引用相等
  • 字符串表示
  • 自动生成hash码
  • 样本类复制
  • 样本类反编译

声明样本类

以下代码声明了一个叫做Block的类,有三个成员变量:idtitleisbn

case class Book(id:Int,title:String,isbn:Long)  

完美有效的语法,并且只需一行代码。

我们很可能会倾向于把它放在我们想放的地方,毕竟它只是一行代码,但是Scala风格指南相当多地集中在造型部分。

当然根据不同的需求情况也不一样,有人可能会把多个有类似功能的样本类一起放在同一个文件里。这个讨论是一个单独的主题,但如果你对把样本类放在哪感到疑惑,那么可以把它放在一个单独的文件里。

不用new关键字创建类

样本类可以不用new关键字来创建。

object RunExample extends App{

  val x = Book(100,"Stephen hawking's : A brief history of time",9788370017361L)

}

这只是出于避免冗长,事实上也可以包含new关键字,工作方式是一样的。

在语法上这可以和Java的字符串比较,他们也可以不用new关键字就能创建。

访问变量

变量可以类似他们的类副本那样访问。

object RunExample extends App{

  val book = Book(100,"Stephen hawking's : A brief history of time",9788370017361L)

  println(book.id)
  println(book.title)
  println(book.isbn)

}

请注意,在样本类里,变量默认是val,而不是像正常类里的private val,所以这里不需要特殊的getter。

默认和部分构造器参数

在前面的教程里,我们看到了如何使用主构造器把默认值给类。在样本类同样也可以这样做。

case class Book(  
private val id:Int = -1,private val title:String = "No name",private val isbn:Long = -1  
)

随后可以创建一个实例,并且像下面这样访问变量。

object RunExample extends App{

   val book = Book()

   println(book.id)
   println(book.title)
   println(book.isbn)

}

程序员有选择提供哪个值而让哪个使用默认值的自由。在下面的示例中,只提供了id参数,其余的是默认值。

object RunExample extends App{

   val book = Book(100)

   println(book.id)
   println(book.title)
   println(book.isbn)

}

如果想提供一个自定义的书的值,可以这样。

object RunExample extends App{

   val book = Book(title = "Lord of the Rings")

   println(book.id)
   println(book.title)
   println(book.isbn)

}

第二种风格是提供变量标识符和变量值,这被视为一种最佳实践,因为它以明确的方式注明了自定义值提供给了哪个变量。

需要注意的一件事是,这种给定部分构造器参数的风格也可以用于正常的Scala类里,但它更适合样本类。

不可变对象

样本类默认情况下是不可变的,即一旦它们声明了就不能再改变。在前面的教程中,我们简单地介绍了和类相似的东西。但是样本类本质上适合于创建不可变类,并且它们有一整套的优点,我们将在下面看到。

在不可变类的情况下,你不必担心直接变量访问/创建的getter和setter,因为它们不能被改变。这给了你一个更整洁的语法。

所有对于常规类的其他东西,例如val/var字段,getter/setter仍然适用于这里。

结构相等 - 自动生成equals

当在Java中比较对象时,最痛苦的任务之一就是结构相等性,即要比较类的各个变量/成员。

样本类生成了很多样板(boilerplate),其中就包括了相等性比较。我们可以像下面这样简单比较两个类。

object RunExample extends App{

  val book1 = Book(100,"Fifty Shades of Grey",9788490322178l)

  val book2 = Book(100,"Fifty Shades of Grey",9788490322178l)

  // 打印true
  println(book1 == book2)

}

作为一名老练的Java开发人员,可能已经有人教你不要在字符串/类使用==而是使用equals方法。在Scala则不太一样,但你当然可以使用println(book1.equals(book2)),并且仍然会打印true。

这里的==也会打印true,它是一种叫合成方法的东西,也调用了一个实际上调用equals方法的bridge方法。作为一个速记/方便,它由Scala编译器生成。

在Scala学习的旅途中,我们会经常遇到这类方法。并不需要理解它背后的具体实现。

引用相等

我们有可能会遇到需要比较实际引用的场景。

在Java可以会像上面那样使用==,但Scala做了一个设计决定,对于结构相等性使用双重equals,因为它在真实世界场景中更有用。有两个比较引用的专门方法,特别是这两个,即:eqne

object RunExample extends App{

  val book_1 = Book(title = "Something else")

  val book_2 = Book(title = "Something else")

  // book_1和book_2是不同的引用
  // 结果是false
  println(book_1 eq book_2)

  // 使用引用副本
  val book_1_copy = book_1

  // 结果是true
  println(book_1 eq book_1_copy)

}

ne实际就是eq的反义词。

object RunExample extends App{

  val book_1 = Book(title = "Something else")

  val book_2 = Book(title = "Something else")

  // 结果是false
  println(book_1 eq book_2)

  // 结果是true
  println(book_1 ne book_2)

}

这些是类似于==的合成方法(synthetic methods),AnyRef类的成员。

字符串表示

在一个普通的Java/Scala类,如果你在对象上调用了toString,通常上它会返回表示hash码的十六进制的字符串,在大多数情况下这是完全没用的。样本类有一个非常漂亮的toString方法,它提供了一个有意义的字符串来表示这个类。

object RunExample extends App{

  val book = Book(100,"Fifty Shades of Grey",9788490322178l)

  // 打印Book(100,Fifty Shades of Grey,9788490322178)
  println(book.toString)

}

它比出现在常规类的默认toString方法还要好,并且有助于调试。如果你的应用需要一些新的东西,那么你可以重载并且提供一个定制化的实现。

自动生成hash码

  • 到底什么是Hash码?
    这里假设读者已熟悉hash码的Java实现,如果不熟悉,它不过是出于把他们放进hash表的便利的一个实现。

    Java文档解释很好地对它进行了总结。

  • 在Java的实现
    需要明白的另一件事是,Java中的实现是本地方法。本地方法是依赖于直接机器实现而不是基于Java的实现的方法。

    对于开发人员,不需要理解这些内部的确切实现,但如果好奇,你可以看看它。

  • Hash码契约
    重要的是理解哈希码的API契约,总结如下。

    • 在Java应用程序的执行期间,无论何时在同一对象上多次调用该对象时,hashCode方法必须一致地返回相同的整数,如果对象修改了则提供不了在相等比较中有用的信息。从应用程序的一个执行到相同应用程序的另一个执行,此整数不需要保持一致。
    • 如果根据equals(对象)方法,两个对象是相等的,那么对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。
    • 如果两个对象根据equals(java.lang.Object)方法不是相等的,那么对两个对象中的每一个调用hashCode方法必须产生不同的整数结果不是必须的。然而,程序员应该意识到,为不等对象产生不同的整数结果可以提高散列表的性能。
  • Scala的哈希码
    Java中的默认哈希实现相当快,但是没有给出良好的抗冲突性,即更容易发生冲突。

    在scala,则有点不同。样本类使用一种称为Murmur哈希的算法,而常规类则使用默认哈希。

    这个主题一个优秀的讨论可以在Scala邮件列表上获得。

    关键点是,哈希的实现有可能会改变,开发人员不应该依赖它。

  • 何时要担心哈希码?
    底层实现由语言/平台开发人员保证,我们通常不需要担心它。但是无论出于什么原因,如果你正在实现自己的哈希算法,那么你需要注意在样本类、普通类和集合库中如何不同地处理它。

撤离关于在Scala里哈希的讨论,就是有一个equals和hashcode契约,当一个类修改了这些保证必须被注意到。在样本类中,这是自动完成的。

样本类复制

样本类的实例可以轻松地复制到其他样本类实例。

这与克隆有点不同。复制在幕后创建了一个新的对象,但它是一种远离程序员的抽象。

我们可以再次使用Book样本类。

case class Book(id:Int,title:String,isbn:Long)  
  • 完整复制
    我们可以把整个样本类的值复制到一个新的实例中。这就像一个完整的副本。
 case class Book(id:Int,title:String,isbn:Long)

  object RunExample extends App {

  val book1 = Book(100,"The Lord of the Rings : The fellowship of the ring",9780261103573l)

  val book_fullcopy = book1.copy()

  //Will copy all of the members
  println(book_fullcopy.id)
  println(book_fullcopy.title)
  println(book_fullcopy.isbn)

  } 
  • 使用自定义值
    程序员可以选择可以直接复制哪些元素,可以定制哪些值。
  case class Book(id:Int = 2000,title:String,isbn:Long)

  object RunExample extends App {

  val book1 = Book(100,"The Lord of the Rings : The fellowship of the ring",9780261103573l)

  val book_partialcopy = book1.copy(title = "The Lord of the Rings : The two towers")

  //Only the title is changed. Rest remains the same
  println(book_partialcopy.id)
  println(book_partialcopy.title)
  println(book_partialcopy.isbn)

  }

请注意,我们不能只复制一个值,而其余的不存在,即创建一个只有id变量的实例,因为这将改变对象本身的结构。

样本类反编译

理解toString,equals和hashCode是如何工作的一个好的方式是查看反编译的类

可以看到生成了toStringequalshashCode这些方法。还生成了一个默认的构造器。

还存在一些其他方法,例如针对样本类字段copy和的getter,这对于常规类也是常见的。

有两个主题与样本类关系非常密切,即apply方法和模式匹配。事实上,样本类的名字来自于几个模式的匹配情况,但我不会在这里解释它们,因为它们都是复杂的主题,我将在后面的教程中进行讨论。

另一个我还没讨论的主题是样本类继承。样本类的继承有点棘手,由于有自动生成的equals和hashcode。一旦涵盖了特质,我将解释他们,因为特质提供了一个实现继承更好的方式。

敬请关注!^_^

dogstar

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

广州