學(xué)習(xí)Kotlin,看這一篇就夠了

人生苦短,要用Kotlin

這是一種對程序猿更為友好的語言,可以減少開發(fā)者的工作量,原本由開發(fā)者干的事情,其實(shí)很多都可以由編譯器實(shí)現(xiàn)了,這是一種更為高級的語言。Java雖然嚴(yán)謹(jǐn),但卻過于繁瑣,太啰嗦了,一個(gè)小事情卻要寫大量的代碼,而且有些代碼又是非常機(jī)械式的,在實(shí)際編碼過程中都是用IDE來自動生成。Java,C,C++,Object C這些都是上世紀(jì)的編程語言。

Kotlin img

現(xiàn)在到了新時(shí)代了,編程也發(fā)展了很多,像lambda表達(dá)式,函數(shù)式編程,等等一些新的概念和范式在涌現(xiàn)。所以就有了新時(shí)代的編程語言,像水果的Swift,Groovy,Scala,以及Java陣營的Kotlin。Kotlin是新一代的編程語言,與Java完美融合,簡潔,方便,可以大大提高程序可讀性,特別是對于Android開發(fā)者來說。水果推出了Swift以解放水果平臺的開發(fā)者,而Kotlin就是來解放Android開發(fā)者的。

雖然說Kotlin可以用在任何可以用Java的地方,但目前主要就是兩大領(lǐng)域服務(wù)端,以及Android應(yīng)用開發(fā),特別是有了Google官方的支持,所以Kotlin對于Android開發(fā)者的意義更為重大,身為一個(gè)Android猿,是一定要學(xué)習(xí)一下這門現(xiàn)代的編程語言的,因?yàn)楫?dāng)你學(xué)過了之后 ,你會發(fā)現(xiàn),之前寫的代碼都是在浪費(fèi)生命。

Development environment setup

有三種方式

命令行

其實(shí),這是最好的方式,因?yàn)榕渲闷饋矸浅5姆奖?。?a target="_blank" rel="nofollow">官網(wǎng)去下載編譯器,解壓,然后把kotlinc/bin/放到PATH環(huán)境變量里面,就可以了。如果要配置Vim,還需要安裝一下插件,大神們早就把插件準(zhǔn)備好了,只需要下載,然后按照官方方法安裝即可,其實(shí)就是把解壓后的東西拷貝到相應(yīng)的目錄里面就好了。

Idea IntellJ

這個(gè)看官方文檔就可以了,孤未親測,如遇困難請自行Google。

Android Studio

因?yàn)镵otlin官已支持了Android Studio,而Google也支持了,總而言之就是在Android Studio中可以直接使用Kotlin。所以, Android Stuido 3.0以后的版本無需特殊配置,就可以用例Kotlin了。

對于剛開始學(xué)習(xí)Kotlin而言呢,孤推薦使用命令行的方式,而不要使用Android Studio,特別是直接創(chuàng)建一個(gè)基于Kotlin的Android項(xiàng)目,因?yàn)榇藭r(shí)對語言還不夠熟悉,直接上項(xiàng)目,會迷失在項(xiàng)目配置,frameworks以及語言基礎(chǔ)之中。剛學(xué)習(xí)一門語言的時(shí)候要先學(xué)習(xí)基本的語法以及語言本身的特性,這最好先繞開框架和項(xiàng)目,會更容易上手一些。

Hello world

這是所有編程語言的入門必學(xué)課程,目的是讓學(xué)習(xí)者快速的體驗(yàn)一下一門語言,我們也不用多想,照著一個(gè)字母,一個(gè)字母的把示例敲進(jìn)去就好了:

  1. 選擇喜歡的文本編輯器,如Vim hello.kt,Kotlin的文件擴(kuò)展名是*.kt,我們遵循就好。
  2. 一字不差的敲進(jìn)去:
package hello

fun main(args: Array<String>) {
    println("Hello, world")
}

然后,保存文件

  1. 回到命令行,編譯源碼,如果一切順利會得到一個(gè)叫hello.jar的文件,這就是kotlin的最終輸出,也就是它的目標(biāo)文件.
kotlinc hello.kt -include-runtime -d hello.jar
  1. 運(yùn)行,這里跟Kotlin其實(shí)已經(jīng)沒啥關(guān)系了,因?yàn)榻?jīng)過編譯得到的是一個(gè)標(biāo)準(zhǔn)的Jar文件,像運(yùn)行其他jar一樣運(yùn)行就好了:
java -jar hello.jar

就會得到輸出Hello, world到此,第一個(gè)Kotlin程序已經(jīng)完成,是不是很酷,已經(jīng)迫不及待的想深入學(xué)習(xí)了!往下看吧。

The basics

語句結(jié)構(gòu)

一行一個(gè)語句(先不糾結(jié)語句與表達(dá)式的區(qū)別),不用加分號,不用打分號,光這個(gè)就可以節(jié)省多少時(shí)間呢?是不是感覺人生都浪費(fèi)在了分號上面。如果想在一行寫多個(gè)語句,前面的要加上分號。

縮進(jìn)規(guī)則與Java一致,用四個(gè)空格,也可以用tab,或者不加縮進(jìn),只要沒人打你。

語句塊需要加上花括號{}??傊?,語句結(jié)構(gòu)與Java很類似。

變量

用var來聲明變量,用val來聲明常量,因?yàn)镵otlin是靜態(tài)強(qiáng)類型語言(也就是說每個(gè)變量在編譯的時(shí)候必須知道類型)聲明時(shí)需要帶上類型,方法是在變量名的后面加冒號,空格跟上類型名字,與Pascal差不多。如果聲明時(shí)直接定義,則可以不用指定類型,編譯器會根據(jù)定義表達(dá)式來推測它的類型。示例:

var str: String
val i: Int
var str = "Hello, world"

語句和表達(dá)式

主要想說一下語句和表達(dá)式的區(qū)別,簡單來說就是表達(dá)式是有值的,可以放在變量賦值的右邊,而語句是沒有值的,不能放在賦值的右邊

基本運(yùn)算

不多說了,跟Java一樣

注釋

這個(gè)跟Java也一樣:
// 單行注釋
/* / 多行注釋
/
* */ documentation

函數(shù)

以fun關(guān)鍵字來定義一個(gè)函數(shù)格式為:fun 函數(shù)名(參數(shù)): 返回類型 {函數(shù)體},如:

fun foo(name: String): Int {
   return name.length()
}

命名參數(shù)和默認(rèn)值,調(diào)用函數(shù)時(shí)可以把參數(shù)的名字帶上,以增加可讀性。聲明函數(shù)時(shí)可以用默認(rèn)值 ,以更好的支持函數(shù)的重載。如:

fun foo(name: String, number: Int = 42, toUpper: Boolean = false): String {}

使用時(shí),可以指定參數(shù)的名字:

foo("a)
foo("b", number = 1)
foo("c", toUpper = true)
foo(name = "d", number = 2, toUpper = false)

表達(dá)式體如果一個(gè)函數(shù)體內(nèi)只有一個(gè)表達(dá)式,且有返回值時(shí),那么,可以直接把返回值放在函數(shù) 的后面,如:

fun foo(name: String): String = name.toUpperCase()

甚至還可以把返回類型的聲明給省略掉,如:

fun foo(name: String) = name.toUpperCase()

跟Java不一樣的是,Kotlin的函數(shù)可以聲明為toplevel也就是跟class一個(gè)級別,也就是說不必非放在類里面,也就是說跟C和C++是類似的。此外,還可以函數(shù)賦值給一個(gè)變量,這個(gè)變量就像其他變量一樣。

類與對象

類的聲明與對象創(chuàng)建

用class來聲明一個(gè)類型,用:來繼承父類或者實(shí)現(xiàn)接口,不需要使用new來創(chuàng)建對象:

class Person {
   var name: String
   var age: Int
}

假如,一個(gè)類,是空的,沒有內(nèi)容,那么花括號{}是可以省略的:

class Person

創(chuàng)建對象:

var someone = Person()

Primary constructor

構(gòu)造方法,有所謂的primary constructor,可以直接寫在類名的后面:

class Person constructor(name: String)

一般情況下,constructor 可以省略掉:

class Person(name: String)

初始化塊因?yàn)閜rimary constructor不能包含代碼,所以,想要做些初始化工作就可以放在初始化塊里面(initializer block),也可以在定義屬性時(shí)直接使用:

class Person(name: String) {
    var firstName: String = name
    init {
        println("First initializer block that prints ${name}")
    }
}

一般情況下,如果聲明的屬性變量在primary constructor中都有賦值(通過initializer block)的話,可以有更簡潔的表達(dá)方式:

class Person(var name: String, var age: Int)

這相當(dāng)于:

class Person(theName: String, theAge: Int) {
   var name: String = theName   var age: Int = theAge
}

如果primary construct前面要聲明屬性,或者有annotation的話,關(guān)鍵字constructor不能省略:

class Person public @Inect constructor(var name: String)

Secondary constructor

如果primary constructor不能滿足需求怎么辦呢?還可以聲明其他constructor,所謂的secondary constructor:

class Person {
   var name: String constructor(name: String){
       this.name = name
   }
}

是不是看起來舒服一些,因?yàn)楦鶭ava一樣了,可以把primary constfuctor和second constructor聯(lián)合起來一起用:

class Person(var name: String) {
    constructor(name: String, parrent: Person) : this(name) {
        parrent.addChild(this)
    }
}

這里要把secondary construct盡可能delegate到primary constructor,這里的delegate的意思就是primary constructor會在second constructor之前 執(zhí)行,還有就是initiailzer block都是在primary construct中執(zhí)行的,這就能保證initiliazer block在second constructor之前執(zhí)行。即使沒有顯示的聲明primary constructor,編譯器還是會生成一個(gè)默認(rèn)的primary constructor以及把secondary constructor默認(rèn)的delegate到primary constrcutor上面。也就是說,會保證primary constructor以及initializer block執(zhí)行在second constructor前面:

class Constructors {
    init {
        println("Initializer block")
    }

    constructor(i: Int) {
        println("second constructor")
    }
}

fun main(args: Array<String>) {
    val c = Constructors(3)
}

輸出:

Initializer block
second constructor

屬性和訪問方法

Kotlin會為聲明的屬性生成默認(rèn)的setter和getter:

class Person(var name: Strring, var age: Int)

val p = Person("Kevin", 24)
p.getName() // 返回"Kevin"
p.setAge(32) // age變成了32

如果想自定義setter和getter,也是可以的:

class Person {
    var name: String
        set(n: String) {
            if (n == null || n == "") {
                name = "Unkown"
            } else {
                name = n
            }
        }
        get() {
            if (name == "Unkwon") {
                return "Nobody"
            }
            return name
        }
}

定義類的方法

跟聲明普通函數(shù)一樣,只不過是放在了類里面:

class Person(val name: String, val age: Int) {
    fun report() = "My name is $name, and I'm $age"
}

如果,要覆寫父類的方法,需要使用在方法聲明時(shí)加上override關(guān)鍵字。

class Doggy(val name: String) : Animal {
    override fun yell() = "Barking from $name"
}

訪問權(quán)限

訪問權(quán)限也跟Java類似分為public,protected,private以及internal,前三個(gè)意義也都一樣,只不過默認(rèn)值不一樣,在Java里,如果對成員沒有指明,則是package scope,也就是同一個(gè)package可以訪問,但是Kotlin默認(rèn)是public的。

internal是module內(nèi)部可見,有點(diǎn)類似于Java中的package,但是module定義跟package不一樣,module是一組編譯在一起的Kotlin文件,它跟編譯打包有關(guān)系,簡單的理解它的范圍要比package要大。

還有就是類,默認(rèn)是不可被繼承的,相當(dāng)于final class。如果想要允許繼承就要在聲明類的時(shí)候加上open。

字串

概念就不說了,大部分與Java一模一樣的,像支持的方法等。唯一需要說的就是字串模板,就是說把其他類型轉(zhuǎn)化為字串時(shí),有較Java更為方便的方式:直接用$來把變量嵌入到字串之中,如:

val msg = "Error 1"
val count = 32
print("We got message $msg") //等同于"We got message " + msg
print("Total is $count") // Total is 32

lambda表達(dá)式

首先要介紹一個(gè)概念,高階函數(shù),其實(shí)就是把另外函數(shù)當(dāng)作參數(shù)的函數(shù),或者說產(chǎn)生一個(gè)函數(shù),也即把函數(shù)作為返回值 的函數(shù)。前面說過,函數(shù)是一級對象,可以像常規(guī)變量一樣來使用,所以,就能把函數(shù)作為參數(shù)或者返回值來使用高階函數(shù)。lambda表達(dá)式就是為高階函數(shù)更方便使用而生的。

lambda 表達(dá)式

作為新時(shí)代的編程語言,都會支持函數(shù)式編程,而lambda表達(dá) 式又是函數(shù)式編程里面必不可少的一份子。其實(shí)啥是lambda表達(dá)式呢?說的簡單點(diǎn)就是沒有名字的函數(shù),非常簡短的,通常都是一兩句話的沒有名字的函數(shù)。就是長這個(gè)樣子{A, B -> C},這里面A,B是參數(shù),C是表達(dá)式,如:

val sum = { x: Int, y: Int -> x + y }

其中,參數(shù)的類型是可以省略的,因?yàn)榫幾g器能從上下文中推測出來:
max(strings, { a, b -> a.length < b.length }
表達(dá)式部分,可以不止一個(gè),最后一個(gè)表達(dá)式作為返回值。

當(dāng)把一個(gè)lambda表達(dá)作為最后一參數(shù),傳給某個(gè)函數(shù)時(shí),可以直接把lambda表達(dá)式寫在參數(shù)的外面,比如:

val product = items.fold(1) { acc, e -> acc * e }

而當(dāng)lambda是唯一的參數(shù)時(shí),也可以把參數(shù)的括號省略掉:

run { println("Hello, world") }

還有就是,如果lambda表達(dá)中只有一個(gè)參數(shù),那么參數(shù)也可以省略,直接寫表達(dá)式:

eval{ x * x }

函數(shù)類型

前面提到了函數(shù)是可以像普通變量一樣使用的一級類,也就是說它是一個(gè)類型。它的具體形式是: (A, B)->C,其中括號內(nèi)的是參數(shù),C是返回類型,如:

val sum: (Int, Int)->Int = { x, y -> x + y }
val square: (Int)->Int = { x -> x * x }

為啥要提一下函數(shù)類型呢,因?yàn)橛袝r(shí)需要聲明高階函數(shù):

fun walk(f: (Int)->Int)
fun run(f: ()->Unit)

Unit是一個(gè)特殊的返回值,相當(dāng)于void,意思就是此函數(shù)沒有返回值。

集合

其實(shí)大部分跟Java是一樣的。只不過有一些函數(shù)式的操作,要多注意使用,從而讓代碼更簡潔,如:

  • 遍歷
  • 過濾
  • 映射
  • 排序
  • 折疊
  • 分組
  • 歸類

這些操作,對于大家應(yīng)該都不難理解,就不一一解釋了,來斷代碼就知道了:

fun collectionTests() {
    val list = listOf("Apple", "Google", "Microsoft", "Facebook", "Twitter", "Intel", "QualComm", "Tesla")
    // 遍歷,以進(jìn)行某種操作
    list.forEach{ println(it) }
    //按條件進(jìn)行過濾,返回條件為true的
    val short = list.filter { it.length < 6 }
    println(short) // [Apple, Intel, Tesla]
    // 把列表元素映射成為另外一種元素
    val lenList = list.map{ it.length }
    println("Length of each item $lenList") //Length of each item [5, 6, 9, 8, 7, 5, 8, 5]
    // 按某種條件進(jìn)行排序
    val ordered = list.sortedBy { it.length }
    println("Sorted by length $ordered") // Sorted by length [Apple, Intel, Tesla, Google, Twitter, Facebook, QualComm, Microsoft]
    // 折疊,用累積的結(jié)果繼續(xù)遍歷
    val joint = list.fold("", {partial, item -> if (partial != "")  "$partial, $item" else item })
    println("Joint list with comma $joint") // Joint list with comma Apple, Google, Microsoft, Facebook, Twitter, Intel, QualComm, Tesla
    //分組,用某種條件 把列表分成兩組
    val (first, second) = list.partition { it.length < 6 }
    println("Length shorter than 6 $first") // Length shorter than 6 [Apple, Intel, Tesla]
    println("Longer than 6 $second") // Longer than 6 [Google, Microsoft, Facebook, Twitter, QualComm]
    // 歸類,按某種方法把元素歸類,之后變成了一個(gè)Map
    val bucket = list.groupBy { it.length }
    println("$bucket is a map now") //{5=[Apple, Intel, Tesla], 6=[Google], 9=[Microsoft], 8=[Facebook, QualComm], 7=[Twitter]} is a map now
}

null處理

為了有效的減少空指針異常,Kotlin加入了Nullable類型,核心的原理是這樣的:聲明類型的時(shí)候要明確的告訴編譯器,這個(gè)變量是否可能為null,如果可能為null,那么可以賦null給這個(gè)變量,并且在使用此變量時(shí)必須檢查是否為null;假如這個(gè)變量不可能為null,那么是不可以賦null給此變量的。也就是說,編譯器會幫忙做一些檢查,以減少NullPointerException的發(fā)生。

Nullable變量

默認(rèn)的變量聲明都是不可為null的,如:

var safe: String
safe = null // 會有compile error

要想允許變量為null,要在類型后面加一個(gè)問號,以告訴編譯器這是一個(gè)nullable類型:

var danger: String?
danger = null // OKay

使用時(shí),nullable不能直接使用,必須檢查是否為null:

safe.length // okay
danger.length // compile error, danger could be null

檢查Nullable的真?zhèn)?/h4>

可以用傳統(tǒng)方式:

val len = if (danger != null) danger.length else -1

Safe call

既然有Nullable類型,自然就有配套的方式來更方便的使用它:

val len = danger?.length

如果danger是null就返回null,否則返回長度,注意它的返回值是一個(gè)Int?(又是一個(gè)Nullable類型)。這個(gè)還能鏈起來:

bob?.department?.head?.name

如果任何一環(huán)為null,則直接返回null。是不是感覺省了好多if (a == null)判斷。

Elvis operator

假如不能接受safe call返回的null,咋辦呢?想提供默認(rèn)值的呢?也有方式:

val len = danger?.length
println(len ?: -1)

稍有點(diǎn)繞哈,首先,danger?.length返回一個(gè)Int?吧,那么?:的作用就是如果len是null,那么就返回-1,否則返回它的值。

強(qiáng)制取值符??!

它的作用是如果Nullable變量為null就拋出NullPointerException,如果正常的話就取其值,返回的類型是一個(gè)non-null類型:

val len = danger!!.length // get length or NullPointerException

盡管,編譯器可以幫助我們做一些事情,但是現(xiàn)實(shí)的項(xiàng)目中的大量的NPE并不是直接來源于,可以方便追蹤的賦值為null,而多是發(fā)生在多線程環(huán)境中,以及非常復(fù)雜的邏輯之中,編譯器能否追蹤到并警示,還有待考察。另外,就是雖有利器,但是要運(yùn)用恰當(dāng),何時(shí)用允許null,何時(shí)不允許,還是要靠工程師的設(shè)計(jì)能力,比如盡可能返回空列表,空Map,或者空字串,而不是直接簡單的返回null,這就能減少一定的NPE。

Exercises

光是看書或者看教程是比較乏味的,學(xué)習(xí)編程最重要的是要上手去練習(xí),這樣能加深印象,更好的理解書中或者教程中所講的概念和知識點(diǎn)。官方也準(zhǔn)備了一個(gè)非常好的練習(xí)項(xiàng)目叫Kotlin-koans,非常適配初學(xué)習(xí)者來練手。
下面說一下如何使用這個(gè)練習(xí)項(xiàng)目:

  1. 官網(wǎng)去下載后,解壓
  2. 用Android Studio打開此項(xiàng)目,一切提示都回答yes
  3. 要想運(yùn)行測試前需要先編譯一下項(xiàng)目,否則會提示找不到基礎(chǔ)的測試類,找到Gradle窗口,一般在右側(cè),點(diǎn)開找到kotlin-koans->Tasks->build->build,運(yùn)行它
  4. 現(xiàn)在就可以用先進(jìn)的TDD方式來學(xué)習(xí)Kotlin了,在Project視圖下面,可以看到kotlin-koans項(xiàng)目,里面有兩個(gè),一個(gè)是java,一個(gè)是tests,這兩個(gè)目錄里面的子目錄都是一一對應(yīng)的,先運(yùn)行tests下面的,會失敗,然后編輯java/下面的對應(yīng)的代碼,直到測試通過。

Essence of Kotlin

致此,我們可以看出Kotlin這門語言的設(shè)計(jì)的核心理念:簡潔,這是Kotlin的核心理念,所以我們看到,一些機(jī)械的,重復(fù)的,可以從上下文中推測 出來的都 可以省略,以增加可讀性。我們在使用Kotlin的時(shí)候要踐行此理念,把語言的特性發(fā)揮到最大。
當(dāng)然,簡潔,不是犧牲可讀性的方式來縮短代碼,而是要使用語言中的標(biāo)準(zhǔn)的簡潔的表達(dá)方式,比如lambda表達(dá)式,省略參數(shù)等。

要注意參考Kotlin conventions以及Android Kotlin conventions以寫出更加簡潔和容易理解的代碼。

Android dev setup

我們來新建一個(gè)項(xiàng)目,用純Kotlin實(shí)現(xiàn)一個(gè)Hello, world Android應(yīng)用,來展示一下如何在Android中使用Kotlin:

注意: 這里使用的是Android Studio 3.1.2版本,默認(rèn)就支持Kotlin,如果使用小于3.0的版本需要安裝Kotlin插件,可自行Google,孤還是建議先升級AS吧。

  1. 新建一個(gè)項(xiàng)目,其實(shí)流程跟新建一個(gè)普通Android Studio項(xiàng)目是一樣一樣的,從Android Studio3.0起,新建項(xiàng)目時(shí)就會有一個(gè)Checkbox,問你要不要添加Kotlin。這里把它選上。


    Step 1
  2. 就直接下一步就好


    Step 2
  3. Next,創(chuàng)建一個(gè)empty activity


    Step 3
  4. Finish

    Step 4
  5. 布局跟其他新建的Android項(xiàng)目無差別


    Step 5
  6. 代碼已經(jīng)是Kotlin的了


    Step 6
  7. 直接顯示"Hello, world"略顯無聊,所以加一下點(diǎn)擊事件:
class HelloActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hello)

        val colorTable = listOf("#ff0000", "#00ff00", "#0000ff", "#ffff00", "#00ffff", "#ff00ff")
        val label = findViewById<TextView>(R.id.label)
        label.setOnClickListener { view ->
            val randomIndex = (Math.random() * colorTable.size).toInt()
            view.setBackgroundColor(Color.parseColor(colorTable[randomIndex]))
        }
    }
}

其實(shí),整體來看,布局和項(xiàng)目的結(jié)構(gòu)還是按照Android的方式來,唯一的不同是代碼可以用Kotlin來寫了。

Good to go

至此,Kotlin就算入門了,可以使用Kotlin來構(gòu)建應(yīng)用程序了,或者在你的項(xiàng)目中應(yīng)用Kotlin了。

參考資料和有用的資料分享

原文鏈接:http://toughcoder.net/blog/2018/05/17/introduction-to-kotlin-programming-language/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,701評論 9 118
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評論 25 709
  • 1.今天一天從上午出門一直忙到晚上8點(diǎn)鐘,一直在工作中度過以至于都沒時(shí)間吃飯,和我們一起的還有兩位朋友,在過程中我...
    Ai馬爺閱讀 208評論 0 0
  • 1.model B為model A的子字段2.model B的formview中, 系統(tǒng)自動生成 parent字段...
    yiangdea閱讀 1,249評論 0 0
  • 呼呼
    用心聽F閱讀 201評論 0 0

友情鏈接更多精彩內(nèi)容