【Scala学习】之Scala中的类

SCALA CLASSES

原文地址:https://docs.scala-lang.org/overviews/scala-book/classes.html

为了支持面向对象编程(OOP),Scala 提供了一个类结构。 语法比 Java 和 C# 等语言简洁得多,但仍然易于使用和阅读。

Basic class constructor

这是一个 Scala 类,它的构造函数定义了两个参数,firstName 和 lastName:

class Person(var firstName: String, var lastName: String)

根据该定义,您可以像这样创建新的 Person 实例:

val p = new Person("Bill", "Panner")

在类构造函数中定义参数会自动在类中创建字段,在此示例中,您可以像这样访问 firstName 和 lastName 字段:

println(p.firstName + " " + p.lastName)
Bill Panner

在这个例子中,因为两个字段都被定义为 var 字段,所以它们也是可变的,这意味着它们可以被更改。 这是你改变它们的方式:

scala> p.firstName = "William"
p.firstName: String = William

scala> p.lastName = "Bernheim"
p.lastName: String = Bernheim

如果你是从 Java 来到 Scala 的,这个 Scala 代码:

class Person(var firstName: String, var lastName: String)

大致相当于此 Java 代码:

public class Person {

    private String firstName;
    private String lastName;
    
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    public String getFirstName() {
        return this.firstName;
    }
    
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    public String getLastName() {
        return this.lastName;
    }
    
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
}

val makes fields read-only

在第一个示例中,两个字段都被定义为 var 字段:

class Person(var firstName: String, var lastName: String)

这使得这些字段可变。 您还可以将它们定义为 val 字段,这使它们不可变:

class Person(val firstName: String, val lastName: String)
             ---                    ---

如果您现在尝试更改 Person 实例的名字或姓氏,您将看到一个错误:

scala> p.firstName = "Fred"
<console>:12: error: reassignment to val
       p.firstName = "Fred"
                   ^

scala> p.lastName = "Jones"
<console>:12: error: reassignment to val
       p.lastName = "Jones"
                  ^

提示:如果您使用 Scala 编写 OOP 代码,请将您的字段创建为 var 字段,以便您可以改变它们。 当您使用 Scala 编写 FP 代码时,您通常会使用案例类而不是这样的类。 (稍后会详细介绍。)

Class constructors

在 Scala 中,类的主要构造函数是以下各项的组合:

  • 构造函数参数
  • 在类的主体中调用的方法
  • 在类的主体中执行的语句和表达式

在 Scala 类的主体中声明的字段以类似于 Java 的方式处理; 它们在类第一次实例化时被分配。

这个 Person 类演示了您可以在类的主体内执行的一些操作:

class Person(var firstName: String, var lastName: String) {

    println("the constructor begins")

    // 'public' access by default
    var age = 0

    // some class fields
    private val HOME = System.getProperty("user.home")

    // some methods
    override def toString(): String = s"$firstName $lastName is $age years old"

    def printHome(): Unit = println(s"HOME = $HOME")    
    def printFullName(): Unit = println(this) 

    printHome()
    printFullName()
    println("you've reached the end of the constructor")

}

Scala REPL 中的这段代码演示了这个类的工作原理:

scala> val p = new Person("Kim", "Carnes")
the constructor begins
HOME = /Users/al
Kim Carnes is 0 years old
you've reached the end of the constructor
p: Person = Kim Carnes is 0 years old

scala> p.age
res0: Int = 0

scala> p.age = 36
p.age: Int = 36

scala> p
res1: Person = Kim Carnes is 36 years old

scala> p.printHome
HOME = /Users/al

scala> p.printFullName
Kim Carnes is 36 years old

当你从一种更冗长的语言来到 Scala 时,这种构造器方法起初可能会感觉有点不寻常,但是一旦你理解并用它编写了几个类,你就会发现它是合乎逻辑的和方便的。

Other Scala class examples

在我们继续之前,这里有一些 Scala 类的其他示例:

class Pizza (var crustSize: Int, var crustType: String)

// a stock, like AAPL or GOOG
class Stock(var symbol: String, var price: BigDecimal)

// a network socket
class Socket(val timeout: Int, val linger: Int) {
    override def toString = s"timeout: $timeout, linger: $linger"
}

class Address (
    var street1: String,
    var street2: String,
    var city: String, 
    var state: String
)

AUXILIARY CLASS CONSTRUCTORS

您可以通过定义名为 this 的方法来定义辅助 Scala 类构造函数。 只需要知道几条规则:

  • 每个辅助构造函数必须有不同的签名(不同的参数列表)
  • 每个构造函数都必须调用先前定义的构造函数之一

下面是一个 Pizza 类的例子,它定义了多个构造函数:

val DefaultCrustSize = 12
val DefaultCrustType = "THIN"

// the primary constructor
class Pizza (var crustSize: Int, var crustType: String) {

    // one-arg auxiliary constructor
    def this(crustSize: Int) = {
        this(crustSize, DefaultCrustType)
    }

    // one-arg auxiliary constructor
    def this(crustType: String) = {
        this(DefaultCrustSize, crustType)
    }

    // zero-arg auxiliary constructor
    def this() = {
        this(DefaultCrustSize, DefaultCrustType)
    }

    override def toString = s"A $crustSize inch pizza with a $crustType crust"

}

定义所有这些构造函数后,您可以通过几种不同的方式创建pizza实例:

val p1 = new Pizza(DefaultCrustSize, DefaultCrustType)
val p2 = new Pizza(DefaultCrustSize)
val p3 = new Pizza(DefaultCrustType)
val p4 = new Pizza

Notes

关于这个例子有两个重要的注意事项:

  • DefaultCrustSizeDefaultCrustType 变量不是处理这种情况的首选方法,但是因为我们还没有展示如何处理枚举,所以我们使用这种方法来保持简单。
  • 辅助类构造函数是一个很棒的特性,但是因为你可以为构造函数参数使用默认值,所以你不需要经常使用这个特性。 下一课演示如何使用像这样的默认参数值通常会使辅助构造函数变得不必要:
class Pizza(
    var crustSize: Int = DefaultCrustSize, 
    var crustType: String = DefaultCrustType
)

SUPPLYING DEFAULT VALUES FOR CONSTRUCTOR PARAMETERS

Scala 允许您为构造函数参数提供默认值。 例如,在之前的课程中,我们展示了您可以像这样定义一个 Socket 类:

class Socket(var timeout: Int, var linger: Int) {
    override def toString = s"timeout: $timeout, linger: $linger"
}

这很好,但是您可以通过为 timeout 和linger参数提供默认值来使这个类更好:

class Socket(var timeout: Int = 2000, var linger: Int = 3000) {
    override def toString = s"timeout: $timeout, linger: $linger"
}

通过为参数提供默认值,您现在可以通过多种不同的方式创建新的 Socket:

new Socket()
new Socket(1000)
new Socket(4000, 6000)

下面是这些示例在 REPL 中的样子:

scala> new Socket()
res0: Socket = timeout: 2000, linger: 3000

scala> new Socket(1000)
res1: Socket = timeout: 1000, linger: 3000

scala> new Socket(4000, 6000)
res2: Socket = timeout: 4000, linger: 6000

Benefits

提供默认构造函数参数至少有两个好处:

  • 您为参数提供首选的默认值
  • 您让您的类的使用者根据自己的需要覆盖这些值

如示例所示,第三个好处是它允许消费者以至少三种不同的方式构造新的 Socket 实例,就好像它具有三个类构造函数一样。

Bonus: Named parameters

Scala 的另一个好处是您可以在创建类的新实例时使用命名参数。 例如,下面这个类:

class Socket(var timeout: Int, var linger: Int) {
    override def toString = s"timeout: $timeout, linger: $linger"
}

您可以像这样创建一个新的 Socket:

val s = new Socket(timeout=2000, linger=3000)

这个特性有时会派上用场,例如当所有类构造函数参数都具有相同类型时,例如本示例中的 Int 参数。 比如有人发现这段代码:

val s = new Socket(timeout=2000, linger=3000)

比这段代码更具可读性:

val s = new Socket(2000, 3000)

A FIRST LOOK AT SCALA METHODS

原文地址 :https://docs.scala-lang.org/overviews/scala-book/methods-first-look.html

在 Scala 中,方法在类中定义(就像 Java 一样),但出于测试目的,您也可以在 REPL 中创建它们。 本课将展示一些方法示例,以便您了解语法的外观。

Defining a method that takes one input parameter

这就是你如何定义一个名为 double 的方法,它接受一个名为 a 的整数输入参数并返回该整数的两倍值:

def double(a: Int) = a * 2

在该示例中,方法名称和签名显示在 = 符号的左侧:

def double(a: Int) = a * 2
    --------------

def 是你用来定义方法的关键字,方法名是double,输入参数a的类型是Int,这是Scala的整数数据类型。

函数的主体显示在右侧,在此示例中,它只是将输入参数 a的值加倍:

def double(a: Int) = a * 2
                     -----

将该方法粘贴到 REPL 后,您可以通过给它一个 Int 值来调用它(调用它):

scala> double(2)
res0: Int = 4

scala> double(10)
res1: Int = 20

Showing the method’s return type

前面的示例没有显示该方法的返回类型,但您可以显示它:

def double(a: Int): Int = a * 2
                  -----

编写这样的方法显式声明了方法的返回类型。 有些人更喜欢显式声明方法返回类型,因为这样可以使代码在未来数周、数月和数年内更容易维护。

如果您将该方法粘贴到 REPL 中,您会看到它的工作方式与之前的方法一样。

Methods with multiple input parameters

为了展示更复杂的东西,这里有一个接受两个输入参数的方法:

def add(a: Int, b: Int) = a + b

这是相同的方法,显式显示了方法的返回类型:

def add(a: Int, b: Int): Int = a + b

这是一个接受三个输入参数的方法:

def add(a: Int, b: Int, c: Int): Int = a + b + c

Multiline methods

当方法只有一行时,您可以使用所示格式,但是当方法主体变长时,您可以将多行放在花括号内:

def addThenDouble(a: Int, b: Int): Int = {
    val sum = a + b
    val doubled = sum * 2
    doubled
}

如果您将该代码粘贴到 REPL 中,您将看到它的工作方式与前面的示例一样:

scala> addThenDouble(1, 1)
res0: Int = 4

ENUMERATIONS (AND A COMPLETE PIZZA CLASS)

原文地址 :https://docs.scala-lang.org/overviews/scala-book/enumerations-pizza-class.html

如果我们接下来演示枚举,我们还可以向您展示以面向对象的方式编写的示例 Pizza 类的样子。 所以这就是我们要走的路。

枚举是创建小组常量的有用工具,例如一周中的几天、一年中的几个月、一副牌中的花色等,在您有一组相关的常量值的情况下。

因为我们提前了一点,所以我们不会过多地解释这个语法,但这就是你如何为一周中的几天创建一个枚举:

sealed trait DayOfWeek
case object Sunday extends DayOfWeek
case object Monday extends DayOfWeek
case object Tuesday extends DayOfWeek
case object Wednesday extends DayOfWeek
case object Thursday extends DayOfWeek
case object Friday extends DayOfWeek
case object Saturday extends DayOfWeek

如图所示,只需声明一个基本特征,然后根据需要使用尽可能多的 case 对象扩展该特征。

同样,这是为一副纸牌中的花色创建枚举的方式:

sealed trait Suit
case object Clubs extends Suit
case object Spades extends Suit
case object Diamonds extends Suit
case object Hearts extends Suit

我们将在本书后面讨论 trait 和 case 对象,但如果你现在相信我们这就是你创建枚举的方式,那么我们可以在 Scala 中创建一个 Pizza 类的小 OOP 版本。

Pizza-related enumerations

鉴于对枚举的(非常简短的)介绍,我们现在可以像这样创建与披萨相关的枚举:

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

这些枚举提供了一种处理比萨配料、外皮大小和外皮类型的好方法。

A sample Pizza class

鉴于这些枚举,我们可以像这样定义一个 Pizza 类:

class Pizza (
    var crustSize: CrustSize = MediumCrustSize, 
    var crustType: CrustType = RegularCrustType
) {

    // ArrayBuffer is a mutable sequence (list)
    val toppings = scala.collection.mutable.ArrayBuffer[Topping]()

    def addTopping(t: Topping): Unit = toppings += t
    def removeTopping(t: Topping): Unit = toppings -= t
    def removeAllToppings(): Unit = toppings.clear()

}

如果将所有代码(包括枚举)保存在名为 Pizza.scala 的文件中,您将看到可以使用通常的命令编译它:

$ scalac Pizza.scala

该代码将创建许多单独的文件,因此我们建议将其放在单独的目录中。

还没有什么可运行的,因为这个类没有 main 方法,但是……

A complete Pizza class with a main method

如果您准备好享受一些乐趣,请复制以下所有源代码并将其粘贴到名为 Pizza.scala 的文件中:

import scala.collection.mutable.ArrayBuffer

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

class Pizza (
    var crustSize: CrustSize = MediumCrustSize, 
    var crustType: CrustType = RegularCrustType
) {

    // ArrayBuffer is a mutable sequence (list)
    val toppings = ArrayBuffer[Topping]()

    def addTopping(t: Topping): Unit = toppings += t
    def removeTopping(t: Topping): Unit = toppings -= t
    def removeAllToppings(): Unit = toppings.clear()

    override def toString(): String = {
        s"""
        |Crust Size: $crustSize
        |Crust Type: $crustType
        |Toppings:   $toppings
        """.stripMargin
    }
}

// a little "driver" app
object PizzaTest extends App {
   val p = new Pizza
   p.addTopping(Cheese)
   p.addTopping(Pepperoni)
   println(p)
}

请注意如何将所有枚举、Pizza 类和 PizzaTest 对象放在同一个文件中。 这是一个非常方便的 Scala 特性。

接下来,使用通常的命令编译该代码:

$ scalac Pizza.scala

现在,使用以下命令运行 PizzaTest 对象:

$ scala PizzaTest

输出应如下所示:

$ scala PizzaTest

Crust Size: MediumCrustSize
Crust Type: RegularCrustType
Toppings:   ArrayBuffer(Cheese, Pepperoni)

这段代码结合了几个不同的概念——包括我们在 import 语句和 ArrayBuffer 中还没有讨论过的两件事——但是如果你有 Java 和其他语言的经验,希望一次扔给你不会太多。

此时,我们鼓励您根据需要使用该代码。 对代码进行更改,并尝试使用 removeToppingremoveAllToppings 方法以确保它们按您期望的方式工作。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值