Scala简明教程
Scala簡明教程
目錄
? ?? ?變量聲明
? ?? ?函數(shù)
? ?? ?包package
? ?? ?數(shù)據(jù)結構
? ?? ?字符串
? ?? ?控制流程
? ?? ?模式匹配
? ?? ?面向對象
? ?? ?泛型
? ?? ?注解
? ?? ?Implicit
? ?? ?空對象Nil,Null,null,Unit,Nothing,None
? ?? ?Scala是一門多范式(multi-paradigm)的編程語言。
? ?? ?Scala源代碼被編譯成Java字節(jié)碼,所以它可以運行于JVM之上,并可以調用現(xiàn)有的Java類庫。
洛桑聯(lián)邦理工學院的Martin Odersky于2001年基于Funnel的工作開始設計Scala。Funnel是把函數(shù)式編程思想和佩特里網相結合的一種編程語言。Odersky先前的工作是Generic Java和javac。Java平臺的Scala于2003年底/2004年初發(fā)布。該語言第二個版本,v2.0,發(fā)布于2006年3月。
Scala是面向對象的,比Java更徹底
一切皆為對象, 數(shù)值,函數(shù)都是對象
全部支持函數(shù)式編程
包括函數(shù)即是對象,lambda,currying, type inference, immutability, lazy evaluation, and pattern matching
強大的靜態(tài)類型系統(tǒng)
algebraic data types, covariance and contravariance, higher-order types, anonymous types, generic classes, upper and lower type bounds, inner classes and abstract types as object members, compound types, explicitly typed self references , views and polymorphic methods
其它Java不支持的功能:
operator overloading, optional parameters, named parameters, raw strings, and no checked exceptions
2009年4月,Twitter宣布他們已經把大部分后端程序從Ruby遷移到Scala,其余部分也打算要遷移。這里有一篇文章解釋Twitter為什么使用Scala編程語言。
Engineer-to-Engineer Series Programming Languages Programming Languages Scala Talks
Coursera把Scala作為服務器語言使用。Why we love Scala at Coursera
一些Scala學習資料:
Scala documentation
Learning Scala
Effective Scala
Scala School
Scala cheatsheets
大數(shù)據(jù)生態(tài)圈中的Kafka和Spark都是由Scala開發(fā)的,這也是我為什么學習Scala的原因之一。
作為一個十多年Java程序員,同時在學習Scala和go這兩種語言。 學習的過程中感覺go語言太好學了, 入手很快, 而Scala的語法和類C語言如Java,C#等很不一樣, 很多語法的技巧在里面。 基于此,我特地整理了這篇文章。 簡單扼要的介紹Scala語言的知識點,尤其是和Java不太一樣的地方。
$變量聲明
var x = 5
var x:Double = 5
val y = 7 復制代碼
var聲明變量, val聲明常量, 類型可省略, Scala可以推斷出數(shù)據(jù)類型
函數(shù)
def f(x: Int) = { x*x }
def f(x: Any): Unit = println(x) 復制代碼
定義函數(shù), 返回值類型可省略, =等號后面可以是塊定義或者表達式。
reply()
reply 復制代碼
無參數(shù)的函數(shù)調用時可以省略括號。
names foreach (n => println(n))
names mkString ","
optStr getOrElse "<empty>"
復制代碼
一個參數(shù)時可以使用infix寫法
infix操作符可以定義如下:
? ?class MyBool(x: Boolean) {
def and(that: MyBool): MyBool = if (x) that else this
def or(that: MyBool): MyBool = if (x) this else that
def negate: MyBool = new MyBool(!x)
def not(x: MyBool) = x negate; // semicolon required here
def xor(x: MyBool, y: MyBool) = (x or y) and not(x and y)
}
//更多例子
5.+(3); 5 + 3
(1 to 5) map (_*2)
def f(x: R)
def f(x: => R)
復制代碼
第一個call-by-value ,第二個call-by-name(lazy parameters)
(x:R) => x*x 復制代碼
匿名函數(shù)(lambda表達式)
=>可以由 ? 字符替代(\u21D2),同樣 <-和 ->也可以由單個的字符取代: ← 和 →
(1 to 5).map(_*2)
(1 to 5).reduceLeft( _+_ ) 復制代碼
下劃線代表替代, 更多的下劃線功能參看 討論
(1 to 5).map(2*)
等價
(1 to 5).map(2* _)
(1 to 5).map { val x=_*2; println(x); x }
(1 to 5) filter {_%2 == 0} map {_*2} 復制代碼
匿名函數(shù)的塊風格實現(xiàn),最后一個語句作為返回值
def compose(g:R=>R, h:R=>R) = (x:R) => g(h(x))
val f = compose({_*2}, {_-1}) 復制代碼
多個塊作為參數(shù)
val zscore = (mean:R, sd:R) => (x:R) => (x-mean)/sd??//currying, obvious syntax.
def zscore(mean:R, sd:R) = (x:R) => (x-mean)/sd??//currying, obvious syntax
def zscore(mean:R, sd:R)(x:R) = (x-mean)/sd? ?? ? //currying, 語法糖,也叫參數(shù)分組. 但是必須按照下面的語法調用:
val normer = zscore(7, 0.4)_? ?? ?? ?//需要尾部的下劃線,僅限于上面一行的語法糖
def sum(args: Int*) = args.reduceLeft(_+_) 復制代碼
可變參數(shù)
$ 包package
import scala.collection._??//通配符導入,類似java中的.*
import scala.collection.Vector
import scala.collection.{Vector, Sequence}??//導入多個
import scala.collection.{Vector => Vec28}??//別名.
import java.util.{Date => _, _}??//除了Date,其它都導入
package pkg at start of file
package pkg { ... } 復制代碼
一般語句后面可以省略 ;
& 數(shù)據(jù)結構
(1,2,3)
var (x,y,z) = (1,2,3) 復制代碼
tuple類型
var xs = List(1,2,3)
xs(2) 復制代碼
List類型
1 :: List(2,3)
List(1, 2) ::: List(2, 3)
List(1, 2) ++ Set(3, 4, 3) 復制代碼
一些特殊的操作符
1 to 5 same as 1 until 6
1 to 10 by 2 復制代碼
range
$ 字符串
val name = "James"
println(s"Hello, $name") // Hello, James
println(s"1 + 1 = ${1 + 1}") 復制代碼
s前綴, 替換字符串中的變量或表達式
val height = 1.9d
val name = "James"
println(f"$name%s is $height%2.2f meters tall") // James is 1.90 meters tall 復制代碼
f前綴, printf風格的格式化
scala> raw"a\nb"
res1: String = a\nb 復制代碼
raw前綴, 原始字符,不轉義
scala> "a".r
res1: scala.util.matching.Regex = a 復制代碼
r后綴, 正則表達式
implicit class JsonHelper(private val sc: StringContext) extends AnyVal {
??def json(args: Any*): JSONObject = ...
}
val x: JSONObject = json"{ a: $a }" 復制代碼
自定義的字符串攔截器
$ 控制流程
if (check) happy else sad
if (check) happy? ?//下面一樣
if (check) happy else () 復制代碼
>> if語句
while (x < 5) { println(x); x += 1}
do { println(x); x += 1} while (x < 5) 復制代碼
while語句
import scala.util.control.Breaks._
breakable {
??for (x <- xs) {
? ? if (Math.random < 0.1) break
??}
}
for (x <- xs if x%2 == 0) yield x*10 //與下面的等價
xs.filter(_%2 == 0).map(_*10)
for ((x,y) <- xs zip ys) yield x*y //與下面的等價
(xs zip ys) map { case (x,y) => x*y }
for (x <- xs; y <- ys) yield x*y??//與下面的等價
xs flatMap {x => ys map {y => x*y}}
for (x <- xs; y <- ys) { //雙層嵌套,類似for {for {}}
println("%d/%d = %.1f".format(x,y, x*y))
}
for (i <- 1 to 5) {
println(i)
}
for (i <- 1 until 5) {
println(i)
} 復制代碼
$ 模式匹配
(xs zip ys) map { case (x,y) => x*y }
val v42 = 42
Some(3) match {
case Some(`v42`) => println("42")
case _ => println("Not 42")
}
val UppercaseVal = 42
Some(3) match {
case Some(UppercaseVal) => println("42")
case _ => println("Not 42")
} 復制代碼
case class自動生成equals和toString,參數(shù)相同則==返回true
$ 面向對象
? ? 沒有訪問修飾符的class或者類成員默認都是public類型的。
class C(x: R) //等價于
class C(private val x: R)
var c = new C(4) 復制代碼
>> 參數(shù)是private類型
class C(val x: R)
var c = new C(4)
c.x 復制代碼
>> 參數(shù)是public類型
class C(var x: R) {
assert(x > 0, "positive please") //constructor is class body,所以你可以在這里寫一些語句
var y = x??//public成員
val readonly = 5 //只能讀取,無法set
private var secret = 1 //private成員
def this = this(42) //其它構造函數(shù)
}
new{ ... }
abstract class D { ... }
class C extends D { ... }??//繼承
class D(var x: R)
class C(x: R) extends D(x)??//繼承和構造函數(shù)參數(shù). (wishlist: automatically pass-up params by default)
object O extends D { ... }??//object定義單例
trait T { ... } //traits.
class C extends T { ... } //實現(xiàn)接口. no constructor params. mixin-able.
class C extends D with T { ... }
trait T1; trait T2 //多個traits
class C extends T1 with T2
class C extends D with T1 with T2
class C extends D { override def f = ...} //必須聲明override
new java.io.File("f")??//產生類對象
List(1,2,3)??
classOf[String] //類字面值
x.isInstanceOf[String] //運行時檢查
x.asInstanceOf[String] //運行時cast
x: String //編譯時指明
final class A{
??final val x = 1
??var y = 2
}
sealed class B 復制代碼
final和sealed
? ?? ?對于內部類, inst1.InnerClass1 和inst2.InnerClass1是不同的類型,這和Java不一樣。 如果想使用相同的類型,使用Class#InnerClass1
$ 泛型
def mapmake[T](g:T=>T)(seq: List[T]) = seq.map(g) 復制代碼
>> 方法帶類型參數(shù)
class Stack[T] {
var elems: List[T] = Nil
def push(x: T) { elems = x :: elems }
def top: T = elems.head
def pop() { elems = elems.tail }
} 復制代碼
>> 類帶類型參數(shù)
<p>class A
class B extends A
def test[T <: A](t: T) = {}
test(new A)
test(new B) //error
Upper Type Bounds
class A
class B extends A
class C extends B
def test[T <: A](t: T) = {}
test[A](new A)
test[C](new C) //error</p> 復制代碼
>>Lower Type Bounds
class Test[+T](x: T) 復制代碼
>> covariant 針對類
class A
class B extends A
class C extends B
class Test[T](x: T)
val c = new Test(new C)
val t:Test[B] = c //Note: C <: B, but class Test is invariant in type T. You may wish to define T as +T instead. (SLS 4.5)
val a = new Test(new A)
val t:Test[B] = a //Note: A >: B, but class Test is invariant in type. You may wish to define T as -T instead. (SLS 4.5) 復制代碼
>>invariant
class Test[-T](x: T) 復制代碼
>>contravariant 針對類
總結:
1) 協(xié)變
[+T], covariant (or “flexible”) in its type parameter T,類似Java中的(? extends T), 即可以用T和T的子類來替換T,里氏替換原則。
2) 不變
不支持T的子類或者父類,只知支持T本身。
3) 逆變
[-T], contravariant, 類似(? supers T) 只能用T的父類來替換T。是逆里氏替換原則。
4) 上界
只允許T的超類U來替換T。 [U >: T]
5) 下界
只允許T的子類U來替代T。 [U <: T]
注解
@interface SourceURL {
public String value();
public String mail() default "";
} 復制代碼
使用
@interface Source {
public String URL();
public String mail();
}
@Source(URL = "http://coders.com/",
mail = "support@coders.com")
class MyScalaClass ... 復制代碼
簡寫(對于屬性名為value的特殊屬性)
@interface SourceURL {
? ?public String value();
? ?public String mail() default "";
? ?}
? ?@SourceURL("http://coders.com/")
class MyScalaClass ..
@SourceURL("http://coders.com/",
mail = "support@coders.com")
class MyScalaClass . 復制代碼
Implicit
implicit parameters 隱式參數(shù)
如果參數(shù)定義為implicit,那么在調用的如果沒設置, 那么參數(shù)會自動提供。
隱式參數(shù)與缺省參數(shù)是完全不一樣的。缺省參數(shù)是函數(shù)定義方設定了一個缺省值,在調用者沒有指明時將使用該缺省值。 隱式參數(shù)則不同,最終是會由調用方指定參數(shù)值,只是不一定在調用的語句里指定而已。編譯器在發(fā)現(xiàn)缺少隱式參數(shù)時,會在程序范圍內尋找符合類型的隱式值,如果找不到則編譯會失敗。
abstract class Logger {def log(s: String)}
class FileLogger extends Logger {
??def log(s: String) {println("Log in file: " + s)}
}
class StdoutLogger extends Logger {
??def log(s: String) {println("Stdout: " + s)}
}
def Add(a: Int, b: Int)(implicit logger: Logger) {
??val sum = a + b
??logger.log("%d + %d = %d".format(a, b, sum ))
}
implicit val log = new FileLogger
Add(1,2)
Add(2,3)(new StdoutLogger) //you may do it explicitly 復制代碼
如果上述代碼沒有implicit val log = new FileLogger這一句,在代碼范圍內也沒有其他的Logger類型的implicit值,編譯器會報錯.
反之,如果能找到Logger類型的隱式值,編譯器會將該隱式值作為參數(shù)傳遞過去。
implicit class 隱式類
A new language construct is proposed to simplify the creation of classes which provide extension methods to another type.
implicit class RichInt(n: Int) extends Ordered[Int] {
def min(m: Int): Int = if (n <= m) n else m
...
} 復制代碼
被轉換為
class RichInt(n: Int) extends Ordered[Int] {
def min(m: Int): Int = if (n <= m) n else m
...
}
implicit final def RichInt(n: Int): RichInt = new RichInt(n)
復制代碼
>> implicit method 隱式轉換
有時候,你并不需要指定一個類型是等/子/超于另一個類,你可以通過轉換這個類來偽裝這種關聯(lián)關系。一個視界指定一個類型可以被“看作是”另一個類型。這對對象的只讀操作是很有用的。
隱函數(shù)允許類型自動轉換。更確切地說,在隱式函數(shù)可以幫助滿足類型推斷時,它們允許按需的函數(shù)應用。例如:
implicit def strToInt(x: String) = x.toInt
val y: Int = "123" 復制代碼
>> view
view,就像類型邊界,要求對給定的類型存在這樣一個函數(shù)。您可以使用<%指定類型限制,例如:
class Container[A <% Int] { def addIt(x: A) = 123 + x } 復制代碼
這是說 A 必須“可被視”為 Int 。
方法可以通過隱含參數(shù)執(zhí)行更復雜的類型限制。例如,List支持對數(shù)字內容執(zhí)行sum,但對其他內容卻不行。可是Scala的數(shù)字類型并不都共享一個超類,所以我們不能使用T <: Number。相反,要使之能工作,Scala的math庫對適當?shù)念愋蚑 定義了一個隱含的Numeric[T]。 然后在List定義中使用它:
sum[B >: A](implicit num: Numeric[B]): B 復制代碼
如果你調用List(1,2).sum(),你并不需要傳入一個 num 參數(shù);它是隱式設置的。但如果你調用List("whoop").sum(),它會抱怨無法設置num。
在沒有設定陌生的對象為Numeric的時候,方法可能會要求某種特定類型的“證據(jù)”。這時可以使用以下類型-關系運算符:
| | |
|---|---|
|A =:= B | A 必須和 B相等|
|A <:< B | A 必須是 B的子類|
|A <%< B | A 必須可以被看做是 B|
class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value } 復制代碼
$ 空對象Nil,Null,null,Unit,Nothing,None
1) Nothing 是trait,定義為:final trait Nothing extends Any。Nothing處于Scala類型體系的最底層,是所有類型的子類型,Nothing沒有實例。
2) Null 是trait,定義為:final trait Null extends AnyRef 。Null是所有引用類型的子類型,唯一的一個實例是null。
3) null是Null的實例,類似Java中的null
4) Nil 是case object,定義為case object Nil extends List[Nothing], 代表一個空list,長度為0。由于Scala中的List是協(xié)變的,因此無論T是何種類型,Nil都是List[T]的實例。
5) None 是case object,定義為:case object None extends Option[Nothing],代表不存在的值。Option有兩個實例。None和Some
目錄
? ?? ?變量聲明
? ?? ?函數(shù)
? ?? ?包package
? ?? ?數(shù)據(jù)結構
? ?? ?字符串
? ?? ?控制流程
? ?? ?模式匹配
? ?? ?面向對象
? ?? ?泛型
? ?? ?注解
? ?? ?Implicit
? ?? ?空對象Nil,Null,null,Unit,Nothing,None
? ?? ?Scala是一門多范式(multi-paradigm)的編程語言。
? ?? ?Scala源代碼被編譯成Java字節(jié)碼,所以它可以運行于JVM之上,并可以調用現(xiàn)有的Java類庫。
洛桑聯(lián)邦理工學院的Martin Odersky于2001年基于Funnel的工作開始設計Scala。Funnel是把函數(shù)式編程思想和佩特里網相結合的一種編程語言。Odersky先前的工作是Generic Java和javac。Java平臺的Scala于2003年底/2004年初發(fā)布。該語言第二個版本,v2.0,發(fā)布于2006年3月。
Scala是面向對象的,比Java更徹底
一切皆為對象, 數(shù)值,函數(shù)都是對象
全部支持函數(shù)式編程
包括函數(shù)即是對象,lambda,currying, type inference, immutability, lazy evaluation, and pattern matching
強大的靜態(tài)類型系統(tǒng)
algebraic data types, covariance and contravariance, higher-order types, anonymous types, generic classes, upper and lower type bounds, inner classes and abstract types as object members, compound types, explicitly typed self references , views and polymorphic methods
其它Java不支持的功能:
operator overloading, optional parameters, named parameters, raw strings, and no checked exceptions
2009年4月,Twitter宣布他們已經把大部分后端程序從Ruby遷移到Scala,其余部分也打算要遷移。這里有一篇文章解釋Twitter為什么使用Scala編程語言。
Engineer-to-Engineer Series Programming Languages Programming Languages Scala Talks
Coursera把Scala作為服務器語言使用。Why we love Scala at Coursera
一些Scala學習資料:
Scala documentation
Learning Scala
Effective Scala
Scala School
Scala cheatsheets
大數(shù)據(jù)生態(tài)圈中的Kafka和Spark都是由Scala開發(fā)的,這也是我為什么學習Scala的原因之一。
作為一個十多年Java程序員,同時在學習Scala和go這兩種語言。 學習的過程中感覺go語言太好學了, 入手很快, 而Scala的語法和類C語言如Java,C#等很不一樣, 很多語法的技巧在里面。 基于此,我特地整理了這篇文章。 簡單扼要的介紹Scala語言的知識點,尤其是和Java不太一樣的地方。
$變量聲明
var聲明變量, val聲明常量, 類型可省略, Scala可以推斷出數(shù)據(jù)類型
函數(shù)
定義函數(shù), 返回值類型可省略, =等號后面可以是塊定義或者表達式。
無參數(shù)的函數(shù)調用時可以省略括號。
一個參數(shù)時可以使用infix寫法
infix操作符可以定義如下:
第一個call-by-value ,第二個call-by-name(lazy parameters)
匿名函數(shù)(lambda表達式)
=>可以由 ? 字符替代(\u21D2),同樣 <-和 ->也可以由單個的字符取代: ← 和 →
下劃線代表替代, 更多的下劃線功能參看 討論
匿名函數(shù)的塊風格實現(xiàn),最后一個語句作為返回值
多個塊作為參數(shù)
可變參數(shù)
$ 包package
一般語句后面可以省略 ;
& 數(shù)據(jù)結構
tuple類型
List類型
一些特殊的操作符
range
$ 字符串
s前綴, 替換字符串中的變量或表達式
f前綴, printf風格的格式化
raw前綴, 原始字符,不轉義
r后綴, 正則表達式
自定義的字符串攔截器
$ 控制流程
>> if語句
while語句
$ 模式匹配
case class自動生成equals和toString,參數(shù)相同則==返回true
$ 面向對象
? ? 沒有訪問修飾符的class或者類成員默認都是public類型的。
>> 參數(shù)是private類型
>> 參數(shù)是public類型
final和sealed
? ?? ?對于內部類, inst1.InnerClass1 和inst2.InnerClass1是不同的類型,這和Java不一樣。 如果想使用相同的類型,使用Class#InnerClass1
$ 泛型
>> 方法帶類型參數(shù)
>> 類帶類型參數(shù)
>>invariant
>>contravariant 針對類
總結:
1) 協(xié)變
[+T], covariant (or “flexible”) in its type parameter T,類似Java中的(? extends T), 即可以用T和T的子類來替換T,里氏替換原則。
2) 不變
不支持T的子類或者父類,只知支持T本身。
3) 逆變
[-T], contravariant, 類似(? supers T) 只能用T的父類來替換T。是逆里氏替換原則。
4) 上界
只允許T的超類U來替換T。 [U >: T]
5) 下界
只允許T的子類U來替代T。 [U <: T]
注解
使用
簡寫(對于屬性名為value的特殊屬性)
Implicit
implicit parameters 隱式參數(shù)
如果參數(shù)定義為implicit,那么在調用的如果沒設置, 那么參數(shù)會自動提供。
隱式參數(shù)與缺省參數(shù)是完全不一樣的。缺省參數(shù)是函數(shù)定義方設定了一個缺省值,在調用者沒有指明時將使用該缺省值。 隱式參數(shù)則不同,最終是會由調用方指定參數(shù)值,只是不一定在調用的語句里指定而已。編譯器在發(fā)現(xiàn)缺少隱式參數(shù)時,會在程序范圍內尋找符合類型的隱式值,如果找不到則編譯會失敗。
如果上述代碼沒有implicit val log = new FileLogger這一句,在代碼范圍內也沒有其他的Logger類型的implicit值,編譯器會報錯.
反之,如果能找到Logger類型的隱式值,編譯器會將該隱式值作為參數(shù)傳遞過去。
implicit class 隱式類
A new language construct is proposed to simplify the creation of classes which provide extension methods to another type.
被轉換為
>> implicit method 隱式轉換
有時候,你并不需要指定一個類型是等/子/超于另一個類,你可以通過轉換這個類來偽裝這種關聯(lián)關系。一個視界指定一個類型可以被“看作是”另一個類型。這對對象的只讀操作是很有用的。
隱函數(shù)允許類型自動轉換。更確切地說,在隱式函數(shù)可以幫助滿足類型推斷時,它們允許按需的函數(shù)應用。例如:
>> view
view,就像類型邊界,要求對給定的類型存在這樣一個函數(shù)。您可以使用<%指定類型限制,例如:
這是說 A 必須“可被視”為 Int 。
方法可以通過隱含參數(shù)執(zhí)行更復雜的類型限制。例如,List支持對數(shù)字內容執(zhí)行sum,但對其他內容卻不行。可是Scala的數(shù)字類型并不都共享一個超類,所以我們不能使用T <: Number。相反,要使之能工作,Scala的math庫對適當?shù)念愋蚑 定義了一個隱含的Numeric[T]。 然后在List定義中使用它:
如果你調用List(1,2).sum(),你并不需要傳入一個 num 參數(shù);它是隱式設置的。但如果你調用List("whoop").sum(),它會抱怨無法設置num。
在沒有設定陌生的對象為Numeric的時候,方法可能會要求某種特定類型的“證據(jù)”。這時可以使用以下類型-關系運算符:
| | |
|---|---|
|A =:= B | A 必須和 B相等|
|A <:< B | A 必須是 B的子類|
|A <%< B | A 必須可以被看做是 B|
$ 空對象Nil,Null,null,Unit,Nothing,None
1) Nothing 是trait,定義為:final trait Nothing extends Any。Nothing處于Scala類型體系的最底層,是所有類型的子類型,Nothing沒有實例。
2) Null 是trait,定義為:final trait Null extends AnyRef 。Null是所有引用類型的子類型,唯一的一個實例是null。
3) null是Null的實例,類似Java中的null
4) Nil 是case object,定義為case object Nil extends List[Nothing], 代表一個空list,長度為0。由于Scala中的List是協(xié)變的,因此無論T是何種類型,Nil都是List[T]的實例。
5) None 是case object,定義為:case object None extends Option[Nothing],代表不存在的值。Option有兩個實例。None和Some
6) Unit 是class,定義為:abstract final class Unit extends AnyVal。Unit跟Java中的void相當,當一個方法不返回任何值的時候,那么方法的類型是Unit。Unit唯一的一個實例是().
from: http://www.aboutyun.com/thread-12224-1-1.html
總結
- 上一篇: 利用github-pages建立个人博客
- 下一篇: Scala基础教程(一):简介、环境安装