`

scala笔记小结

 
阅读更多

来自 fair-jm.iteye.com 转截请注明出处

 

摘录的一些笔记 来源挺杂的

主要是coursera上 FP principle in Scala课程的笔记

 

内也许有诸多错误 欢迎指正

 

 

避免状态改变

 

函数是一等公民

 

求值策略

Call by name:先代入等使用时再求值

Call by value:先求值再代入

 

如果CBV能终止 那么CBN一定能终止 反过来不成立

例子:

def first(x:Int , y:Int) =x

def loop()=loop

first(1,loop)

那么CBN一定可以终止的 CBV不会

 

scala用的是CBV

scala也运行用CBN 在定义时用 =>

如下:

def constOne(x:Int,y: => Int) = 1

 

 

def --->  by-name 使用时求值

val --->  by-value 先求值

简单的例子:

def loop:Boolean=loop

用 def x=loop 直接返回

x:Boolean

 

而用

val x=loop

会因为无限循环而卡住

 

 

block:

{}

在block的变量只能在{}内可见

在block的变量会隐藏在{}外的同名变量

 

;可选 在同一行写多个表达式的时候要用

一个表达式写多行的话用()括起来

或者把运算符写在上一行的结尾

 

尾递归(tail recursion):

占用的栈空间是固定的 本次结果不依赖下一次

回溯过程调用

 

用@tailrec注解来说明函数是尾递归的(如果所给函数不是尾递归的 会报错)

 

 

构建高阶函数

参数里用

f:Int => Int 的形式 (参数是Int 返回Int)

匿名函数的写法:

(参数) => 函数体

{def f(x1:T1,...,xn:Tn)=E;f}

例子:

  def a= (b:Int)=>b                               //> a: => Int => Int

  a(2)    

啊哈这里就很简单啦 有名函数用= 匿名函数用 =>

类型的话 就是 (参数) => 返回类型

一个完整的例子:

  def f(fun:Int => Int,b:Int):Int= {
    def sum(acc:Int,e:Int):Int = {
      if(e==0) acc
      else sum(fun(e)+acc,e-1)
    }
    sum(0,b)
  }                                               //> f: (fun: Int => Int, b: Int)Int
  f((a:Int)=>a*a,2)    

 

  

  

 

curring

函数柯里化

 def add(x:Int) = (y:Int) => x+y                  //> add: (x: Int)Int => Int

 add(5)                                           //> res2: Int => Int = <function1>

 add(5)(1)                                        //> res3: Int = 6

和:

def add(x:Int)(y:Int) = x+y                      //> add: (x: Int)(y: Int)Int

 add(5)_                                          //> res2: Int => Int = <function1>

 add(5)(1)                                        //> res3: Int = 6

 

是等价的

也就是 def 函数名(参数1:类型1)....(参数n:类型n)=E 等同于 def 函数名(参数1:类型1) = (参数2:类型2) => ...(参数n:类型n) => E

 def add=(x:Int)=>(y:Int) =>(z:Int)=> x+y+z       //> add: => Int => (Int => (Int => Int))

 等同与

 def add(x:Int)(y:Int)(z:Int) = x+y+z            //> addx: (x: Int)(y: Int)(z: Int)Int

 

 

数据结构:

class Rational(x:Int,y:Int){
 def numer = x
 def denom = y 
}

 

 

方法默认是public的

要用私有的 加上private 就可以了

 

require(条件,"异常时显示")

不满足条件 抛出异常 字符串是错误时文字

 

scala中可以作为标识符的:

字母

符号

_

字母以符号结尾

例如:

 

x1 * +?%& vector_++ counter_=

 

运算符重载也是遵循原有的优先级:

从低到高如下:

所有的字母

|

^

&

<  >

= !

:

+ - 

* / %

所有的特殊字符

 

还有一点要注意的是...

*? *!的优先级等同于 *

 

一个完整的例子:

class Rational(x:Int,y:Int){
  require(y!=0,"denominator must be nonzero")
  
  def this(x:Int) = this(x,1)
  
  private def gcd(a:Int,b:Int):Int = if (b==0) a else gcd(b,a%b)
  private val g=gcd(x,y)
  def numer = x / g
  def denom = y / g
  
  def <(that:Rational) = numer*that.denom < that.numer*denom
  
  def max(that:Rational) = if(this < that) that else this
  
  def + (that :Rational) =
    new Rational(
    numer*that.denom+ that.numer*denom,
    denom*that.denom
    )
  
  def unary_- =
    new Rational(
    -numer,
    denom
  )
  
  def - (that:Rational)=
    this + -that
    
  def * (that:Rational) =
    new Rational(numer*that.numer,denom*that.denom)
  
  override def toString=numer+"/"+denom

}

 

 

请注意 对象之间的== 可以重载== 也可以重写equals方法(最好重写equals方法)

 

 

 

抽象类:

abstract class IntSet {
  def incl(x:Int):IntSet
  def contains(x:Int):Boolean
}

 

继承依旧使用extends

抽象的方法可以不写override 但不抽象的一定要写override

 

 

用Object定义的就是单例

object Empty extends IntSet {

  def incl(x:Int):IntSet = new NonEmpty(x,Empty,Empty)

  def contains(x:Int):Boolean=false

  override def toString="."

}

不能用new创建

 

动态绑定

 

import中

import week3.Rational

import week3.{Rational,Hello} //导入Rational和Hello

import week3._

 

默认导入的

scala

java.lang

scala.Predef中的单例

 

Int --> scala.Int

Boolean -->scala.Boolean

Object --> java.lang.Object

require --> scala.Predef.require

assert --> scala.Predef.assert

===========================================================================

 

trait

java和scala中都是单继承的

 

用trait的定义很类似abstract class

直接用trait代替abstract class就可以了

一个类可以实现多个traits 用 with

比interface更强 可以包括域和已实现的方法

trait不能有构造参数和类后面跟的type parameter(抽象类可以有)

 

 

===========================================================================

        Scala.Any

Scala.AnyVal     Scala.AnyRef

基本类型的基类    引用类型的基类

 

 

 

 

    Scala.Nothing   Scala.Null

 

top types:

Any 所有的基类

    方法:'==' '!=' equals hashCode toString

 

AnyRef 所有引用类型的基类:

       java.lang.Object的别名

  

AnyVal 所有基本类型的基类

 

 

底层:

Scala.Nothing是所有类的子类

作用:

 1.函数异常返回时 返回Nothing

 2.作为空集合的里的元素

 Set[Nothing]

 

Scala抛出异常:

throw Exc  

例子:def error(msg:String) = throw new Error(msg)    //> error: (msg: String)Nothing

返回值就是Nothing

 

 

 Null是所有引用类型的子类

 null是Null类型的 所以所有的引用类型都可以用null来赋值 记住基本类型中不成立

 

 

===========================================================================

 

 

 

多态:

Cons-Lists

 

Nil

Cons

 

泛型:

[T]

可用于类也可用于方法

def singleton[T](elem:T) = new Cons[T](elem,new Nil[T])

使用:

singleton[Int](1)

singleton[Boolean](true)

因为scala支持类型推断 所以 也可以写成singleton(1) 没有问题

 

scala也是会进行泛型擦除的(type erasure) 

 

多态:

subtyping 子类的实例就是基类的实例

generics 泛型

 

 

函数作为对象:

A=>B

和Scala.Functional[A,B]:

trait Function[A,B]{

  def apply(x:A):B

}

 

等价关系:

(x:Int) => x*x

 

def apply(x:Int):Int = x*x

 

eta-expansion

函数调用 例子:

List(1,2)=List.apply(1,2)

 

object Function{
 def apply[T](x:T):T=x
}
Function[Int](1)                                //> res0: Int = 1

 

 

===========================================================================

 

Type Bounds:

def assertAllPos[S<:IntSet](r:S) :S=

 

S<:T 表示 S是T的子类   封顶

S>:T表示  S是T的父类   封底

还有一种限制范围的

[S>:NonEmpty <:IntSet] 以NonEmpty为底 以IntSet为顶的

 

covariant

这个的意思是 如果List是convariant的

    S<:T (S是T的子类)成立

那么List[S]<:List[T]也成立

 

covariant带来的问题:

NonEmpty[] a=new NonEmpty[]{new NonEmpty(1,Empty,Empty)}

IntSet[] b = a

b[0]=Empty

NonEmpty s=a[0]

 

Empty不是NonEmpty 若执行肯定会错

 

这里NonEmpty是Inset的子类 java中Array是covariant的

所以 第二行成立 但Empty不是NonEmpty 

java中的解决方法是在创建Array的时候就放入一个表明最初创建类型的Tag

因为Empty不是NonEmpty类型的 所以 b[0]=Empty这句话就会报错(所以是个运行时的错误)

 

scala中的Array就不是corvariant的:

  var x=Array[Any]()

  var y=Array[Int](1,2)

  x=y //这里报错 type mismatch;  found   : Array[Int]  required: Array[Any] 

 

  

里氏替换原则:  

Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.

if S<:T then q(x:T)==true ==> q(y:S)==true

如果下A<:B 那么对B能进行的操作 对A也可以进行

或者说对于q(x) x是B类型的能工作 那么q(y) y是A类型的也能工作如果A<:B

 

也就是 父类型能做的 子类型也要能做

要达到这个 父类方法的参数类型要比子类的"窄"(父类的参数类型要是子类参数类型的子类) 但父类方法的返回值类型应该要比子类的"宽"

===========================================================================

 

scala:FP OOP

基本类型

 

Peano numbers

 

 

 

 

 

 

Variance 

Scala中List是covariant的 而Array不是

因为List是不可变的 所以不存在一些插入的问题(如上面的java的例子) 而Array不是

 

关系:

C[T] A B 其中A <: B

若 C[A]<:C[B]  C 是 covariant

若 C[A]>:C[B]  C 是 cotravariant

如果 C[A]和C[B]没有父子关系 那么C是nonvariant

 

Scala中是允许你自己定义是否是variant的

class C[+A]{...} C是covariant

class C[-A]{...} C是contrvariant

class C[A] {...} C是nonvariant(默认)

 

 if A2 <: A1 and B1<:B2 ,then

   A1 => B1 <: A2 => B2

              父类的参数更窄 返回更宽

 

 

variance check

 因此 Scala会给标识为variant的类做限制:

 covariant的类型只能作为返回值

 cotrvariant的类型只能作为参数值

 

 

 

===========================================================================

===========================================================================

scala                         java

x.isInstanceOf[T]            x instanceof T

x.asInstanceOf[T]           (T)x

 

 

 

eval 

 

 

 

Pattern Matching:

 

case class 

 

e match{

   case pattern => expr 

}

在没有匹配项时抛出 MatchError

 

Pattern可以是:

 

 小写字母开头表示变量(保留字除外) 用于绑定值到这个变量

 

 大写开头表示常量 两者是否相等来进行匹配(==)

 _

 构造器pattern C(p1,....,pn) 匹配所有C类型(或子类型)通过p1...p构造的

 (注意类的定义要用case class)

 同样的变量只能在Pattern出现一次

 

 顺序匹配下去

 

  例子:

 

  trait Expr {
  def isNumber: Boolean
  def isSum: Boolean
  def numValue: Int
  def leftOp: Expr
  def rightOp: Expr
  def show():String = this match{
      case Number(n) => n+""
      case Sum(e1,e2) => e1+"+"+e2
  }
}

 

 

另一个例子:

 

 def show(e: Expr): String = e match {
    case Sum(Prod(x1, x2), Prod(x3, x4)) =>
      if (x1 == x3) show(x1) + "*" + "(" + show(x2) + "+" + show(x4) + ")"
      else if (x1 == x4) show(x1) + "*" + "(" + show(x2) + "+" + show(x3) + ")"
      else if (x2 == x3) show(x2) + "*" + "(" + show(x1) + "+" + show(x4) + ")"
      else if (x2 == x4) show(x2) + "*" + "(" + show(x1) + "+" + show(x3) + ")"
      else show(_Prod(x1, x2)) + "+" + show(_Prod(x3, x4))
    case Sum(n1, n2) => show(n1) + "+" + show(n2)
    case Prod(Sum(n1, n2), n3) => "(" + show(_Sum(n1, n2)) + ")" + "*" + show(n3)
    case Prod(n3, Sum(n1, n2)) => show(n3) + "*(" + show(_Sum(n1, n2)) + ")"
    case Prod(n1, n2) => show(n1) + "*" + show(n2)
    case _ => e.toString()
  }         

 

 

 

  

  

  

  

Lists:

List("xxx","xxx")   List[String]

List()              List[Nothing]

List(1,2,3,4,5)    

 

:: 是 scala的cons操作符

car :: cdt 

"1"::("2"::("3"::Nil))            //> res1: List[String] = List(1, 2, 3)

scala中做了简化 所以 也可以写成"1"::"2"::"3"::Nil是等价的 (::其实是方法名 相当于调用方法 可以写成.::)

 

1 :: 2 :: 3 :: 4 :: Nil 和 Nil.::(4).::(3).::(2).::(1)

等价

List中一样有 head和tail方法

此外 两个List连接可以用:::

 List(1,2,3):::List(4,5,6)                      //> res6: List[Int] = List(1, 2, 3, 4, 5, 6)

 这个和List(4,5,6).:::(List(1,2,3))等价

 

 

List的模式:

Nil 表示空list

p :: ps 表示以p开头的元素 这个模式和[Head|Tail]一致

List(p1,...,p2) 也可以写成 p1::....::pn::Nil

当然也可以用_

例子:

   List(1,2,3) match {

      case List(1,_,3) => "yes"  //这样其实也是一种构造模式吧

   }                                              //> res2: String = yes

 

 

更多用于List的函数:

xs.length

xs.last   获得最后一个

xs.init   得到一个除了最后一个元素的列表

xs take n

xs drop n

xs(n)

 

scala> def xs= List(0,1,2,3,4)
xs: List[Int]

scala> xs.length
res0: Int = 5

scala> xs.last
res1: Int = 4

scala> xs.init
res2: List[Int] = List(0, 1, 2, 3)

scala> xs take 1
res3: List[Int] = List(0)

scala> xs drop 1
res4: List[Int] = List(1, 2, 3, 4)

scala> xs(0)
res5: Int = 0

 

 

 

 

创建新列表:

xs ++ ys        合并

xs.reverse      倒序

xs.updated(n,x)

 

查找元素:

xs indexOf x   不存在返回-1

xs contains x   

 

 

 

 

 

 

 

tuple:

 

pair: (x,y)

scala> val (label,value) = (1,"s")

label: Int = 1

value: String = s

 

scala> (1,"s")

res0: (Int, String) = (1,s)

超过两个元素的就是tuple了

 

(T1,....,Tn)是Scala.Tuplen[T1,...,Tn]的缩写

(e1,....,en)是Scala.Tuplen(e1,...,en)的缩写

tuple模式中也和上面的(e1,...,en)一样

 

 

 

 

implicit:

在函数的参数中使用 编译器会根据类型得到正确的隐含参数

The compiler will ?gure out the right implicit to pass based on the demanded type.

 

def msort[T](xs:List[T])(implicit ord:Ordering)

调用时只需:

msort(List(1,2,3)) 或者msort(List("1","2","3"))

Ordering在scala.math包下 提供了常用的比较操作等

 

如何查找的:

? is marked implicit

? has a type compatible with T

? is visible at the point of the function call, or is de?ned in a

companion object associated with T.

 

以上的原则 Ordering是包含类型参数的 完整的形式是 Ordering[T]

所以编译器在使用List(1,2,3)时会正确找到Ordering[Int] 而为List("1","2","3")时则为Ordering[String]

  def less[T](a:T,b:T)(implicit ord:math.Ordering[T]):Boolean=ord.lt(a, b) 

  less(1,2)   

 

  

  

List的高阶函数

map

filter

span

takeWhile

dropWhile

 

fold/reduce:

List(x1,...,xn) reduceLeft op = (...(x1 op x2) op...) op xn

((x,y) => x*y)可以写成(_*_)

 

reduceLeft不能处理Nil 一般用foldLeft代替使用

(List(x1,...,xn) foldLeft z)(op) = (..(z op x1) op...) op xn

 

def sum(xs:List[Int]) = (xs foldLeft 0)(_+_)

 

以及foldRight

注意foldLeft和foldRight并不一定能通用

原因在于

foldLeft中 累加器在参数左

foldRight中 累加器在参数右

累加器的类型是U List的类型是T

foldLeft是:

  U T

 U T

U T 

而foldRight则是

  T U

    T U

      U T

也就是一个操作符的问题 例如:: 左边是要T的 而右边是List[T] 注意一下缩写的op函数就可以了 

foldLeft中累加器是第一个参数 元素是第二个参数

foldRight中累加器是第二个参数 元素是第一个参数

 

 

 

 

其他的集合类:

都是不可变的

Vector 用Vector()c创建 +: :+  x+:xs xs:+x

默认32大小 如果不够用了 会增加一层 log32(N)

 

List和Vector的基类是Seq 所有sequences的基类

Seq是Iterable的子类

   Iterable

Seq  Set Map

 

String和Array也类似Seq(但不是子类) 他们一样可以用map fold 等方法

 

Ranges:

val r:Range = 1 until 5

val 3:Range = 1 to 5

1 to 10 by 3

6 to 1 by -2

Range中的域只有 上限 下限 步长

Range也可以用map fold等方法

 

for:

for (s) yield e //yield是将后面内容输出为一个List

s中可以写赋值(必须在第一项)和判断

如果有多个赋值 可以写成 {}的形式

 

 

mkString 把元素通过制定的字符连接成字符串 和groovy的join有点类似

 

 

 

 

 

for的转换:

for(x <- e1) yield e2 被转换成 e1 map(x=>e2)

等等

for的转换是基于 map flatMap withFilter的

 

ScalaQuery Slick

 

 

 

 

Map:

构建用Map(key -> value)

 

取出用get 

如果不存在返回None 不然返回Some(x) 用模式【匹配获取

List: sortWith sorted

     groupBy 返回的是Map

1 ->2 和 (1,2)是一样的:

 

scala> 1 -> 2
res5: (Int, Int) = (1,2)
scala> (1,2)
res6: (Int, Int) = (1,2)
scala> Map((1,2))
res7: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2)

 

 

默认值:

用withDefaultValue方法 后面跟一个defaultValue 构建一个新的Map

Scala用*表示不定参数函数 例如:

def this(bindings:(Int,Double)*)=???

 

对于合适的数据 可以用toMap toList进行相互的转换

 

Map也可以作为函数 参数是key 返回值是value

操作总结:

Map:

  • + map和元素之间
  • ++ map和map之间 如果有相同的key 则以第二个map为准
  • withDefaultValue 返回一个对于不存在key 有默认值的Map
  • 1 -> 2 和 (1,2)等价
  • 符合结构的Map和List可以互相转换

 

List:

  • 元素 :: List cons构造(往前追加一个元素)
  • List ++ List 合并
  • List ::: List 合并

 

 

 

Stream:

构造 可以用Stream.empty 用Stream.cons方法进行构造

 

可以用 Stream(el1,...,eln)进行构造

 

也可以用 其他Seq的toStream方法进行构造

 

scala> Stream(1,2)

res0: scala.collection.immutable.Stream[Int] = Stream(1, ?)

注意后面是个?  第二个或者后面的还没用到就先不求值 结构和List是类似的

 

Stream基本支持List的方法 但有一个例外 Stream不支持 head :: tail  不过有另外一个选择 就是使用 #:: 同样:Stream.cons(x,xs)和 x #:: xs 一样

 

 

 

Lazy Evaluation 惰性求值

和 by-name 求值(每次用到的时候再求值) 和 strict求值 (先求值再使用) 不同

惰性求值在使用时计算并且以后使用的话用已计算好的值

scala支持惰性求值 使用lazy val 定义就可以了

 

scala> def eval_():Int = {
     | println("evalution")
     | 1
     | }
eval_: ()Int

scala> def pp(i: =>Int) ={
     | println(i);println(i);
     | }
pp: (i: => Int)Unit

scala> pp(eval_()) // by-name求值
evalution
1
evalution
1

scala> lazy val x=eval_() //惰性求值
x: Int = <lazy>

scala> x
evalution
res11: Int = 1

scala> x
res12: Int = 1

scala> def y =eval_() //def也是by-name求值
y: Int

scala> y
evalution
res13: Int = 1

scala> y
evalution
res14: Int = 1

 

 

当然scala默认用的是strict求值

 

例子:

 

scala> def expr = {
     | val x = {print("x");1}
     | lazy val y = { print("y");2}
     | def z = {print("z");3}
     | z+y+x+z+y+x
     | }
expr: Int

scala> expr
xzyzres15: Int = 12

 

 

Stream的cons就是使用的惰性求值:

 

scala> val s = Stream.cons(1,Stream({println("x");2}))
s: Stream.Cons[Int] = Stream(1, ?)

scala> s(1)
x
res21: Int = 2

scala> s(1)
res22: Int = 2

 

 

 

 

用Stream可以构建无穷的数据(需要时取一部分就可以了)

 

===========================================================================

===========================================================================

一些语法:

循环:

while用法和java一样

for的语法是  

for(i <- 初始值 until 终止值){

}

输出0-9:

 

for(i <- 0 until 10){
      println(i)                                 
}

 

 

还可以用forEach

 

List(1,2,3,4,"string").foreach( i => println(i))

 

 

 

 

 

 

 

范围和元组

范围用

val ran= 初始值 until 终止值 

默认步长为1 修改步长用 () by 步长:

 val range=0 until 10 by 2                     //> range  : scala.collection.immutable.Range = Range(0, 2, 4, 6, 8)

  range.size                                      //> res1: Int = 5

  range.start                                     //> res2: Int = 0

  range.end                                       //> res3: Int = 10

  range.step                                      //> res4: Int = 2

  

  或者用 to(包含终点)

   val range=0 to 10 by 2                          //> range  : scala.collection.immutable.Range = Range(0, 2, 4, 6, 8, 10)

   

元祖依旧是用()

用._1取第一个元素以此类推 元祖可以用来进行多值赋值:

val (x,y)=(1,2)                                  //> x  : Int = 1

                                                  //| y  : Int = 2

 val tuple=(1,2)                                  //> tuple  : (Int, Int) = (1,2)

 tuple._1                                         //> res1: Int = 1

 

 一样可以用_做任意匹配

 

 

===========================================================================

===========================================================================

scala中方法和函数的区别

===========================================================================

方法的定义:

def m(x:Int):Int = x * 2

 

函数的定义:

def f= (x:Int) => x * 2

===========================================================================

有参数的方法不能写成 m 而函数则没有此限制:

 

scala> f
res0: Int => Int = <function1>

scala> m
<console>:9: error: missing arguments for method m;
follow this method with `_' if you want to treat it as a partially applied function
              m
              ^

 

 

 

===========================================================================

方法没有参数时可以不写() 而函数一定要写():

 

scala> def m = 2
m: Int

scala> def f = =>2
<console>:1: error: illegal start of simple expression
       def f = =>2
               ^

 

 

当然方法写了也可以:

 

scala> def m()= 2
m: ()Int

 

 

 

这样的情况下用 m 其实是调用了方法  而对于f 如果直接f则是返回函数本身

 

 

===========================================================================

Scala中含有 ETA expansion

 

所以可以将传递函数的地方直接传递方法 但是不能用于赋值:
scala> val myList = List(3, 56, 1, 4, 72)
myList: List[Int] = List(3, 56, 1, 4, 72)

scala>  myList.map((x)=>2*x)
res1: List[Int] = List(6, 112, 2, 8, 144)

scala> def m3(x: Int) = 3*x
m3: (x: Int)Int

scala> myList.map(m3)
res2: List[Int] = List(9, 168, 3, 12, 216)

 

 

===========================================================================

 

方法强转为函数 使用_就可以了:

 

scala>  def m4(x: Int) = 4*x
m4: (x: Int)Int

scala>  val f4 = m4 _   //注意这边有个空格 但如果是方法做柯里化有个()就不用多加一个空格(加了也没关系)
f4: Int => Int = <function1>

scala>  f4(2)
res3: Int = 8

scala>

 

 

 

===========================================================================

http://java.dzone.com/articles/revealing-scala-magician%E2%80%99s 写道
call-by-name的参数一定是方法而不是函数
//use "x" twice, meaning that the method is invoked twice.
scala> def m1(x: => Int) = List(x, x)
m1: (x: => Int)List[Int]

scala> import util.Random
import util.Random

scala> val r = new Random()
r: scala.util.Random = scala.util.Random@ad662c

//as the method is invoked twice, the two values are different.
scala> m1(r.nextInt)
res37: List[Int] = List(1317293255, 1268355315

翻译自:
http://java.dzone.com/articles/revealing-scala-magician%E2%80%99s

 

 

一些学习资料:

http://zh.scala-tour.com/#/welcome

http://twitter.github.io/scala_school/zh_cn/

http://twitter.github.io/effectivescala/index-cn.html

 

0
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics