Kotlin | 擴(kuò)展函數(shù)(終于知道為什么 with 用 this有决,let 用 it)

點(diǎn)贊關(guān)注,不再迷路轿亮,你的支持對我意義重大疮薇!

?? Hi,我是丑丑我注。本文 「Android 路線」| 導(dǎo)讀 —— 從零到無窮大 已收錄按咒,這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長但骨。(聯(lián)系方式在 GitHub)


歷史上的今天

2000 年 2 月 29 日励七,R 1.0.0 正式發(fā)布。
R 語言最初是由新西蘭奧克蘭大學(xué)的羅斯·伊哈卡和羅伯特·杰特曼開發(fā)的奔缠,由來由 “R 開發(fā)核心團(tuán)隊(duì)” 負(fù)責(zé)掠抬。R 是基于 S 語言的一個 GNU 計(jì)劃項(xiàng)目,語法來自 Schema校哎,主要用于統(tǒng)計(jì)分析两波、繪圖和數(shù)據(jù)挖掘。RStudio 是針對 R 語言設(shè)計(jì)的廣泛使用的集成開發(fā)環(huán)境闷哆。
—— 《了不起的程序員》


前言

擴(kuò)展是 Kotlin 的一種語言特性腰奋,即:在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性抱怔。擴(kuò)展使我們可以合理地遵循開閉原則劣坊,在大多數(shù)情況下是比繼承更好的選擇。


目錄


前置知識

這篇文章的內(nèi)容會涉及以下前置 / 相關(guān)知識屈留,貼心的我都幫你準(zhǔn)備好了局冰,請享用~


1. 為什么要使用擴(kuò)展测蘑?

在 Java 中,我們習(xí)慣于把通用代碼封裝到工具類中康二,諸如 StringUtils碳胳、ViewUtils 等,例如:

StringUtils.java

public static void firstChar(String str) {
    ...
}

在使用時赠摇,我們就需要調(diào)用StringUtils.firstChar(str)固逗。然而,這種傳統(tǒng)的調(diào)用方式不夠簡單直接藕帜,表意性也不夠強(qiáng)烫罩,會讓調(diào)用方忽略 String 和 firstChar() 間的強(qiáng)聯(lián)系。另外洽故,調(diào)用方也希望省略 StringUtils 類名贝攒,讓 firstChar() 看起來更像是 String 內(nèi)部的一個屬性和方法,像這樣:"str".firstChar()时甚。

要實(shí)現(xiàn)這種方式隘弊,在 Java 中就需要修改或繼承 String 類,然而 String 是 JDK 中的 final 類荒适,不能修改或繼承梨熙。

這個時候可以使用 Kotlin 擴(kuò)展來解決這個問題,我們可以把 firstChar 定義為 String 的擴(kuò)展函數(shù):

StringUtils.kt

定義 String 的擴(kuò)展函數(shù)

fun String.firstChar() {
    ...
}

此時刀诬,在使用時可以采用"str".firstChar()的方式咽扇。在這里我們擴(kuò)展了 String 類,卻沒有修改或繼承 String陕壹。

總結(jié):擴(kuò)展是 Kotlin 中的一種特性质欲,可以 在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性糠馆,更符合開閉原則嘶伟。

開閉原則(OCP,Open Closed Principle)

開閉原則是面向?qū)ο筌浖O(shè)計(jì)的原則之一又碌,即:對擴(kuò)展開放九昧,而對修改是封閉的。


2. 擴(kuò)展函數(shù) & 擴(kuò)展屬性

2.1 聲明擴(kuò)展

聲明擴(kuò)展非常簡單毕匀,只需要在聲明時增加「類或者接口名」铸鹰。這個類的名稱稱為 接收者類型(receiver type),調(diào)用這個擴(kuò)展的對象稱為 接收者對象期揪。大多數(shù)情況下,擴(kuò)展會聲明為「頂級成員」规个,例如:

Utils.kt

聲明擴(kuò)展函數(shù):
fun <T : Any?> MutableList<T>.exchange(fromIndex: Int, toIndex: Int) {
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}

聲明擴(kuò)展屬性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0

在使用時凤薛,就可以直接像使用普通成員函數(shù) / 屬性一樣:

xxx.kt

val list = mutableListOf(1,2,3)

使用擴(kuò)展函數(shù):
list.exchange(1,2)

使用擴(kuò)展屬性:
val isEven = list.sumIsEven

提示: MutableList 是接收者類型姓建,list 是接收者對象。

在擴(kuò)展函數(shù)內(nèi)部缤苫,你可以像 「成員函數(shù)」 那樣使用this來引用接受者對象速兔,當(dāng)然有時也可以省略,例如:

聲明擴(kuò)展屬性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0 // 省略了 this.sum() 中的 this

2.2 可空接收者

第 2.1 節(jié) 中使用了「非空的接收者類型」來定義擴(kuò)展(MutableList 沒有關(guān)鍵詞?)活玲,當(dāng)使用「可空變量」調(diào)用擴(kuò)展時涣狗,會報編譯時錯誤。例如:

val list:MutableList<Int>? = null
list.sumIsEven // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type MutableList<Int>?

根據(jù)提示舒憾,我們知道可以 使用「可空的接收者類型」來定義擴(kuò)展镀钓,同時還要在內(nèi)部使用null == this來對接收者對象進(jìn)行判空。例如:

可空接收者類型的擴(kuò)展函數(shù)
fun <T : Any?> MutableList<T>?.exchange(fromIndex: Int, toIndex: Int) {
    if (null == this) return
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}

可空接收者類型的擴(kuò)展屬性
val MutableList<Int>?.sumIsEven: Boolean
    get() = if (null == this)
        false
    else
        this.sum() % 2 == 0

2.3 在 Java 中調(diào)用

擴(kuò)展的本質(zhì):擴(kuò)展函數(shù)是定義在類外部的靜態(tài)函數(shù)镀迂,函數(shù)的第一個參數(shù)是接收者類型的對象丁溅。這意味著調(diào)用擴(kuò)展時不會創(chuàng)建適配對象或者任何運(yùn)行時的額外消耗。

在 Java 中探遵,我們只需要像調(diào)用普通靜態(tài)方法那樣調(diào)用擴(kuò)展即可窟赏。例如:

xxx.java

ArrayList<Integer> list = new ArrayList<>(3);

使用擴(kuò)展函數(shù):
UtilsKt.exchange(list, 1, 2);

使用擴(kuò)展屬性:
boolean isEven = UtilsKt.getSumIsEven(list);

2.4 擴(kuò)展的作用域

當(dāng)你定義了一個擴(kuò)展之后,它不會自動在整個項(xiàng)目內(nèi)生效箱季。在其它包路徑下涯穷,需要使用improt導(dǎo)入。例如:

import Utils.exchange
或
import Utils.*

當(dāng)你在不同包中定義了 「重名擴(kuò)展」藏雏,并且需要在同一個文件中去使用它們拷况,那么你需要使用as關(guān)鍵字重新命名。例如:

import Utils.exchange as swap

使用時:
list.swap(0,1)

2.5 注意事項(xiàng)

  • 1诉稍、擴(kuò)展函數(shù)不能訪問 private 或 protected 成員

擴(kuò)展函數(shù)或擴(kuò)展屬性本質(zhì)上是定義在類外部的靜態(tài)方法蝠嘉,因此擴(kuò)展不可能打破類的封裝性而去調(diào)用 private 或 protected 成員;

  • 2杯巨、不能重寫擴(kuò)展函數(shù)

擴(kuò)展函數(shù)在 Java 中會被編譯為靜態(tài)函數(shù)蚤告,并不是類的一部分,不具備多態(tài)性服爷。盡管你可以給父類和子類都定義一個同名的擴(kuò)展函數(shù)杜恰,看起來像是方法重寫,但實(shí)際上兩個函數(shù)沒有任何關(guān)系仍源。當(dāng)這個函數(shù)被調(diào)用時心褐,具體調(diào)用的函數(shù)版本取決于變量的 「靜態(tài)類型」,而不是 「動態(tài)類型」笼踩。

靜態(tài)方法調(diào)用

關(guān)于 Java 方法調(diào)用的本質(zhì)逗爹,在我之前寫過的一篇文章里系統(tǒng)分析過:Java | 方法調(diào)用的本質(zhì)(含重載與重寫區(qū)別)。靜態(tài)方法調(diào)用在編譯后生成invokestatic字節(jié)碼指令嚎于,它的處理邏輯如下:

  • 1掘而、編譯階段:確定方法的符號引用挟冠,并固化到字節(jié)碼中方法調(diào)用指令的參數(shù)中;
  • 2袍睡、類加載解析階段:根據(jù)符號引用中類名知染,在對應(yīng)的類中找到簡單名稱與描述符相符合的方法,如果找到則將符號引用轉(zhuǎn)換為直接引用斑胜;否則控淡,按照繼承關(guān)系從下往上依次在各個父類中搜索;
  • 3止潘、調(diào)用階段:符號引用已經(jīng)轉(zhuǎn)換為直接引用掺炭;調(diào)用invokestatic不需要將對象加載到操作數(shù)棧,只需要將所需要的參數(shù)入棧就可以執(zhí)行invokestatic指令覆山。
  • 3竹伸、如果類的成員函數(shù)和擴(kuò)展函數(shù)擁有相同的簽名,成員函數(shù)優(yōu)先

  • 4簇宽、擴(kuò)展屬性沒有支持字段勋篓,不會保存任何狀態(tài)

擴(kuò)展屬性是沒有狀態(tài)的,必須定義 getter 訪問器魏割。因?yàn)椴豢赡芙o現(xiàn)有的 Java 類添加額外的字段譬嚣,所以也就沒有地方可以存儲支持字段。舉個例子钞它,以下代碼是編譯錯誤的:

val MutableList<Int>?.sumIsEven: Boolean = true // (X) Initializer is not allowed here because this property has no backing field
    get() = if (null == this)
        false
    else
        this.sum() % 2 == 0

3. 標(biāo)準(zhǔn)庫中的函數(shù)

在 Kotlin 標(biāo)準(zhǔn)庫中拜银,定義了一系列通用的內(nèi)聯(lián)函數(shù):T.apply、T.also遭垛、T.let尼桶、T.run、with锯仪。你是否清楚理解它們的用法 & 本質(zhì)泵督,它們都是擴(kuò)展函數(shù)嗎?

val str1: String = "".run {
    println(this.length)
    this
}

val str2: String = with("") {
    println(this.length)
    this
}

val str3: String = "".apply {
    println(this.length)
}

val str4: String = "".also {
    println(it.length)
}

val str5: String = "".let {
    println(it.length)
    it
}

在上面的示例中庶喜,我們看到有的函數(shù)作用域內(nèi)使用了this小腊,而其它又使用了it。這兩個關(guān)鍵字到底引用的是什么久窟,為什么會有差別呢秩冈?

我們先找到這些函數(shù)的聲明:

standard.kt

public inline fun <R> run(block: () -> R): R { 
    return block()
}

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

一臉懵逼,別急斥扛,我們梳理一下:

函數(shù) 參數(shù)1 參數(shù)2 返回值
run / ()->R R
T.run / T.()->R R
with T T.()->R R
T.apply / T.()->Unit T
T.also / (T)->Unit T
T.let / (T)-R R

還是一臉懵逼入问,那我提幾個問題:

  • runvsT.run,差了一個T,區(qū)別是什么芬失?
    區(qū)別在于:run是普通函數(shù)卷仑,T.run是擴(kuò)展函數(shù)。run中的this是聲明的類對象(頂級函數(shù)除外)麸折,T.run中的this是接收者對象;

  • T.()->Unitvs(T)->Unit粘昨,或者T.()->Rvs(T)->R垢啼,T 的位置不同,區(qū)別是什么张肾?
    區(qū)別在于:T.()->Unit中的 T 是接收者類型芭析,(T)->Unit中的 T 是函數(shù)參數(shù);

  • 為什么withthis吞瞪,letit馁启?

    • run、with芍秆、apply 函數(shù)中的參數(shù) block 是 「T 的擴(kuò)展函數(shù)」惯疙,所以采用 this 是擴(kuò)展函數(shù)的接收者對象(receiver)。另外因?yàn)?block 沒有參數(shù)妖啥,所以不存在 it 的定義霉颠。
    • also 和 let 參數(shù) block 是 「參數(shù)為 T 的函數(shù)」,所以采用 it 是唯一參數(shù)(argument)荆虱。另外因?yàn)?block 不是擴(kuò)展函數(shù)蒿偎,所以不存在 this 的定義。

lambda 表達(dá)式

lambda 表達(dá)式本質(zhì)上是 「可以作為值傳遞的代碼塊」怀读。在老版本 Java 中诉位,傳遞代碼塊需要使用匿名內(nèi)部類實(shí)現(xiàn),而使用 lambda 表達(dá)式甚至連函數(shù)聲明都不需要菜枷,可以直接傳遞代碼塊作為函數(shù)值苍糠。

當(dāng) lambda 表達(dá)式只有一個參數(shù),可以用it關(guān)鍵字來引用唯一的實(shí)參犁跪。


4. 擴(kuò)展的應(yīng)用場景

在這一節(jié)里椿息,我們來介紹一些在 Android 開發(fā)中使用擴(kuò)展的應(yīng)用場景。

4.1 封裝工具 Utils

在 Java 中坷衍,我們習(xí)慣于把通用代碼封裝到工具類中寝优。傳統(tǒng) Java 的工具方法的調(diào)用方式不夠簡單直接,表意性也不夠強(qiáng)枫耳,會讓調(diào)用方忽略 String 和 firstChar() 間的強(qiáng)聯(lián)系乏矾。另外,調(diào)用方也希望省略 StringUtils 類名,讓 firstChar() 看起來更像是 String 內(nèi)部的一個屬性和方法钻心。這些需求對 Kotlin 擴(kuò)展來說都不是問題凄硼。

4.2 解決煩人的 findViewById

在 Android 中,經(jīng)常會調(diào)用 findViewById() 來找到視圖樹中的某一個 View 實(shí)例捷沸,例如:

舊版 SDK:loginButton = (Button) findViewById(R.id.btn_login);
新版 SDK:loginButton = findViewById(R.id.btn_login);

提示: 新版的 SDK 中摊沉,findViewById() 是一個泛型方法,所以你就不再需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換痒给。

public <T extends View> T findViewById(@IdRes int id) {
   return getWindow().findViewById(id);
}

通常说墨,我們會定義一個實(shí)例變量或者局部變量來承載 findViewById() 的返回值,很多時候苍柏,這些變量都只是 “臨時變量” 尼斧,在進(jìn)行事件綁定 / 賦值之后就沒有很大的用處了。如果你面對一些比較復(fù)雜的界面试吁,你甚至需要定義幾十行臨時變量棺棵!

能不能省略這些臨時變量,直接操作R.id.*呢熄捍?答案是可以的烛恤,我們可以利用 Kotlin 「高階函數(shù) + 擴(kuò)展函數(shù)」。例如:

fun Int.onClick(click: () -> Unit) {
    findViewById<View>(this).apply {
        setOnClickListener {
            click()
        }
    }
}

此時余耽,我們可以直接使用R.id.*來綁定點(diǎn)擊事件(R.id* 本質(zhì)就是一個整數(shù)類型):

R.id.btn_login.onClick {
    // do something
}

這樣就簡潔多了棒动,我們就不再需要定義一堆臨時變量了。不過宾添,你每次都需要寫R.id前綴船惨,這似乎也很多余,能不能再省略呢缕陕?確實(shí)可以粱锐,我們需要使用 Kotlin 為 Android 量身定制的 Gradle 插件:kotlin-android-extensions

apply plugin : 'kotlin-android-extension'

此時扛邑,我們可以直接用組件的 id 來操作 View 實(shí)例怜浅,例如:
MainActivity .java

btn_login.setOnClickListener{
    // do something
}

我們試試反編譯這段代碼,可以看到kotlin-android-extensions插件自動在 Activity 類中插入了以下代碼:

MainActivity.class

public class MainActivity extends AppCompatActivity {

    private HashMap _$_findViewCache;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ((Button) this._$_findViewCache(id.btn_login)).setOnClickListener((View.OnClickListener) null.INSTANCE);
    }

    public View _$_findCachedViewById(int var1) {
        if (this._$_findViewCache == null) {
            this._$_findViewCache = new HashMap();
        }
        View var2 = (View) this._$_findViewCache.get(Integer.valueOf(var1));
        if (var2 == null) {
            var2 = findViewById(var1);
            this._$_findViewCache.put(Integer.valueOf(var1), var2);
        }
        return var2;
    }

    public void _$_clearFindViewByIdCache() {
        if (this._$_findViewCache != null) {
            this._$_findViewCache.clear();
        }
    }
}

可以看到蔬崩,在訪問R.id.*控件時恶座,先在緩存集合_$_findViewCache中查找,有就直接返回沥阳,沒有就通過 findViewById() 進(jìn)行查找跨琳,并添加到緩存集合中。

另外還提供了一個_$_clearFindViewByIdCache()方法桐罕,用于在徹底替換界面視圖時清除徹底緩存脉让。在 Fragment#onDestroyView() 中桂敛,會調(diào)用該方法清除緩存,而 Activity 中沒有溅潜。

4.3 簡潔的 LeetCode 題解

在解算法題時术唬,使用擴(kuò)展函數(shù)可以讓代碼更簡潔,表意性更強(qiáng)滚澜。舉個例子粗仓,我們需要交換數(shù)組中的兩個位置上的元素。相對于傳統(tǒng)的寫法设捐,可以看到擴(kuò)展函數(shù)的寫法意思更清楚潦牛。

fun swap(arr: IntArray, from: Int, to: Int) {
    ...
}
swap(arr,0,1)

fun IntArray.swap(from: Int, toInt) {
    ...
}
arr.swap(0,1)

5. 總結(jié)

  • 擴(kuò)展可以在不修改類 / 不繼承類的情況下,向一個類添加新函數(shù)或者新屬性挡育,更符合開閉原則。相對于傳統(tǒng) Java 的工具方法的調(diào)用方式更簡單直接朴爬,表意性更強(qiáng)即寒;

  • 擴(kuò)展函數(shù)是定義在類外部的靜態(tài)函數(shù),函數(shù)的第一個參數(shù)是接收者類型召噩,調(diào)用擴(kuò)展時不會創(chuàng)建適配對象或者任何運(yùn)行時的額外消耗母赵。在 Java 中,我們只需要像調(diào)用普通靜態(tài)方法那樣調(diào)用擴(kuò)展即可具滴;

  • 標(biāo)準(zhǔn)庫提供的函數(shù)中凹嘲,run、with构韵、apply 函數(shù)中的參數(shù) block 是「T 的擴(kuò)展函數(shù)」周蹭,所以采用 this 是擴(kuò)展函數(shù)的接收者對象(receiver);also 和 let 參數(shù) block 是「參數(shù)為 T 的函數(shù)」疲恢,所以采用 it 是唯一參數(shù)(argument)凶朗。


參考資料


創(chuàng)作不易,你的「三連」是丑丑最大的動力显拳,我們下次見棚愤!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市杂数,隨后出現(xiàn)的幾起案子宛畦,更是在濱河造成了極大的恐慌,老刑警劉巖揍移,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件次和,死亡現(xiàn)場離奇詭異,居然都是意外死亡那伐,警方通過查閱死者的電腦和手機(jī)斯够,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門囚玫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人读规,你說我怎么就攤上這事抓督。” “怎么了束亏?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵铃在,是天一觀的道長。 經(jīng)常有香客問我碍遍,道長定铜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任怕敬,我火速辦了婚禮揣炕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘东跪。我一直安慰自己畸陡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布虽填。 她就那樣靜靜地躺著丁恭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斋日。 梳的紋絲不亂的頭發(fā)上牲览,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音恶守,去河邊找鬼第献。 笑死,一個胖子當(dāng)著我的面吹牛兔港,可吹牛的內(nèi)容都是我干的痊硕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼押框,長吁一口氣:“原來是場噩夢啊……” “哼岔绸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起橡伞,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盒揉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后兑徘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刚盈,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年挂脑,在試婚紗的時候發(fā)現(xiàn)自己被綠了藕漱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲侮。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肋联,靈堂內(nèi)的尸體忽然破棺而出威蕉,到底是詐尸還是另有隱情,我是刑警寧澤橄仍,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布韧涨,位于F島的核電站,受9級特大地震影響侮繁,放射性物質(zhì)發(fā)生泄漏虑粥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一宪哩、第九天 我趴在偏房一處隱蔽的房頂上張望娩贷。 院中可真熱鬧,春花似錦锁孟、人聲如沸彬祖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腹躁,卻和暖如春桑包,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纺非。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工哑了, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烧颖。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓弱左,卻偏偏與公主長得像,于是被迫代替她去往敵國和親炕淮。 傳聞我的和親對象是個殘疾皇子拆火,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359