溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么理解Scala的類語法和語義

發(fā)布時(shí)間:2021-11-20 16:40:05 來源:億速云 閱讀:131 作者:柒染 欄目:編程語言

怎么理解Scala的類語法和語義,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。

Scala 的函數(shù)編程特性非常引人注目,但這并非 Java 開發(fā)人員應(yīng)該對(duì)這門語言感興趣的惟一原因。實(shí)際上,Scala 融合了函數(shù)概念和面向?qū)ο蟾拍?。為了?Java 和 Scala 程序員感到得心應(yīng)手,可以了解一下 Scala 的對(duì)象特性,看看它們是如何在語言方面與 Java 對(duì)應(yīng)的。記住,其中的一些特性并不是直接對(duì)應(yīng),或者說,在某些情況下,“對(duì)應(yīng)” 更像是一種類比,而不是直接的對(duì)應(yīng)。不過,遇到重要區(qū)別時(shí),我會(huì)指出來。

Scala 和 Java 一樣使用類

我們不對(duì) Scala 支持的類特性作冗長(zhǎng)而抽象的討論,而是著眼于一個(gè)類的定義,這個(gè)類可用于為 Scala 平臺(tái)引入對(duì)有理數(shù)的支持:

清單 1. rational.scala

class Rational(n:Int, d:Int)  {    private def gcd(x:Int, y:Int): Int =    {      if (x==0) y      else if (x<0) gcd(-x, y)      else if (y<0) -gcd(x, -y)      else gcd(y%x, x)    }    private val g = gcd(n,d)        val numer:Int = n/g    val denom:Int = d/g        def +(that:Rational) =      new Rational(numer*that.denom + that.numer*denom, denom * that.denom)    def -(that:Rational) =      new Rational(numer * that.denom - that.numer * denom, denom * that.denom)    def *(that:Rational) =      new Rational(numer * that.numer, denom * that.denom)    def /(that:Rational) =      new Rational(numer * that.denom, denom * that.numer)     override def toString() =      "Rational: [" + numer + " / " + denom + "]" }

從詞匯上看,清單 1 的整體結(jié)構(gòu)與 Java 代碼類似,但是,這里顯然還有一些新的元素。在詳細(xì)討論這個(gè)定義之前,先看一段使用這個(gè)新 Rational 類的代碼:

清單 2. RunRational

