Kotlin DSL原理解析:帶接收者的lambda以及invoke約定

DSL(領(lǐng)域特定語言)是Kotlin所帶來的強(qiáng)大語法特性之一八回,也是Java中所不存在的功能肴甸,JetBrain也基于DSL開發(fā)出了眾多的開源庫稚疹,Kotlin的開發(fā)者可以使用DSL來重構(gòu)許多已有的代碼香拉,甚至有可能做到徹底拋棄HTML病毡,XML濒翻,SQL等代碼的地步。
現(xiàn)代編程語言已經(jīng)越來越向自然語言靠攏啦膜,因此學(xué)習(xí)使用一個(gè)語法特性并非難事有送,所以本文將延續(xù)本專題的風(fēng)格:“理論先行”,重點(diǎn)在于詳細(xì)講解DSL在Kotlin中的實(shí)現(xiàn)原理僧家,以及如果我們使用DSL怎樣去構(gòu)建一套合理的API雀摘,而如何去使用已有的DSL庫,這個(gè)根據(jù)不同的開發(fā)需求因人而異八拱,本文會簡單介紹阵赠,但不是重點(diǎn);文章將會先解釋到底什么是DSL肌稻,再來詳細(xì)講解DSL的兩大原理——帶接收者的lambda和invoke約定清蚀,最后簡單介紹一些好用的基于DSL的開源庫。

  • Part 1:到底什么是DSL

  • Part 2:深入理解帶接收者的lambda

  • Part 3:函數(shù)式的對象——invoke約定

  • Part 4:那些優(yōu)秀的DSL開源庫

Part 1:到底什么是DSL

DSL的簡單介紹

文章開頭我們就已經(jīng)提到爹谭,DSL是領(lǐng)域特定語言的英文縮寫枷邪。那到底什么是領(lǐng)域特定語言?我們最常使用的領(lǐng)域特定語言就是SQL以及正則表達(dá)式诺凡,SQL和正則表達(dá)式都只能解決它們特定領(lǐng)域內(nèi)的問題东揣,SQL用于數(shù)據(jù)庫操作,而正則表達(dá)式則是用來處理文本字符串腹泌,它們也都有自己的語法嘶卧,但是你無法使用它們在計(jì)算機(jī)上編寫完整的程序;所以它們并不是我們常規(guī)意義上理解的“編程語言”凉袱,那些有能力在計(jì)算機(jī)上編寫幾乎任何程序的編程語言芥吟,諸如,Kotlin绑蔫,Java运沦,Python等等我們有一個(gè)專業(yè)術(shù)語來定義它們,叫做圖靈完備語言配深,而上面介紹的那些DSL就不是圖靈完備的携添。

這里再多說一句,針對上面這個(gè)概念篓叶,還有一個(gè)比較好的例子來說明它們烈掠。了解區(qū)塊鏈的朋友們也許知道羞秤,比特幣內(nèi)部有一種比特幣腳本語言,它用來描述整個(gè)比特幣的交易過程左敌,它同樣有順序瘾蛋,分支等結(jié)構(gòu),但是沒有循環(huán)矫限;它除了用來做比特幣交易之外什么都做不了哺哼,因此它是一種DSL,當(dāng)然叼风,它也不是圖靈完備的取董。而區(qū)塊鏈的另一大系統(tǒng)——以太坊則完全不同,它的內(nèi)部也有一種用來描述以太坊內(nèi)部運(yùn)作的語言——Solidity无宿,但是它有完整的編程語言結(jié)構(gòu)茵汰,而以太坊內(nèi)部執(zhí)行Solidity的系統(tǒng)被稱為EVM(以太坊虛擬機(jī)),我們甚至可以使用Solidity來編寫一個(gè)國際象棋游戲并發(fā)布到以太坊區(qū)塊鏈孽鸡,因此Solidity是一種圖靈完備的編程語言蹂午。我們通過以下兩個(gè)簡單的例子來直觀的感受DSL和圖靈完備語言在語法上的不同之處:

/** 
 * 比特幣腳本語言
 * 摘自《區(qū)塊鏈:技術(shù)驅(qū)動金融》P74
 */
 OP_DUP
 OP_HASH160
 69e02e18...
 OP_EQUALVERIFY
 OP_CHECKSIG

