原想簡單籠統介紹一下scala,后感覺這么做意思不大,網友看了和沒看一樣,還是應該稍微詳細具體一點,故而把系列編號由(上)(中)(下),改為(上)(2)(3)(4)(5)....,(上)就是(1)吧,以下內容部分節選于我們即將出版的書稿:
本次推文介紹scala里的類用法:
在Scala里,類是用關鍵字“class”來定義,一旦定義完成,就可以通過“new 類名”的方式來構造一個對象。而這個對象的類型,就是這個類。換句話說,一個類就是一個類型,不同的類就是不同的類型。在后續會講到類的繼承關系,以及超類、子類和子類型多態的概念。
在類里可以定義val或var類型的變量,它們被稱為“字段”;還可以定義“def”函數,它們被稱為“方法”;字段和方法統稱“成員”。字段通常用于保存對象的狀態或數據,而方法則用于承擔對象的計算任務。字段也叫“實例變量”,因為每個被構造出來的對象都有其自己的字段。在運行時,操作系統會為每個對象分配一定的內存空間,用于保存對象的字段。方法則不同,對所有對象來說,方法都是一樣的程序段,因此不需要為某個對象單獨保存其方法。而且,方法的代碼只有在被調用時才會被執行,如果一個對象在生命周期內都沒有調用某些方法,那么完全沒必要浪費內存為某個對象去保存這些無用的代碼。
外部想要訪問對象的成員時,可以使用句點符號“ . ”,通過“對象.成員”的形式來訪問。此外,用new構造出來的對象可以賦給變量,讓變量名成為該對象的一個指代名稱。需要注意的是,val類型的變量只能與初始化時的對象綁定,不能再被賦予新的對象。一旦對象與變量綁定了,便可以通過“變量名.成員”的方式來多次訪問對象的成員。例如:
? ? scala> class Students {
?????????????| ???var name = "None"
?????????????| ???def register(n: String) = name = n
?????????????| ?}
????defined class Students
????scala> val stu = new Students
?stu: Students = Students@1a2e563e
????scala> stu.name
????res0: String = None
????scala> stu.register("Bob")
????scala> stu.name
????res2: String = Bob
????scala> stu = new Students
????:13: error: reassignment to val
???????????stu = new Students
Scala的類成員默認都是公有的,即可以通過“對象.成員”的方式來訪問對象的成員,而且沒有“public”這個關鍵字。如果不想某個成員被外部訪問,則可以在前面加上關鍵字“private”來修飾,這樣該成員只能被類內部的其他成員或上篇提到的伴生對象來訪問,外部只能通過其他公有成員來間接訪問。例如:
????scala> class Students {
?????????????| ???private var name = "None"
?????????????| ???def register(n: String) = name = n
?????????????| ???def display() = println(name)
?????????????| ?}
????defined class Students
????scala> val stu = new Students
????stu: Students = Students@75063bd0
????scala> stu.register("Bob")
????scala> stu.name
????:13: error: variable name in class Students cannot be accessed in Students
???????????stu.name
???? ? ? ? ? ?^
?? ? scala> stu.display
????Bob
類的構造方法
???1、主構造方法
在C++、Java、Python等面向對象語言里,類通常需要定義一個額外的構造方法(構造函數)。這樣,要構造一個類的對象,除了需要關鍵字new,還需要調用構造方法。事實上,這個過程中有一些代碼是完全重復的。Scala則不需要顯式定義構造方法 ,而是把類內部的非字段、非方法的代碼都當作“主構造方法”。而且,類名后面可以定義若干個參數列表,用于接收參數,這些參數將在構造對象時用于初始化字段并傳遞給主構造方法使用。Scala的這種獨特語法減少了一些代碼量。例如:
????scala> class Students(n: String) {
??????????| ???val name = n
??????????| ???println("A student named " + n + " has been registered.")
??????????| ?}
????defined class Students
????scala> val stu = new Students("Tom")
????A student named Tom has been registered.
????stu: Students = Students@5464eb28
在這個例子中,Students類接收一個String參數n,并用n來初始化字段name。這樣做,就無需像之前那樣把name定義成var類型,而是使用函數式風格的val類型,而且不再需要一個register方法在構造對象時來更新name的數據。
函數println既不是字段,也不是方法定義,"= n"也不是字段,也不是方法定義,所以都被當成是主構造函數的一部分。在構造對象時,主構造函數被執行,因此在解釋器里會打印了相關信息。
???2、輔助構造方法
除了主構造方法,scala還可以定義若干個輔助構造方法。輔助構造方法都是以“def this(..)”來開頭的,而且第一步行為必須是調用該類的另一個構造方法,即第一條語句必須是“this(...)”——要么是主構造方法,要么是之前的另一個輔助構造方法。這種規則的結果就是任何構造方法最終都會調用該類的主構造方法(而主構造方法其實就是類定義本身),使得主構造方法成為類的單一入口。例如:
????scala> class Students(n: String) {
????????|val name = n
????????|def this() = this("None")?//輔助構造方法是無參數的,其內部調用主構造方法
????????|println("A student named " + n + " has been registered.")
????????| ?}
????defined class Students
? ? scala> val stu = new Students
????A student named None has been registered.
????stu: Students = Students@64105ca4
在這個例子中,定義了一個輔助構造方法,該方法是無參的,其行為也僅是給主構造方法傳遞一個字符串“None”。
定義此輔助構造方法的作用:當創建該類的實例對象時,如果缺省了參數,這樣與主構造方法的參數列表是不匹配的,但是與輔助構造方法匹配,則系統會使用stu的輔助構造方法構造該實例對象。
Scala只允許主構造方法調用超類的構造方法,不允許輔助構造函數調用超類的構造方法(后續具體講解)。這種限制源于Scala為了代碼簡潔性與簡單性做出的折衷處理。
???3、無析構函數
因為Scala沒有指針,同時使用了Java的垃圾回收器,所以不需要像C++那樣定義析構函數。
???4、私有主構造方法
如果在類名與類的參數列表之間加上關鍵字“private”,那么主構造方法就是私有的,只能被內部定義的輔助構造函數訪問,外部代碼實列化對象時就不能通過主構造方法進行,而必須使用其他公有的輔助構造方法或工廠方法(專門用于構造對象的方法)。例如:
????scala> class Students private (n: String, m: Int) {
?????????????| ???val name = n
?????????????| ???val score = m
?????????????| ???def this(n: String) = this(n, 100)
?????????????| ???println(n + "'s score is " + m)
?????????????| ?}
????defined class Students
????scala> val stu = new Students("Bill", 90)?//與私有的主構造方法匹配,訪問錯誤
????:12: error: too many arguments (2) for constructor Students: (n: String)Students
???????????val stu = new Students("Bill", 90)
????scala> val stu = new Students("Bill") //與輔助構造方法匹配
????Bill's score is 100
????stu: Students = Students@7509b8e7
方法重載
如果在類里定義了多個同名的方法,但是每個方法的參數(主要是參數類型)不一樣,那么就稱這個方法有多個不同的版本,即方法重載,它是面向對象里多態屬性的一種表現。這些方法雖然同名,但是它們是不同的,因為函數真正的特征標識是它的參數,而不是函數名或返回類型。注意重載與前面的重寫的區別,重載是一個類里有多個不同版本的同名方法,重寫是子類覆蓋已定義在超類(即父類)的某個方法。
類參數
從前面的例子可以發現,多數時候類的列表參數僅僅是直接賦給某個字段。Scala為了進一步簡化代碼,允許在類參數前加上val或var來修飾,這樣就會在類的內部生成一個與參數同名的公有字段(此字段也是類的成員)。構造對象時,這些參數會直接復制給同名字段。除此之外,還可以加上關鍵字private、protected或override來表明字段的權限(關于權限修飾見后續章節)。如果參數沒有任何val或var關鍵字,那它就僅僅是“參數”,不是類的成員,只能用來初始化字段或給方法使用,外部不能訪問這樣的參數,內部也不能修改它,因為它不是類的成員。例如:
scala> class ab(a:Int){ //a不是類的成員,僅僅是個傳遞參數
?????| val b=a+1
?????| }
defined class ab
scala> val c=new ab(8)
c: ab = ab@6e2d3f2
scala> c.b
res0: Int = 9
scala> c.a
?????????^
???????error: value a is not a member of ab
以下在類參數定義時,加上val,則類參數同時變成了類成員:
scala> class ab(val a:Int){
?????| val b=a+1
?????| }
defined class ab
scala> val c=new ab(8)
c: ab = ab@ccd000e
scala> c.b
res2: Int = 9
scala> c.a
res3: Int = 8
單例對象與伴生對象
在Scala里,除了用new可以構造一個對象,也可以用“object”開頭定義一個對象。它類似于類的定義,只不過不能像類那樣有參數,也沒有構造方法。因此,不能用new來實例化一個object的定義,因為它已經是一個對象了,對比class用new可以實例化無數個對象,而object只能定義一個對象,因此也把object定義的對象叫單列對象,如果對于單列對象object A 存在伴生類class A,則又把object A叫Class A的伴生對象,它們可以相互訪問私有成員。概念雖多但是意思很明確
工廠對象與工廠方法
如果定義一個方法專門用來構造某一個類的對象,那么這種方法就稱為“工廠方法”。包含這些工廠方法集合的單例對象,也就叫“工廠對象” 。通常,工廠方法會定義在伴生對象里。尤其是當一系列類存在繼承關系時,可以在基類的伴生對象里定義一系列對應的工廠方法。使用工廠方法的好處是可以不用直接使用new來實例化對象,改用方法調用,而且方法名可以是任意的,這樣對外隱藏了類的實現細節。例如:
? ? class Students(val name: String, var score: Int) {
??????def exam(s: Int) = score = s
??????override def toString = name + "'s score is " + score + "."
??????//?重寫超類方法toString,注意關鍵字override?
????}
????object Students {
???def registerStu(name: String, score: Int) = new Students(name, score)
????}
將文件students.scala編譯后,并在解釋器里用“import Students._”導入單例對象后,就能這樣使用:
????scala> import Students._
????import Students._
????scala> val stu = registerStu("Tim", 100)
????stu: Students = Tim's score is 100.
apply方法
類和伴生對象有一個特殊的方法名:apply,如果定義了這個方法,那么既可以顯式調用:“對象.apply(參數)” ,也可以隱式調用:“對象(參數)”。隱式調用時,編譯器會自動插入缺失的“.apply”。如果apply是無參方法,應該寫出空括號(這和普通方法不同),否則無法隱式調用。無論是類還是單例對象,都能定義這樣的apply方法。
通常,在伴生對象里定義名為apply的工廠方法,就能通過“伴生對象名(參數)”來構造一個對象。也常常在類里定義一個與類相關的、具有特定行為的apply方法,讓使用者可以隱式調用,進而隱藏相應的實現細節。例如:
class Students(val name: String, var score: Int) {
??????def apply(s: Int) = score = s
??????def display() = println("Current score is " + score + ".")
??????override def toString = name + "'s score is " + score + "."
????}
????object Students?{
??????def apply(name: String, score: Int) = new Students(name, score)
????}
? ? scala> val stu?= Students("laozhang", 59)?
? ??stu: Students?= laozhang's score is 59.
?//注意沒有使用new?Students("laozhang",?59),這是傳統構造方法
????scala> stu(60)
?? ?scala> stu.display
????Current score is 60.
其中“Students("laozhang", 59)”被翻譯成“Students.apply("laozhang", 59)”,也就是調用了伴生對象里的工廠方法,所以構造了一個Students的對象并賦給變量stu。“stu(60)”被翻譯成“stu.apply(60)” ,也就是更新了字段score的數據。
主函數
主函數是Scala程序唯一的入口,即程序是從主函數開始運行的。要提供這樣的入口,則必須在某個單例對象里定義一個名為“main”的函數,而且該函數只有一個參數,類型為字符串數組Array[String],函數的返回類型是Unit(即空類型,注意不是UInt類型哈)。任何符合條件的單例對象都能成為程序的入口。例如:
//在students.scala文件中定義
? ? class Students(val name: String, var score: Int) {
??????def apply(s: Int) = score = s
??????def display() = println("Current score is " + score + ".")
??????override def toString = name + "'s score is " + score + "."
????}
? ??object Students?{
??????def apply(name: String, score: Int) = new Students(name, score)
????}
//在?main.scala文件中定義
? ? object Start {
??????def main(args: Array[String]) = {
????????try {
??????????val score = args(1).toInt
??????????val s = Students(args(0), score)
??????????println(s.toString)
????????} catch {
? ? ? ? ? .....
????????} } }
使用命令“scalac students.scala main.scala”將兩個文件編譯后,就能用命令“scala Start 參數1 參數2”來運行程序。命令里的“Start”就是包含主函數main的單例對象的名字,后面可以輸入若干個用空格間隔的參數。這些參數被打包成字符串數組供主函數使用,也就是代碼里的args(0)、args(1)。
主函數的另外一種寫法是讓單例對象繼承“App”特質(特質的概念引入是因為scala不支持多重超類的繼承,為實現多重超類繼承的某些特性而引入了特質概念),這樣就只需在單例對象里編寫主函數的函數體,無需顯式定義main方法。例如:
? ? object Start?extends App {
??????try {
????????var sum = 0
????????for(arg
??????????sum += arg.toInt
????????}
????????println("sum = " + sum)
??????} catch {
? ? ? ? ......
??????}
????}
??將文件編譯后使用方法同前
關于class的知識,做為硬件設計語言chisel3/spinalHDL,這些已經夠用了。
照例結合spinalHDL給個小提示:
以下視頻簡單演示伯克利分校的教學用riscv32處理器,其使用基于scala宿主語言的chisel3硬件設計語言編寫:
更多信息請長期關注微信公眾號:RiscV與IC設計