class Rational(n:Int, d:Int)  {    // ... as before  }   object RunRational extends Application  {    val r1 = new Rational(1, 3)    val r2 = new Rational(2, 5)    val r3 = r1 - r2    val r4 = r1 + r2    Console.println("r1 = " + r1)    Console.println("r2 = " + r2)    Console.println("r3 = r1 - r2 = " + r3)    Console.println("r4 = r1 + r2 = " + r4)  }

清單 2 中的內(nèi)容平淡無奇:先創(chuàng)建兩個(gè)有理數(shù),然后再創(chuàng)建兩個(gè) Rational,作為前面兩個(gè)有理數(shù)的和與差,最后將這幾個(gè)數(shù)回傳到控制臺(tái)上(注意, Console.println() 來自 Scala 核心庫(kù),位于 scala.* 中,它被隱式地導(dǎo)入每個(gè) Scala 程序中,就像 Java 編程中的 java.lang 一樣)。

用多少種方法構(gòu)造類?

現(xiàn)在,回顧一下 Rational 類定義中的第一行:

清單 3. Scala 的默認(rèn)構(gòu)造函數(shù)

class Rational(n:Int, d:Int)  {    // ...

您也許會(huì)認(rèn)為清單 3 中使用了某種類似于泛型的語法,這其實(shí)是 Rational 類的默認(rèn)的、首選的構(gòu)造函數(shù):n 和 d 是構(gòu)造函數(shù)的參數(shù)。

Scala 優(yōu)先使用單個(gè)構(gòu)造函數(shù),這具有一定的意義 —— 大多數(shù)類只有一個(gè)構(gòu)造函數(shù),或者通過一個(gè)構(gòu)造函數(shù)將一組構(gòu)造函數(shù) “鏈接” 起來。如果需要,可以在一個(gè) Rational 上定義更多的構(gòu)造函數(shù),例如:

清單 4. 構(gòu)造函數(shù)鏈

class Rational(n:Int, d:Int)  {    def this(d:Int) = { this(0, d) }

注意,Scala 的構(gòu)造函數(shù)鏈通過調(diào)用首選構(gòu)造函數(shù)(Int,Int 版本)實(shí)現(xiàn) Java 構(gòu)造函數(shù)鏈的功能。

實(shí)現(xiàn)細(xì)節(jié)

在處理有理數(shù)時(shí),采取一點(diǎn)數(shù)值技巧將會(huì)有所幫助:也就是說,找到公分母,使某些操作變得更容易。如果要將 1/2 與 2/4 相加,那么 Rational 類應(yīng)該足夠聰明,能夠認(rèn)識(shí)到 2/4 和 1/2 是相等的,并在將這兩個(gè)數(shù)相加之前進(jìn)行相應(yīng)的轉(zhuǎn)換。

嵌套的私有 gcd() 函數(shù)和 Rational 類中的 g 值可以實(shí)現(xiàn)這樣的功能。在 Scala 中調(diào)用構(gòu)造函數(shù)時(shí),將對(duì)整個(gè)類進(jìn)行計(jì)算,這意味著將 g 初始化為 n 和 d 的最大公分母,然后用它依次設(shè)置 n 和 d。

回顧一下 清單 1 就會(huì)發(fā)現(xiàn),我創(chuàng)建了一個(gè)覆蓋的 toString 方法來返回 Rational 的值,在 RunRational 驅(qū)動(dòng)程序代碼中使用 toString 時(shí),這樣做非常有用。

然而,請(qǐng)注意 toString 的語法:定義前面的 override 關(guān)鍵字是必需的,這樣 Scala 才能確認(rèn)基類中存在相應(yīng)的定義。這有助于預(yù)防因意外的輸入錯(cuò)誤導(dǎo)致難于覺察的 bug(Java 5 中創(chuàng)建 @Override 注釋的動(dòng)機(jī)也在于此)。還應(yīng)注意,這里沒有指定返回類型 —— 從方法體的定義很容易看出 —— 返回值沒有用 return 關(guān)鍵字顯式地標(biāo)注,而在 Java 中則必須這樣做。相反,函數(shù)中的最后一個(gè)值將被隱式地當(dāng)作返回值(但是,如果您更喜歡 Java 語法,也可以使用 return 關(guān)鍵字)。

一些重要值

接下來分別是 numer 和 denom 的定義。這里涉及的語法可能讓 Java 程序員認(rèn)為 numer 和 denom 是公共的 Int 字段,它們分別被初始化為 n-over-g 和 d-over-g;但這種想法是不對(duì)的。

在形式上,Scala 調(diào)用無參數(shù)的 numer 和 denom 方法,這種方法用于創(chuàng)建快捷的語法以定義 accessor。Rational 類仍然有 3 個(gè)私有字段:n、d 和 g,但是,其中的 n 和 d 被默認(rèn)定義為私有訪問,而 g 則被顯式地定義為私有訪問,它們對(duì)于外部都是隱藏的。

此時(shí),Java 程序員可能會(huì)問:“n 和 d 各自的 ‘setter’ 在哪里?” Scala 中不存在這樣的 setter。Scala 的一個(gè)強(qiáng)大之處就在于,它鼓勵(lì)開發(fā)人員以默認(rèn)方式創(chuàng)建不可改變的對(duì)象。但是,也可使用語法創(chuàng)建修改 Rational 內(nèi)部結(jié)構(gòu)的方法,但是這樣做會(huì)破壞該類固有的線程安全性。因此,至少對(duì)于這個(gè)例子而言,我將保持 Rational 不變。

當(dāng)然還有一個(gè)問題,如何操縱 Rational 呢?與 java.lang.String 一樣,不能直接修改現(xiàn)有的 Rational 的值,所以惟一的辦法是根據(jù)現(xiàn)有類的值創(chuàng)建一個(gè)新的 Rational,或者從頭創(chuàng)建。這涉及到 4 個(gè)名稱比較古怪的方法:+、 -、* 和 /。

與其外表相反,這并非操作符重載。

操作符

記住,在 Scala 中一切都是對(duì)象。在上一篇 文章 中, 您看到了函數(shù)本身也是對(duì)象這一原則的應(yīng)用,這使 Scala 程序員可以將函數(shù)賦予變量,將函數(shù)作為對(duì)象參數(shù)傳遞等等。另一個(gè)同樣重要的原則是,一切都是函數(shù);也就是說,在此處,命名為 add 的函數(shù)與命名為 + 的函數(shù)沒有區(qū)別。在 Scala 中,所有操作符都是類的函數(shù)。只不過它們的名稱比較古怪罷了。

在 Rational 類中,為有理數(shù)定義了 4 種操作。它們是規(guī)范的數(shù)學(xué)操作:加、減、乘、除。每種操作以它的數(shù)學(xué)符號(hào)命名:+、-、 * 和 /。

但是請(qǐng)注意,這些操作符每次操作時(shí)都構(gòu)造一個(gè)新的 Rational 對(duì)象。同樣,這與 java.lang.String 非常相似,這是默認(rèn)的實(shí)現(xiàn),因?yàn)檫@樣可以產(chǎn)生線程安全的代碼(如果線程沒有修改共享狀態(tài) —— 默認(rèn)情況下,跨線程共享的對(duì)象的內(nèi)部狀態(tài)也屬于共享狀態(tài) —— 則不會(huì)影響對(duì)那個(gè)狀態(tài)的并發(fā)訪問)。

有什么變化?

一切都是函數(shù),這一規(guī)則產(chǎn)生兩個(gè)重要影響:

首先,您已經(jīng)看到,函數(shù)可以作為對(duì)象進(jìn)行操縱和存儲(chǔ)。這使函數(shù)具有強(qiáng)大的可重用性,本系列 第一篇文章 對(duì)此作了探討。

第二個(gè)影響是,Scala 語言設(shè)計(jì)者提供的操作符與 Scala 程序員認(rèn)為應(yīng)該 提供的操作符之間沒有特別的差異。例如,假設(shè)提供一個(gè) “求倒數(shù)” 操作符,這個(gè)操作符會(huì)將分子和分母調(diào)換,返回一個(gè)新的 Rational (即對(duì)于 Rational(2,5) 將返回 Rational(5,2))。如果您認(rèn)為 ~ 符號(hào)最適合表示這個(gè)概念,那么可以使用此符號(hào)作為名稱定義一個(gè)新方法,該方法將和 Java 代碼中任何其他操作符一樣,如清單 5 所示:

清單 5. 求倒數(shù)

val r6 = ~r1  Console.println(r6) // should print [3 / 1], since r1 = [1 / 3]

在 Scala 中定義這種一元 “操作符” 需要一點(diǎn)技巧,但這只是語法上的問題而已:

清單 6. 如何求倒數(shù)

class Rational(n:Int, d:Int)  {    // ... as before ...     def unary_~ : Rational =      new Rational(denom, numer)  }

當(dāng)然,需要注意的地方是,必須在名稱 ~ 之前加上前綴 “unary_”,告訴 Scala 編譯器它屬于一元操作符。因此,該語法將顛覆大多數(shù)對(duì)象語言中常見的傳統(tǒng) reference-then-method 語法。

這條規(guī)則與 “一切都是對(duì)象” 規(guī)則結(jié)合起來,可以實(shí)現(xiàn)功能強(qiáng)大(但很簡(jiǎn)單)的代碼:

清單 7. 求和

1 + 2 + 3 // same as 1.+(2.+(3))  r1 + r2 + r3 // same as r1.+(r2.+(r3))

當(dāng)然,對(duì)于簡(jiǎn)單的整數(shù)加法,Scala 編譯器也會(huì) “得到正確的結(jié)果”,它們?cè)谡Z法上是完全一樣的。這意味著您可以開發(fā)與 Scala 語言 “內(nèi)置” 的類型完全相同的類型。

Scala 編譯器甚至?xí)L試推斷具有某種預(yù)定含義的 “操作符” 的其他含義,例如 += 操作符。注意,雖然 Rational 類并沒有顯式地定義 +=,下面的代碼仍然會(huì)正常運(yùn)行:

清單 8. Scala 推斷

var r5 = new Rational(3,4)  r5 += r1  Console.println(r5)

打印結(jié)果時(shí),r5 的值為 [13 / 12],結(jié)果是正確的。

Scala 內(nèi)幕

記住,Scala 將被編譯為 Java 字節(jié)碼,這意味著它在 JVM 上運(yùn)行。如果您需要證據(jù),那么只需注意編譯器生成以 0xCAFEBABE 開頭的 .class 文件,就像 javac 一樣。另外請(qǐng)注意,如果啟動(dòng) JDK 自帶的 Java 字節(jié)碼反編譯器(javap),并將它指向生成的 Rational 類,將會(huì)出現(xiàn)什么情況,如清單 9 所示:

清單 9. 從 rational.scala 編譯的類

C:\Projects\scala-classes\code>javap -private -classpath classes Rational  Compiled from "rational.scala" public class Rational extends java.lang.Object implements scala.ScalaObject{      private int denom;      private int numer;      private int g;      public Rational(int, int);      public Rational unary_$tilde();      public java.lang.String toString();      public Rational $div(Rational);      public Rational $times(Rational);      public Rational $minus(Rational);      public Rational $plus(Rational);      public int denom();      public int numer();      private int g();      private int gcd(int, int);      public Rational(int);      public int $tag();  }    C:\Projects\scala-classes\code>

Scala 類中定義的 “操作符” 被轉(zhuǎn)換成傳統(tǒng) Java 編程中的方法調(diào)用,不過它們?nèi)允褂每瓷先ビ行┕殴值拿Q。類中定義了兩個(gè)構(gòu)造函數(shù):一個(gè)構(gòu)造函數(shù)帶有一個(gè) int 參數(shù),另一個(gè)帶有兩個(gè) int 參數(shù)。您可能會(huì)注意到,大寫的 Int 類型與 java.lang.Integer 有點(diǎn)相似,Scala 編譯器非常聰明,會(huì)在類定義中將它們轉(zhuǎn)換成常規(guī)的 Java 原語 int。

