【Scala学习】函数式编程续 case类

CASE CLASSES

https://docs.scala-lang.org/overviews/scala-book/case-classes.html

另一个为函数式编程提供支持的 Scala 特性是 case 类。案例类具有常规类的所有功能,等等。当编译器在类前面看到 case 关键字时,它会为您生成代码,具有以下好处:

  • 默认情况下,案例类构造函数参数是公共 val 字段,因此为每个参数生成访问器方法。
  • apply 方法是在类的伴生对象中创建的,因此您不需要使用 new 关键字来创建类的新实例。
  • 生成了一个 unapply 方法,它使您可以在匹配表达式中以更多方式使用案例类。
  • 在类中生成了一个复制方法。你可能不会在 Scala/OOP 代码中使用这个特性,但它在 Scala/FP 中一直在使用。
  • 生成了 equals 和 hashCode 方法,它们让您可以比较对象并轻松地将它们用作映射中的键。
  • 生成了默认的 toString 方法,有助于调试。

这些功能都在以下部分中进行了演示。

With apply you don’t need new

当你将一个类定义为一个 case 类时,你不必使用 new 关键字来创建一个新的实例:

scala> case class Person(name: String, relation: String)
defined class Person

// "new" not needed before Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)

正如在上一课中所讨论的,这是因为在 Person 的伴生对象中生成了一个名为“apply”的方法。

No mutator methods

默认情况下,案例类构造函数参数是 val 字段,因此为每个参数生成一个 accessor 方法:

scala> christina.name
res0: String = Christina

但是,不会生成 mutator 方法:

// can't mutate the `name` field
scala> christina.name = "Fred"
<console>:10: error: reassignment to val
       christina.name = "Fred"
                  ^

因为在 FP 中你永远不会改变数据结构,所以构造函数字段默认为 val 是有道理的。

An unapply method

在上一课关于伴随对象的课程中,您看到了如何编写 unapply 方法。 case 类的一个好处是它会自动为你的类生成一个 unapply 方法,所以你不必编写一个。

为了证明这一点,想象一下你有这个特征:

trait Person {
    def name: String
}

然后,创建这些案例类来扩展该特征:

case class Student(name: String, year: Int) extends Person
case class Teacher(name: String, specialty: String) extends Person

因为它们被定义为 case 类——并且它们具有内置的 unapply 方法——你可以编写这样的匹配表达式:

def getPrintableString(p: Person): String = p match {
    case Student(name, year) =>
        s"$name is a student in Year $year."
    case Teacher(name, whatTheyTeach) =>
        s"$name teaches $whatTheyTeach."
}

注意 case 语句中的这两种模式:

case Student(name, year) =>
case Teacher(name, whatTheyTeach) =>

这些模式之所以有效,是因为 StudentTeacher 被定义为具有类型签名符合特定标准的 unapply 方法的案例类。 从技术上讲,这些示例中显示的特定类型的模式匹配称为构造函数模式

Scala 标准是unapply方法返回包含在Option中的元组中的case 类构造函数字段。 解决方案的“元组”部分已在上一课中展示。

为了展示该代码是如何工作的,创建一个 StudentTeacher 的实例:

val s = Student("Al", 1)
val t = Teacher("Bob Donnan", "Mathematics")

接下来,当您使用这两个实例调用 getPrintableString 时,REPL 中的输出如下所示:

scala> getPrintableString(s)
res0: String = Al is a student in Year 1.

scala> getPrintableString(t)
res1: String = Bob Donnan teaches Mathematics.

关于unapply方法和提取器的所有内容对于这样的介绍性书籍来说有点高级,但是因为案例类是一个重要的FP主题,所以最好覆盖它们,而不是跳过它们。

copy method

case 类还有一个自动生成的 copy 方法,当你需要执行 a) 克隆对象和 b) 在克隆过程中更新一个或多个字段的过程时,它非常有用。 例如,这就是 REPL 中的流程:

scala> case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
defined class BaseballTeam

scala> val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
cubs1908: BaseballTeam = BaseballTeam(Chicago Cubs,1908)

scala> val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)

如图所示,当您使用 copy 方法时,您所要做的就是提供要在克隆过程中修改的字段的名称。

因为您从不改变 FP 中的数据结构,这就是您从现有实例创建类的新实例的方式。 此过程可称为“复制时更新”。

equals and hashCode methods

Case 类也有自动生成的 equalshashCode 方法,因此可以比较实例:

scala> case class Person(name: String, relation: String)
defined class Person

scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)

scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)

scala> christina == hannah
res1: Boolean = false

这些方法还让您可以轻松地在集合和地图等集合中使用您的对象。

toString methods

最后,case 类还有一个很好的默认 toString 方法实现,这至少在调试代码时很有帮助:

scala> christina
res0: Person = Person(Christina,niece)

The biggest advantage

虽然所有这些特性都对函数式编程有很大好处,但正如他们在书中所写,Scala 编程 (Odersky、Spoon 和 Venners),“案例类的最大优势是它们支持模式匹配。” 模式匹配是 FP 语言的一大特性,Scala 的 case 类提供了一种在匹配表达式等领域实现模式匹配的简单方法。

CASE OBJECTS

https://docs.scala-lang.org/overviews/scala-book/case-objects.html

在我们进入case对象之前,我们应该提供一些关于“常规”Scala对象的背景知识。 正如我们在本书前面提到的,当你想创建一个单例对象时,你可以使用 Scala 的“对象”。 正如文档所述,“与类的单个实例无关的方法和值属于单例对象,表示为 使用关键字object而不是class。”

一个常见的例子是当你创建一个“工具”对象时,比如这个:

object PizzaUtils {
    def addTopping(p: Pizza, t: Topping): Pizza = ...
    def removeTopping(p: Pizza, t: Topping): Pizza = ...
    def removeAllToppings(p: Pizza): Pizza = ...
}

Or this one:

object FileUtils {
    def readTextFileAsString(filename: String): Try[String] = ...
    def copyFile(srcFile: File, destFile: File): Try[Boolean] = ...
    def readFileToByteArray(file: File): Try[Array[Byte]] = ...
    def readFileToString(file: File): Try[String] = ...
    def readFileToString(file: File, encoding: String): Try[String] = ...
    def readLines(file: File, encoding: String): Try[List[String]] = ...
}

这是使用 Scala object 结构的常用方法。

Case objects

case object 就像一个 object,但就像一个 case 类比一个普通类具有更多的特征一样,一个 case 对象比一个普通对象具有更多的特征。 其特点包括:

  • 它是可序列化的
  • 它有一个默认的 hashCode 实现
  • 它有一个改进的toString 实现

由于这些特性,case 对象主要用于两个地方(而不是常规对象):

  • 创建枚举时
  • 为要在其他对象之间传递的“消息”创建容器时 (such as with the Akka actors library)

Creating enumerations with case objects

正如我们在本书前面所展示的,您可以像这样在 Scala 中创建枚举:

sealed trait Topping
case object Cheese extends Topping
case object Pepperoni extends Topping
case object Sausage extends Topping
case object Mushrooms extends Topping
case object Onions extends Topping

sealed trait CrustSize
case object SmallCrustSize extends CrustSize
case object MediumCrustSize extends CrustSize
case object LargeCrustSize extends CrustSize

sealed trait CrustType
case object RegularCrustType extends CrustType
case object ThinCrustType extends CrustType
case object ThickCrustType extends CrustType

然后在您的代码中稍后使用这些枚举:

case class Pizza (
    crustSize: CrustSize,
    crustType: CrustType,
    toppings: Seq[Topping]
)

Using case objects as messages

案例对象派上用场的另一个地方是当您想对“消息”的概念进行建模时。 例如,假设您正在编写一个类似于 Amazon 的 Alexa 的应用程序,并且您希望能够传递“说话”消息,例如“说出所附的文本”、“停止说话”、“暂停”和“继续” 。” 在 Scala 中,您可以为这些消息创建单例对象,如下所示:

case class StartSpeakingMessage(textToSpeak: String)
case object StopSpeakingMessage
case object PauseSpeakingMessage
case object ResumeSpeakingMessage

请注意,StartSpeakingMessage 被定义为一个 case class 而不是 case object。 这是因为 case 对象不能有任何构造函数参数。

鉴于这些消息,如果 Alexa 是使用 Akka 库编写的,您会在“speak”类中找到这样的代码:

class Speak extends Actor {
  def receive = {
    case StartSpeakingMessage(textToSpeak) =>
        // code to speak the text
    case StopSpeakingMessage =>
        // code to stop speaking
    case PauseSpeakingMessage =>
        // code to pause speaking
    case ResumeSpeakingMessage =>
        // code to resume speaking
  }
}

这是在 Scala 应用程序中传递消息的一种很好的、安全的方式。

FUNCTIONAL ERROR HANDLING IN SCALA

https://docs.scala-lang.org/overviews/scala-book/functional-error-handling.html

因为函数式编程就像代数,所以没有空值或异常。 但是当然,当您尝试访问已关闭的服务器或丢失的文件时,您仍然会遇到异常,那么您该怎么办? 本课演示了 Scala 中函数式错误处理的技术。

Option/Some/None

我们已经演示了在 Scala 中处理错误的一种技术:名为OptionSomeNone的三个类。 不是编写像 toInt 这样的方法来抛出异常或返回空值,而是声明该方法返回一个 Option,在这种情况下是一个 Option[Int]

def toInt(s: String): Option[Int] = {
    try {
        Some(Integer.parseInt(s.trim))
    } catch {
        case e: Exception => None
    }
}

稍后在您的代码中,您使用 matchfor 表达式处理来自 toInt 的结果:

toInt(x) match {
    case Some(i) => println(i)
    case None => println("That didn't work.")
}

val y = for {
    a <- toInt(stringA)
    b <- toInt(stringB)
    c <- toInt(stringC)
} yield a + b + c

这些方法在 “No Null Values”课程中讨论过,所以我们不会在这里重复讨论。

Try/Success/Failure

另一个名为 TrySuccessFailure 的类的工作方式与 OptionSomeNone 类似,但具有两个不错的特性:

  • Try 使得捕获异常变得非常简单
  • Failure 包含异常

这是为使用这些类而重新编写的 toInt 方法。 首先,将类导入当前范围:

import scala.util.{Try,Success,Failure}

之后,这就是 toIntTry 的样子:

def toInt(s: String): Try[Int] = Try {
    Integer.parseInt(s.trim)
}

如您所见,这比 Option/Some/None 方法要短得多,并且可以进一步缩短为:

def toInt(s: String): Try[Int] = Try(Integer.parseInt(s.trim))

这两种方法都比 Option/Some/None 方法短得多。

REPL 演示了这是如何工作的。 一、成功案例:

scala> val a = toInt("1")
a: scala.util.Try[Int] = Success(1)

其次,这是当 Integer.parseInt 抛出异常时的样子:

scala> val b = toInt("boo")
b: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "boo")

正如该输出所示,toInt 返回的 Failure 包含失败的原因,即异常。

有很多方法可以处理 Try 的结果——包括从失败中“recover”的能力——但常见的方法仍然涉及使用 matchfor 表达式:

toInt(x) match {
    case Success(i) => println(i)
    case Failure(s) => println(s"Failed. Reason: $s")
}

val y = for {
    a <- toInt(stringA)
    b <- toInt(stringB)
    c <- toInt(stringC)
} yield a + b + c

请注意,当使用 for 表达式并且一切正常时,它返回包含在 Success 中的值:

scala.util.Try[Int] = Success(6)

相反,如果失败,则返回一个 Failure

scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "a")

Even more …

还有其他类的工作方式类似,包括Scala库中的Either/Left/Right等第三方库,但常用Option/Some/None和Try/Success/Failure,先学好 .

你可以使用任何你喜欢的东西,但 Try/Success/Failure 通常用于处理可能抛出异常的代码——因为你几乎总是想了解异常——而 Option/Some/None 用于其他地方,例如 避免使用空值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值