比特幣腳本語言的語法規(guī)則非常簡單,就像一行一行在輸入命令彬碱。

/** 
 * 以太坊中的Solidity
 * 摘自《區(qū)塊鏈:技術(shù)驅(qū)動金融》P351
 */
contract NameRegistry {
    mapping(bytes32 => address) public registryTable;
    function claimName(byte32 name) {
        if (msg.value < 10) {
            throw;
        }
        if (registryTable[name] == 0) {
            registryTable[name] = msg.sender;
        }
    }
}

我們可以看到Solidity的語法看起來就非常強(qiáng)大豆胸,各種常見的if語句,賦值運(yùn)算等就不說了巷疼,還能定義且調(diào)用和執(zhí)行強(qiáng)大的函數(shù)配乱,這些常規(guī)編程語言中最常見的功能它都有;通過這兩個(gè)直觀的例子皮迟,我們很容易看出來DSL和圖靈完備語言的不同之處。

Kotlin中的DSL

區(qū)塊鏈那一塊有點(diǎn)扯遠(yuǎn)了桑寨,我們回到Kotlin伏尼;我們?nèi)绻谖覀兙帉懙某绦蛑惺褂肈SL,需要把它們保存到一個(gè)文本字符串內(nèi)尉尾,然后再通過調(diào)用源編程語言的函數(shù)(方法)爆阶,將它們作為參數(shù)傳入進(jìn)去,然后它們才能得到執(zhí)行沙咏,不妨回憶一下我們是如何在Java中使用SQL和正則表達(dá)式的辨图;這樣的使用方式最大的缺點(diǎn)就是內(nèi)嵌在源編程語言中的DSL,無法在編寫過程中獲得IDE的錯(cuò)誤提示肢藐,也無法在編譯期獲得語法錯(cuò)誤檢查故河,哪怕只是輸錯(cuò)一個(gè)字母,都將導(dǎo)致運(yùn)行時(shí)的執(zhí)行失敗吆豹,因此我們原先使用DSL的方式是不安全的鱼的。

既然將DSL作為文本字符串直接內(nèi)嵌在源編程語言的代碼中是不安全的理盆,那有沒有可能使用某種方式,讓它們和源編程語言在一定程度上掛鉤凑阶,這樣既方便使用猿规,又能讓它們得到IDE的錯(cuò)誤提示和編譯期的語法檢查?這也是一些編程語言社區(qū)內(nèi)討論過很多的概念——內(nèi)部DSL宙橱;如果你是inteliJ IDEA的用戶姨俩,其實(shí)也早就使用過內(nèi)部DSL了,當(dāng)你在編寫Gradle的規(guī)則文件的時(shí)候师郑,使用的就是Groovy語言的內(nèi)部DSL(當(dāng)然环葵,目前Gradle文件已經(jīng)支持使用Kotlin DSL來編寫。)

使用內(nèi)部DSL編寫出來的代碼呕乎,和它們的源編程語言是一樣的积担,比如說Kotlin代碼文件的擴(kuò)展名是.kt,而使用Kotlin DSL編寫出來的代碼的文件也是.kt猬仁,因?yàn)樗举|(zhì)上就是Kotlin代碼帝璧,因此它也可以存在于任何.kt文件中和普通Kotlin代碼混編;那普通Kotlin代碼和Kotlin DSL之間到底有什么明顯的界限湿刽?實(shí)際上并沒有(《Kotlin in Action》的作者在書中說:判斷的標(biāo)準(zhǔn)應(yīng)該主觀到:“當(dāng)我看見它的時(shí)候的烁,就知道它是一個(gè)DSL”。)诈闺,DSL看起來特殊的語法其實(shí)是由Kotlin中的兩種語法特性——帶接收者的lambda和invoke約定渴庆,來提供支持,關(guān)于這兩者雅镊,后文會有具體討論襟雷。

Kotlin DSL的例子

我們來舉一個(gè)Kotlin DSL的例子,如果我們使用JetBrain構(gòu)建Android UI的開源庫Anko的話仁烹,我們可以用DSL重構(gòu)一份XML代碼耸弄;我們先來看看XML:

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="30dp"
            android:orientation="vertical" >
            
            <EditText
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:hint="Name"
                android:textSize="24sp" />

            <EditText
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:hint="Password"
                android:textSize="24sp" />

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Login"
                android:textSize="26sp" />
            
        </LinearLayout>

如果換成DSL來編寫如下:

    lineatLayout {
        orientation = LinearLayout.VERTICAL
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }.lparam(wrapContent, wrapContent)
        editText {
            hint = "Password"
            textSize = 24f
        }.lparam(wrapContent, wrapContent)
        button("Login") {
            textSize = 26f
        }.lparam(wrapContent, wrapContent)
    }

即使你不是Android開發(fā)者,也可以輕易的看出兩者的異同卓缰,XML中的元素:LinearLayout, EditText计呈,Button等的層級嵌套關(guān)系和DSL中的完全一至,屬性的賦值也是應(yīng)有盡有征唬;這說明捌显,如果你想把當(dāng)前的一些編寫起來不那么方便的代碼,遷移到基于Kotlin DSL的庫总寒,大多數(shù)情況下其實(shí)學(xué)習(xí)成本并不高扶歪,實(shí)際上變化的只是一些簡單的語法規(guī)則。

我們在開始下一節(jié)之前偿乖,先來看看這一段DSL代碼击罪,做一個(gè)簡單的分析哲嘲,并提出幾個(gè)問題。我可以首先先告訴大家一個(gè)結(jié)論媳禁,linearLayout {}眠副,editText {},button {}竣稽,這些東西全部都是Kotlin高階函數(shù)囱怕,而orientation和padding這些都是Kotlin中的屬性;學(xué)習(xí)過Kotlin的高階函數(shù)的你應(yīng)該知道毫别,linearLayout {}大括號的內(nèi)部實(shí)際上是一個(gè)lambda表達(dá)式娃弓,它作為一個(gè)參數(shù),被傳遞給了函數(shù)linearLayout岛宦,而在這個(gè)lambda表達(dá)式的外部台丛,你是無法引用到orientation和padding屬性的,同理砾肺,在editText {}的lambda表達(dá)式的外部挽霉,也是無法引用到hint和textSize屬性的。因?yàn)閛rientation是LinearLayout類的屬性变汪,而hint和textSize是EditText類的屬性侠坎;這也就說明在這些lambda表達(dá)式的內(nèi)部,持有了一個(gè)對這些類型對象的引用裙盾;而這樣的lambda表達(dá)式就是帶接收者的lambda实胸。

Part 2:深入理解帶接收者的lambda

對象調(diào)用其對應(yīng)的類內(nèi)部的方法,是所有有面向?qū)ο缶幊探?jīng)驗(yàn)的開發(fā)者都知道的原則番官,但這里要講清楚帶接收者的lambda庐完,還是要從這里講起。我們先來看下面的例子:

class A {
    fun function1() {
        function2()
    }
    
    fun function2() {
        // do something...
    }
}

// 擴(kuò)展函數(shù)
fun A.function3() {
    function1()
    function2()
}

fun main(args: Array<String>) {
    val a = A()
    a.function1()
    a.function2()
    a.function3()
}

代碼很基礎(chǔ)徘熔,function1和function2都是A的成員函數(shù)假褪,在function1中可以直接調(diào)用function2,即在同一個(gè)類的方法中可以直接調(diào)用另一個(gè)方法近顷,而在A的外面,我們則需要創(chuàng)建一個(gè)A的對象來調(diào)用function1和function2宁否;因?yàn)樵贏的內(nèi)部窒升,所有的成員(變量/函數(shù))都持有一個(gè)A類型對象的引用,而在A的外部慕匠,在調(diào)用這些成員的時(shí)候饱须,我們需要知道調(diào)用它的到底是哪一個(gè)對象,這是最基本的類和對象之間的關(guān)系台谊,我就不再多說了蓉媳。但在Kotlin中唯一的例外就是擴(kuò)展函數(shù)譬挚,在擴(kuò)展函數(shù)中調(diào)用其接收者的成員函數(shù)(或?qū)傩裕┛梢灾苯诱{(diào)用,這是因?yàn)樵贏的外部調(diào)用它的擴(kuò)展函數(shù)酪呻,需要一個(gè)A的對象减宣。學(xué)過高階函數(shù)和lambda編程后我們都知道,函數(shù)和lambda在很多時(shí)候可以認(rèn)為是同一種東西玩荠,都可以把它們看作是一種有類型的(類型由參數(shù)類型漆腌,數(shù)量,順序以及返回值類型來確定)可被執(zhí)行阶冈,且可以被保存在一個(gè)變量中的代碼段闷尿;所以帶接收者的lambda在某些時(shí)候可以認(rèn)為和擴(kuò)展函數(shù)是等價(jià)的(注意,只是某些時(shí)候女坑,因?yàn)閘ambda和函數(shù)在被編譯成.class字節(jié)碼以后是不同的填具,這是另一個(gè)話題,這里不再展開了)匆骗,假如我們要定義一個(gè)A類型作為接收者類型且一個(gè)Int類型作為參數(shù)劳景,無返回值的帶接收者的lambda,就可以像如下這樣定義:

val receiver: A.(Int) -> Until = {
    // do something...
}

如果我們要調(diào)用執(zhí)行這個(gè)lambda:

val a = A()
a.receiver(3)

所以Part 1中介紹的那些諸如linearLayout {}绰筛,editText {}枢泰,button {}這些函數(shù),都是以一個(gè)帶接收者的lambda作為參數(shù)的普通內(nèi)聯(lián)函數(shù)铝噩,讓我們以editText {}為例來看看它是如何定義的:

inline fun ViewManager.editText(init: (@AnkoViewDslMarker android.widget.EditText).() -> Unit): android.widget.EditText {
    return ankoView(`$$Anko$Factories$Sdk25View`.EDIT_TEXT, theme = 0) { init() }
}

inline fun <T : View> ViewManager.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
    val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)
    val view = factory(ctx)
    view.init()
    AnkoInternals.addView(this, view)
    return view
}

看起來有點(diǎn)復(fù)雜衡蚂,啟示拆開來看其實(shí)很簡單,首先這是一個(gè)擴(kuò)展函數(shù)骏庸,接收者是ViewManager毛甲,這樣就限制了這個(gè)函數(shù)的調(diào)用范圍,即只能在某個(gè)父布局中被調(diào)用具被,隨后我們看到參數(shù)init就是一個(gè)標(biāo)準(zhǔn)的帶接收者的lambda玻募,而init在函數(shù)內(nèi)部調(diào)用ankoView函數(shù)的時(shí)候又會在它的lambda參數(shù)中被調(diào)用,ankoView函數(shù)用來生成一個(gè)EditText對象一姿,至于內(nèi)部的原理七咧,我們不去分析,而editText函數(shù)又會將這個(gè)EditText對象返回叮叹,便于函數(shù)的調(diào)用者獲取這個(gè)對象的引用艾栋;最后我們看到,整個(gè)函數(shù)加了inline修飾符蛉顽,即被聲明成內(nèi)聯(lián)的蝗砾,這樣就保證了DSL API的執(zhí)行效率,而執(zhí)行init這個(gè)帶接收者lambda的ankoView實(shí)際上也是ViewManager的擴(kuò)展函數(shù),而且它也是內(nèi)聯(lián)的悼粮,這里不再做過多的源碼深入闲勺。我們簡單的體驗(yàn)了一下如何聲明一個(gè)DSL API,從Anko來看扣猫,實(shí)際上就是以下三點(diǎn):

  • 1.使用擴(kuò)展函數(shù)來限制函數(shù)的調(diào)用范圍
  • 2.使用帶接收者的lambda來保證API中的嵌套關(guān)系
  • 3.使用inline修飾符菜循,把這些有l(wèi)ambda表達(dá)式作為參數(shù)的函數(shù)聲明成內(nèi)聯(lián)的來保證執(zhí)行效率

我們這里再詳細(xì)說一下第二點(diǎn)。

我們在編寫HTML和XML的時(shí)候苞笨,其中一點(diǎn)非常重要债朵,那就是嵌套關(guān)系;這些嵌套關(guān)系即保證了這些元素之間的包含和被包含的關(guān)系瀑凝,又保證了HTML或XML的可讀性序芦;以使用XML來編寫Android UI為例,如果不使用XML粤咪,而是直接編寫Java代碼的話谚中,也是可行的,但是我們只能使用Java那種從上到下不停new出一個(gè)對象寥枝,然后用對象不停調(diào)用不同方法的辦法來創(chuàng)建UI宪塔,當(dāng)然也是可行的,但是這幾乎可以說是讓代碼的可讀性瞬間歸零囊拜,這樣編寫代碼即容易出錯(cuò)某筐,后期也幾乎不可維護(hù)。但是現(xiàn)在Kotlin有了帶接收者的lambda冠跷,我們可以在保留嵌套關(guān)系的同時(shí)南誊,使用Kotlin這樣的圖靈完備語言來編寫我們需要的UI,這樣就實(shí)現(xiàn)了Part 1中提到的內(nèi)部DSL的全部優(yōu)點(diǎn)蜜托。

Part 3:函數(shù)式的對象——invoke約定

Kotlin的約定有很多種抄囚,而比如使用便捷的get操作,以及重載運(yùn)算符等等橄务,invoke約定也僅僅是一種約定而已幔托;我們可以把lambda表達(dá)式或者函數(shù)直接保存在一個(gè)變量中,然后就像執(zhí)行函數(shù)一樣直接執(zhí)行這個(gè)變量蜂挪,這樣的變量通常聲明的時(shí)候都被我們賦值了已經(jīng)直接定義好的lambda重挑,或者通過成員引用而獲取到的函數(shù);但是別忘了棠涮,在面向?qū)ο缶幊讨性艹郏粋€(gè)對象在通常情況下都有自己對應(yīng)的類,那我們能不能定義一個(gè)類故爵,然后通過構(gòu)造方法來產(chǎn)生一個(gè)對象,然后直接執(zhí)行它呢?這正是invoke約定發(fā)揮作用的地方诬垂。

class A(val str: String) {
    operator fun invoke() {
        println(str)
    }
}

fun main(args: Array<String>) {
    val a = A("Hello")
    a()
}

輸出:Hello

我們只需要在一個(gè)類中使用operator來修飾invoke函數(shù)劲室,這樣的類的對象就可以直接像一個(gè)保存lambda表達(dá)式的變量一樣直接調(diào)用,而調(diào)用后執(zhí)行的函數(shù)就是invoke函數(shù)结窘。

我們還有另一種方式來實(shí)現(xiàn)可調(diào)用的對象很洋,即讓類繼承自函數(shù)類型,然后重寫invoke方法:

class A : (String) -> String {
    override fun invoke(str: String): String {
        println(str)
        return str
    }
}

fun main(args: Array<String>) {
    val a = A("Hello")
    println(a())
}
輸出:Hello
     Hello

直接讓一個(gè)類繼承自函數(shù)類型隧枫,這樣invoke的函數(shù)類型就和繼承的類型一致了喉磁,我們也可以像上面那樣直接調(diào)用A類的對象,最終會執(zhí)行invoke函數(shù)官脓。

使用invoke約定可以構(gòu)建出什么樣的DSL API呢协怒?在Anko中好像還沒有發(fā)現(xiàn)這樣的例子,但是在Gradle的構(gòu)建腳本中這樣的例子就比較常見:

/** 
 * invoke約定的例子
 * 摘自《Kotlin實(shí)戰(zhàn)》P313
 */
dependencies.compile("junit:junit:4.11")

dependiences {
    compile("junit:junit:4.11")
}

dependiences實(shí)際上就是一個(gè)對象卑笨,它既可以直接調(diào)用compile方法孕暇,又能在它的lambda表達(dá)式參數(shù)內(nèi)調(diào)用compile,可見dependiences也是一個(gè)使用了invoke約定的類的對象赤兴,而它接收的是一個(gè)帶接收者的lambda表達(dá)式作為函數(shù)參數(shù)妖滔。

帶接收者的lambda和invoke約定是支撐Kotlin DSL的兩大語法特性,但實(shí)際上在Kotlin中眾多的語法糖中桶良,還有許多特性為你設(shè)計(jì)DSL的優(yōu)雅語法提供了可能座舍,這其中包括了:中輟調(diào)用,運(yùn)算符重載陨帆,括號外的lambda等等等等曲秉;我們不妨充分發(fā)散自己的思維,讓我們使用這些眾多的優(yōu)雅語法構(gòu)建一個(gè)屬于自己的DSL庫歧譬,用來解決編程中某一類特定領(lǐng)域的棘手問題岸浑;Json數(shù)據(jù)格式也是一個(gè)講究嵌套的數(shù)據(jù)格式,我們能否充分發(fā)揮我們的想象來編寫一個(gè)基于DSL的庫瑰步,來對Json做點(diǎn)什么呢矢洲?

Part 4:那些優(yōu)秀的DSL開源庫

下面介紹的Kotlin DSL開源庫都是Kotlin的親爹JetBrain開發(fā)的,這說明缩焦,就目前來看廣大開發(fā)者應(yīng)該還沒有把DSL的潛力發(fā)揮到極致读虏,如果您有其它優(yōu)秀的的DSL庫推薦,可以給文章留言袁滥。

數(shù)據(jù)庫操作:Exposed

Exposed是JetBrain推出的盖桥,可以使用DSL代替SQL來操作數(shù)據(jù)庫的開源庫,項(xiàng)目地址如下:

Exposed

動態(tài)構(gòu)建Android UI:Anko

Anko也是JetBrain推出的题翻,上文已經(jīng)提到過了揩徊;它是一款便于Android開發(fā)者使用Kotlin進(jìn)行Android開發(fā)的函數(shù)庫,其中,使用DSL動態(tài)構(gòu)建Android UI只是其中的一部分功能塑荒,這個(gè)庫的Github地址如下:

Anko

我本人對Anko的實(shí)踐較多熄赡,曾寫過一篇討論Anko的文章;

使用Anko高速動態(tài)構(gòu)建Android UI

我使用Anko編寫過一個(gè)完全使用DSL構(gòu)建UI的App齿税,如果您感興趣彼硫,也可以參考它的源碼,Github地址如下:

HertzDictionary

歡迎star和fork凌箕。

動態(tài)構(gòu)建HTML布局:kotlinx.html

也是JetBrain官方推出的庫拧篮,用來使用DSL來構(gòu)建HTML布局,從它的包名中含有kotlinx就可以看出來牵舱,它的受重視程度高于Anko串绩,基本上屬于Kotlin官方develop kit中的一部分,它的Github地址如下:

kotlinx.html

除此之外仆葡,Gradle已經(jīng)支持使用Kotlin DSL來編寫構(gòu)建腳本赏参,使用Gradle的同學(xué),也不妨立刻開始嘗試沿盅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末把篓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子腰涧,更是在濱河造成了極大的恐慌韧掩,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窖铡,死亡現(xiàn)場離奇詭異疗锐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)费彼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門滑臊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人箍铲,你說我怎么就攤上這事雇卷。” “怎么了颠猴?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵关划,是天一觀的道長。 經(jīng)常有香客問我翘瓮,道長贮折,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任资盅,我火速辦了婚禮调榄,結(jié)果婚禮上踊赠,老公的妹妹穿的比我還像新娘。我一直安慰自己每庆,他們只是感情好臼疫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扣孟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荣赶。 梳的紋絲不亂的頭發(fā)上凤价,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音拔创,去河邊找鬼利诺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛剩燥,可吹牛的內(nèi)容都是我干的慢逾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灭红,長吁一口氣:“原來是場噩夢啊……” “哼侣滩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起变擒,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤君珠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后娇斑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體策添,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年毫缆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唯竹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡苦丁,死狀恐怖浸颓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芬骄,我是刑警寧澤猾愿,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站账阻,受9級特大地震影響蒂秘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淘太,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一姻僧、第九天 我趴在偏房一處隱蔽的房頂上張望规丽。 院中可真熱鬧,春花似錦撇贺、人聲如沸赌莺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艘狭。三九已至,卻和暖如春翠订,著一層夾襖步出監(jiān)牢的瞬間巢音,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工尽超, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留官撼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓似谁,卻偏偏與公主長得像傲绣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子巩踏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容