測(cè)試 Rational 類

一種著名的觀點(diǎn)認(rèn)為,優(yōu)秀的程序員編寫代碼,偉大的程序員編寫測(cè)試;到目前為止,我還沒有對(duì)我的 Scala 代碼嚴(yán)格地實(shí)踐這一規(guī)則,那么現(xiàn)在看看將這個(gè) Rational 類放入一個(gè)傳統(tǒng)的 JUnit 測(cè)試套件中會(huì)怎樣,如清單 10 所示:

清單 10. RationalTest.java

import org.junit.*;  import static org.junit.Assert.*;   public class RationalTest  {      @Test public void test2ArgRationalConstructor()      {          Rational r = new Rational(2, 5);           assertTrue(r.numer() == 2);          assertTrue(r.denom() == 5);      }            @Test public void test1ArgRationalConstructor()      {          Rational r = new Rational(5);           assertTrue(r.numer() == 0);          assertTrue(r.denom() == 1);              // 1 because of gcd() invocation during construction;              // 0-over-5 is the same as 0-over-1      }                @Test public void testAddRationals()      {          Rational r1 = new Rational(2, 5);          Rational r2 = new Rational(1, 3);           Rational r3 = (Rational) reflectInvoke(r1, "$plus", r2); //r1.$plus(r2);           assertTrue(r3.numer() == 11);          assertTrue(r3.denom() == 15);      }            // ... some details omitted  }

SUnit

現(xiàn)在已經(jīng)有一個(gè)基于 Scala 的單元測(cè)試套件,其名稱為 SUnit。如果將 SUnit 用于清單 10 中的測(cè)試,則不需要基于 Reflection 的方法?;?Scala 的單元測(cè)試代碼將針對(duì) Scala 類進(jìn)行編譯,所以編譯器可以構(gòu)成符號(hào)行。一些開發(fā)人員發(fā)現(xiàn),使用 Scala 編寫用于測(cè)試 POJO 的單元測(cè)試實(shí)際上更加有趣。

SUnit 是標(biāo)準(zhǔn) Scala 發(fā)行版的一部分,位于 scala.testing 包中(要了解更多關(guān)于 SUnit 的信息,請(qǐng)參閱 參考資料)。

除了確認(rèn) Rational 類運(yùn)行正常之外,上面的測(cè)試套件還證明可以從 Java 代碼中調(diào)用 Scala 代碼(盡管在操作符方面有點(diǎn)不匹配)。當(dāng)然,令人高興的是,您可以將 Java 類遷移至 Scala 類,同時(shí)不必更改支持這些類的測(cè)試,然后慢慢嘗試 Scala。

您惟一可能覺得古怪的地方是操作符調(diào)用,在本例中就是 Rational 類中的 + 方法?;仡櫼幌?javap 的輸出,Scala 顯然已經(jīng)將 + 函數(shù)轉(zhuǎn)換為 JVM 方法 $plus,但是 Java 語言規(guī)范并不允許標(biāo)識(shí)符中出現(xiàn) $ 字符(這正是它被用于嵌套和匿名嵌套類名稱中的原因)。

為了調(diào)用那些方法,需要用 Groovy 或 JRuby(或者其他對(duì) $ 字符沒有限制的語言)編寫測(cè)試,或者編寫 Reflection 代碼來調(diào)用它。我采用后一種方法,從 Scala 的角度看這不是那么有趣,但是如果您有興趣的話,可以看看本文的代碼中包含的結(jié)果(參見 下載)。

注意,只有當(dāng)函數(shù)名稱不是合法的 Java 標(biāo)識(shí)符時(shí)才需要用這類方法。

“更好的” Java

我學(xué)習(xí) C++ 的時(shí)候,Bjarne Stroustrup 建議,學(xué)習(xí) C++ 的一種方法是將它看作 “更好的 C 語言”(參見 參考資料)。在某些方面,如今的 Java 開發(fā)人員也可以將 Scala 看作是 “更好的 Java”,因?yàn)樗峁┝艘环N編寫傳統(tǒng) Java POJO 的更簡(jiǎn)潔的方式??紤]清單 11 中顯示的傳統(tǒng) Person POJO:

清單 11. JavaPerson.java(原始 POJO)

public class JavaPerson  {      public JavaPerson(String firstName, String lastName, int age)      {          this.firstName = firstName;          this.lastName = lastName;          this.age = age;      }            public String getFirstName()      {          return this.firstName;      }      public void setFirstName(String value)      {          this.firstName = value;      }            public String getLastName()      {          return this.lastName;      }      public void setLastName(String value)      {          this.lastName = value;      }            public int getAge()      {          return this.age;      }      public void setAge(int value)      {          this.age = value;      }            public String toString()      {          return "[Person: firstName" + firstName + " lastName:" + lastName +              " age:" + age + " ]";      }            private String firstName;      private String lastName;      private int age;  }

現(xiàn)在考慮用 Scala 編寫的對(duì)等物:

清單 12. person.scala(線程安全的 POJO)

class Person(firstName:String, lastName:String, age:Int)  {      def getFirstName = firstName      def getLastName = lastName      def getAge = age       override def toString =          "[Person firstName:" + firstName + " lastName:" + lastName +              " age:" + age + " ]" }

這不是一個(gè)完全匹配的替換,因?yàn)樵嫉?Person 包含一些可變的 setter。但是,由于原始的 Person 沒有與這些可變 setter 相關(guān)的同步代碼,所以 Scala 版本使用起來更安全。而且,如果目標(biāo)是減少 Person 中的代碼行數(shù),那么可以刪除整個(gè) getFoo 屬性方法,因?yàn)?Scala 將為每個(gè)構(gòu)造函數(shù)參數(shù)生成 accessor 方法 —— firstName() 返回一個(gè) String,lastName() 返回一個(gè) String,age() 返回一個(gè) int。

即使必須包含這些可變的 setter 方法,Scala 版本仍然更加簡(jiǎn)單,如清單 13 所示:

清單 13. person.scala(完整的 POJO)

class Person(var firstName:String, var lastName:String, var age:Int)  {      def getFirstName = firstName      def getLastName = lastName      def getAge = age            def setFirstName(value:String):Unit = firstName = value      def setLastName(value:String) = lastName = value      def setAge(value:Int) = age = value       override def toString =          "[Person firstName:" + firstName + " lastName:" + lastName +              " age:" + age + " ]" }

注意,構(gòu)造函數(shù)參數(shù)引入了 var 關(guān)鍵字。簡(jiǎn)單來說, var 告訴編譯器這個(gè)值是可變的。因此,Scala 同時(shí)生成 accessor( String firstName(void))和 mutator(void firstName_$eq(String))方法。然后,就可以方便地創(chuàng)建 setFoo 屬性 mutator 方法,它在幕后使用生成的 mutator 方法。

Scala 將函數(shù)概念與簡(jiǎn)潔性相融合,同時(shí)又未失去對(duì)象的豐富特性。從本系列中您可能已經(jīng)看到,Scala 還修正了 Java 語言中的一些語法問題(后見之明)。

看完上述內(nèi)容,你們掌握怎么理解Scala的類語法和語義的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI