函數式編程 - 實現響應式框架

前言

函數式響應式編程框架我們應該也用得比較多了,如ReactiveCocoaReactiveX系列(RxSwift、RxKotlin、RxJava),這些框架內部實現都是基于函數式編程的思想來構建的。還記得前不久面試的時候面試官有問道:“有閱讀過ReactiveCocoa的源碼嗎?有沒有看過其中的核心函數bind?你知道這個函數如何實現的嗎?”。在回答這個問題時,如果面試者只是單純的看過RAC源碼,雖能憑自己的印象說出這個方法的大概流程,不過對其中的思想可能也只是一知半解,但如果你充分了解過函數式編程,熟悉Monad概念,就能知道bind方法其實就是Monad概念中的一部分,RAC正是利用Monad來實現它的Signal。此時你就可以向面試官開始你的表演了。

請開始你的表演

如標題所述,在這篇文章中我們將利用函數式編程的思想,去構建一個小型的響應式框架。它具有響應回調的能力,且能將一個個事件數據抽象成管道中流動的流體,我們可以對這些事件數據進行若干的轉換,最后再訂閱它們。

本文為《函數式編程》系列文章中的第三篇,若大家對函數式編程感興趣,可以閱讀系列的前兩篇文章:

原理

函數式響應式的本質是什么

先附上一張流轉換思想的概念圖:

流轉換思想

在日常項目邏輯的構建中,我們總會對一些數據進行轉換運算,這里我們將數據的轉換過程抽象成一條包裹著流動數據的管道,數據以流的形式在這條管道中流通,當經過轉換器時,原始的數據流將會被轉換成新的數據流,然后繼續流動下去。針對數據的轉換運算,我們會使用一些函數/方法,將運算的數據作為實參傳入函數中/對運算的對象調用方法,得到轉換后的結果。此時整個運算將會同步運行,轉換函數接收舊數據進行轉換,成功后返回新的數據。除此之外,你還可以在這個管道中安置多個轉換器,數據在通過若干的轉換器后便轉換成了最終我們所期望的結果值,并從管道中流出。

不過,事實上項目邏輯中也會涉及到許多非同步進行的操作,如某些較為耗時的操作(數據庫操作、網絡請求)、基于事件循環(RunLoop)的事件監聽處理(屏幕觸摸監聽、設備傳感器監聽),這些操作有的會在后臺創建新的線程進行處理,當處理完成后將數據饋回到主線程中,有的則是會在整個運行循環中通過對每一次循環周期從事件隊列中取得需要處理的事件,派發到相應的Handler中。對于這些操作,它們都具有共同點,那就是:數據返回的過程都是通過回調(Callback)來實現的。

對于如何將流轉換的思想用于Callback上,就是函數式響應式所探討解決的問題。

在前不久我有幸參與了中國2017年Swift大會,會議邀請了RxSwift的作者前來演講,在演講中他闡明了RxSwift的本質:

RxSwift just a callback! (RxSwift就是一個回調)

可能這里有人會有疑問:為什么回調不使用一個簡單的代理模式或者一個閉包,反而構建起這么復雜且重量級的框架?因為,這些函數式響應式框架要做的事情就是讓回調結合流轉換的思想,讓開發者只專注于數據的轉換過程而不必多花精力在回調的設計上,輕松寫出簡潔優雅的回調過程。

核心思想

流轉換的思想為將數據事件抽象成管道中流通的流體,用過轉換器轉換成新的數據事件,若加上回調的實現,我們可以說這條管道是建立在回調上的。這時候,我們就可以理清管道和數據的關系:建立在回調上的管道包裹著數據。換句話說,具有回調能力的管道作為一個Context(上下文),包裹著基本的數據值,并且它還擁有某種運算的能力,那就是觸發事件、監聽回調,而這種運算不需要我們去花精力放在上面,我們只想專注于數據的轉換。

看到上面對函數式響應式的描述,你或許也發現了這跟函數式編程里面一個十分重要的概念高度匹配,那就是Monad(單子)。是的,函數式響應式的核心其實就是建立在Monad之上,所以,要實現函數式響應式,我們須構建出一個Monad,可以把它叫做響應式Monad

看過ReactiveCocoa源碼的小伙伴可能知道,RACSignal中具有方法bind和派生類RACReturnSignal,它們就是用來實現Monad中的bindreturn函數,所以,Signal就是一個Monad。不過我們這里需要知道的是,ReactiveCocoa中的bind方法并非完全標準的Monad bind函數,它在參數類型上有所變化,在外表封裝多了一層RACSignalBindBlock,要說最接近Monad bind的,應該就屬RACSignal中的flattenMap方法了(RACSignal的flattenMap方法也是基于bind包裝)。所以,實現了響應式Monad,你就能免費得到flattenMap方法。

因為Monad必定也是一個Functor,所以當你實現一個響應式Monad后,相應的Functor中的map方法你就能很輕易地實現出來了。是的,map方法并非RACSignal所特有的,其也是來自于函數式編程中的Functor

實現

因為個人熱衷于Swift,接下來我將基于Swift語言實現一個簡單的函數式響應式框架。

Event

首先我們來實現Event(事件),像ReactiveCocoaRxSwift中,事件具有三種類型,分別是:

  • next 表示一個數據流元素
  • completed 表示數據流已經完成
  • error 表示數據流中產生了錯誤

這個我實現的事件就簡單一點,它僅具有nexterror類型:

enum Event<E> {
    case next(E)
    case error(Error)
}

Event中的泛型E代表其中數據元素的類型。這里需要注意的是,當事件類型為error時,其關聯的錯誤實例并沒有類型限制,這里為了簡單演示我沒有添加約束錯誤實例的泛型,大家在后面如果嘗試自己去實現的話可以稍作優化,如:

enum Event<E, R> where R: Error {
    case next(E)
    case error(R)
}

Observer

Observer要做的事情有兩個,分別是發送事件以及監聽事件

// MARK: - Protocol - Observer
protocol ObserverType {
    associatedtype E
    
    var action: (Event<E>) -> () { get }
    
    init(_ action: @escaping (Event<E>) -> ())
    
    func send(_ event: Event<E>)
}

extension ObserverType {
    func send(_ event: Event<E>) {
        action(event)
    }

    func sendNext(_ value: E) {
        send(.next(value))
    }

    func sendError(_ error: Error) {
        send(.error(error))
    }
}

// MARK: - Class - Observer
final class Observer<Element>: ObserverType {
    typealias E = Element
    let action: (Event<E>) -> ()

    init(_ action: @escaping (Event<E>) -> ()) {
        self.action = action
    }
}

通過send方法,Observer可以發送出事件,而通過實現一個閉包并將其傳入到Observer的構造器中,我們就可以監聽到Observer發出的事件。

Signal

接下來就是重頭戲:Signal(命名是我從ReactiveCocoa中直接借鑒而來),它就是我們上面所提到的響應式Monad,整個函數式響應式的核心。

我們先來看看SignalType協議:

// MARK: - Protocol - Signal
protocol SignalType {
    associatedtype E
    func subscribe(_ observer: Observer<E>)
}

extension SignalType {
    func subscribe(next: ((E) -> ())? = nil,
                   error: ((Error) -> ())? = nil) {
        let observer = Observer<E> { event in
            switch event {
            case .error(let e):
                error?(e)
            case .next(let element):
                next?(element)
            }
        }
        subscribe(observer)
    }
}

協議聲明了用于訂閱事件的方法subscribe(_:),這個方法接收了一個Observer作為參數,基于此方法我們就可以擴展出專門針對特殊事件類型(next、error)的訂閱方法:subscribe(next:error:)

接下來就是Signal的實現:

// MARK: - Class - Signal
final class Signal<Element>: SignalType {
    typealias E = Element

    private var value: E?
    private var observer: Observer<E>?

    init(value: E) {
        self.value = value
    }

    init(_ creater: (Observer<E>) -> ()) {
        let observer = Observer(action)
        creater(observer)
    }

    func action(_ event: Event<E>) {
        observer?.action(event)
    }

    static func `return`(_ value: E) -> Signal<E> {
        return Signal(value: value)
    }

    func subscribe(_ observer: Observer<E>) {
        if let value = value { observer.sendNext(value) }
        self.observer = observer
    }

    static func pipe() -> (Observer<E>, Signal<E>) {
        var observer: Observer<E>!
        let signal = Signal<E> {
            observer = $0
        }
        return (observer, signal)
    }
}

我們可以看到Signal內部具有一個成員屬性observer,當我們調用subscribe(_:)方法時就將傳入的參數賦予給這個成員。對于另一個成員屬性value,它的作用是為了讓Signal實現Monad return函數,我在《函數式編程》系列文章的前面已經介紹過,Monad return函數就是將一個基本的數據包裹在一個Monad上下文中。所以在Signal中我定義了類方法return(_:),內部調用了針對于value初始化的Signal構造器init(value: E),將一個基本的數據賦予給了value成員屬性。在subscribe(_:)方法的實現中,我們首先對value做非空判斷,若此時value存在,傳入的observer參數將發送關聯了valuenext事件,這樣做是為了保證整個Signal符合Monad特性。

接著到init(_ creater: (Observer<E>) -> ())構造方法,這個方法接受一個閉包,閉包里面做的,就是進行某些運算處理邏輯或事件監聽,如網絡請求、事件監聽等。閉包帶有一個Observer類型的參數,當閉包中的運算處理邏輯完成或者接收到事件回調時,就利用這個Observer發送事件。在這個構造方法實現的內部,我首先將Signal自己的action(_:)方法作為參數傳入Observer的構造器從而創建了一個Observer實例,其中,action(_:)方法做的事情是:指使成員屬性observer將自己接收到的事件參數轉發出去。這里的設計比較巧妙,我們在構造器閉包類型參數creater中進行處理邏輯或事件監聽,若得到結果,將使用閉包中的Observer參數發送事件,事件將會傳遞到訂閱了這個Signal的訂閱者中,從而觸發相關回調。

這里可能有人會有疑惑:為什么需要用兩個observer來傳遞事件?可以在subscribe(_:)方法調用的時候再順便調用creater閉包,把接收到的訂閱者傳入即可。其實,我這么做的目的是為了保證creater的調用跟init(_ creater: (Observer<E>) -> ())同步進行,因為在Signal中我提供了pipe方法。

pipe方法返回一個二元組,第一項為Observer,我們可以利用它來發送事件,第二項為Signal,我們可以通過它來訂閱事件,它就像RxSwift中的Subject,只不過這里我將事件發送者與訂閱者區分開了。這里有一個需要注意的地方:

上面說到,對于我們使用pipe函數獲取到的Observer,其內部的action成員屬性來自于Signalaction(_:)方法,這個方法引用到了Signal中的成員屬性。由此,我們可以推出此時ObserverSignal具有引用的關系,Observer不釋放,Signal也會一直保留。

接下來就是讓Signal實現Monadbind方法了:

// MARK: - Monad - Signal
extension Signal {
    func bind<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> {
        return Signal<O> { [weak self] observer in
            self?.subscribe(next: { element in
                f(element).subscribe(observer)
            }, error: { error in
                observer.sendError(error)
            })
        }
    }

    func flatMap<O>(_ f: @escaping (E) -> Signal<O>) -> Signal<O> {
        return bind(f)
    }

    func map<O>(_ f: @escaping (E) -> O) -> Signal<O> {
        return bind { element in
            return Signal<O>.return(f(element))
        }
    }
}

bind方法接受一個函數作為參數,這個函數的類型為(E) -> Signal<O>E泛型為舊Signal元素中的類型,O則是新Signal元素中的類型,這個bind方法其實跟ReactiveCocoaflattenMap或是RxSwift中的flatMap做的事情一樣,所以在下面的flatMap方法的實現中我只是直接地調用bind方法。很多人俗稱這個過程為降維

bind方法的實現中,我們返回一個新的Signal,為了構造這個Signal,我們使用初始化方法init(_ creater: (Observer<E>) -> ()),在creater閉包中訂閱舊的Signal。倘若舊SignalObserver發出error事件,則直接把error事件中關聯的Error實例提取出來,通過creater閉包中作為參數傳入的Observer包裹起來再傳遞出去;而若是舊SignalObserver發出next事件,則先把next關聯的數據元素提取出來,通過調用bind傳進來的函數,獲取一個中間層的Signal,再通過對這個中間層Signal進行訂閱,將事件傳遞到新的Signal中。

creater閉包中我使用了[weak self]捕獲列表來對舊Signal進行若引用以防止循環引用的發生,為什么這里可能會發生循環引用?上面提到過,Observer會引用Signal,而在creater閉包中舊的Signal將引用新SignalObserver,從而可以推出舊的Signal會對新Signal持引用關系,這里如果不留意的話會造成循環引用。

Monad中的bind方法將自動處理上下文。在Signal中,bind則幫我們自己處理好事件的訂閱、轉移、傳遞,而我們只需要專注于純數據的轉換。

map方法的實現十分簡單,通過在內部調用bind方法,并將最終數據通過return包裹進Signal上下文中,在這里我就不多說了。

以上,我們的響應式Monad就實現完成了!

以上只是非常簡單地實現函數式響應式,目的是為了簡單介紹如何利用函數式編程思想去完成響應式的操作,其中并沒有考慮有關跨線程調度的問題,大家如果有興趣的可以自己嘗試去進行相關優化。

下面我們來測試使用一下。

簡單使用

通過creater閉包構建Signal

let mSignal: Signal<Int> = Signal { observer in
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        observer.sendNext(1)
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        observer.sendNext(2)
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        observer.sendNext(3)

    }
}

mSignal.map { $0 + 1 }.map { $0 * 3 }.map { "The number is \($0)" }.subscribe(next: { numString in
    print(numString)
})

輸出:

The number is 6
The number is 9
The number is 12

通過pipe構建Signal

let (mObserver, mSignal) = Signal<Int>.pipe()

mSignal.map { $0 * 3 }.map { $0 + 1 }.map { "The value is \($0)" }.subscribe(next: { value in
    print(value)
})

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    mObserver.sendNext(3)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    mObserver.sendNext(2)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    mObserver.sendNext(1)
}

輸出:

The value is 10
The value is 7
The value is 4

擴展

接下來我們對剛剛實現的函數式響應式進行擴展,關聯一些平時我們常用到的類。

UIControl

UIControl的觸發事件進行監聽,傳統的做法是通過調用addTarget(_:, action:, for:)方法,傳入target以及一個回調函數Selector。很多人比較厭倦這種方法,覺得每次監聽事件都需要定義一個事件處理函數,比較麻煩,希望能直接通過閉包回調事件觸發。

這里只需簡單地封裝一下即可滿足這種需求:

final class ControlTarget: NSObject {
    private let _callback: (UIControl) -> ()

    init(control: UIControl, events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
        _callback = callback
        super.init()
        control.addTarget(self, action: #selector(ControlTarget._handle(control:)), for: events)
    }

    @objc private func _handle(control: UIControl) {
        _callback(control)
    }
}

fileprivate var targetsKey: UInt8 = 23
extension UIControl {
    func on(events: UIControlEvents, callback: @escaping (UIControl) -> ()) {
        var targets = objc_getAssociatedObject(self, &targetsKey) as? [UInt: ControlTarget] ?? [:]
        targets[events.rawValue] = ControlTarget(control: self, events: events, callback: callback)
        objc_setAssociatedObject(self, &targetsKey, targets, .OBJC_ASSOCIATION_RETAIN)
    }
}

在這里我間接利用ControlTarget對象來將UIControl事件觸發傳遞到閉包中,并通過關聯對象來使得UIControl保持對ControlTarget的引用,以防止其被自動釋放。經過上面簡單的封裝后,我們就能很方面地利用閉包監聽UIControl的事件回調:

button.on(events: .touchUpInside) { button in
    print("\(button) - TouchUpInside")
}
button.on(events: .touchUpOutside) { button in
    print("\(button) - TouchUpOutside")
}

由此,我們可以簡單地基于上面的封裝來擴展我們的函數式響應式:

extension UIControl {
    func trigger(events: UIControlEvents) -> Signal<UIControl> {
        return Signal { [weak self] observer in
            self?.on(events: events, callback: { control in
                observer.sendNext(control)
            })
        }
    }

    var tap: Signal<()> {
        return trigger(events: .touchUpInside).map { _ in () }
    }
}

trigger(events:)方法傳入一個需要進行監聽的事件類型,返回一個Signal,當對應的事件觸發時,Signal中則會發射出事件。而tap返回的則是針對TouchUpInside事件觸發的Signal

使用起來跟RxSwiftReactiveCocoa一樣,十分簡潔優雅:

button.tap.map { _ in "Tap~" }.subscribe(next: { message in
    print(message)
})

上面整個過程的引用關系為: UIControl -> ControlTarget -> _callback -> Observer -> Signal,由此我們知道,只要保持對UIControl的引用,那么其所關聯的事件監聽Signal則不會被自動釋放,可以在整個RunLoop中持續工作,


NotificationCenter

將函數式響應式適配控制中心,方法跟上面對UIControl的擴展一樣,通過一個中間層NotificationObserver來做事件的傳遞轉發:

final class NotificationObserver: NSObject {
    private unowned let _center: NotificationCenter
    private let _callback: (Notification) -> ()

    init(center: NotificationCenter, name: Notification.Name, object: Any?, callback: @escaping (Notification) -> ()) {
        _center = center
        _callback = callback
        super.init()
        center.addObserver(self, selector: #selector(NotificationObserver._handle(notification:)), name: name, object: object)
    }

    @objc private func _handle(notification: Notification) {
        _callback(notification)
    }

    deinit {
        _center.removeObserver(self)
    }
}

fileprivate var observersKey: UInt = 78
extension NotificationCenter {
    func callback(_ name: Notification.Name, object: Any?, callback: @escaping (Notification) -> ()) {
        var observers = objc_getAssociatedObject(self, &observersKey) as? [String: NotificationObserver] ?? [:]
        observers[name.rawValue] = NotificationObserver(center: self, name: name, object: object, callback: callback)
        objc_setAssociatedObject(self, &observersKey, observers, .OBJC_ASSOCIATION_RETAIN)
    }

    func listen(_ name: Notification.Name, object: Any?) -> Signal<Notification> {
        // Warning: 注意object可能對返回的Signal進行引用,從而造成循環引用
        return Signal { [weak self] observer in
            self?.callback(name, object: object, callback: { notification in
                observer.sendNext(notification)
            })
        }
    }
}

由此,我們可以基于上面對NotificationCenter的響應式擴展,來完成對UITextFiled文字變化的監聽:

extension UITextField {
    var listen: Signal<String?> {
        return NotificationCenter.default.listen(.UITextFieldTextDidChange, object: self).map { $0.object as? UITextField }.map { $0?.text }
    }
}

// 使用
textField.listen.map { "Input: \($0 ?? "")" }.subscribe(next: {
    print($0)
})

方法調用監聽 / 代理調用監聽

我們有時候想監聽某個對象中指定方法的調用,來實現面向切面編程或者埋點,另外,當函數式響應式被引入后,我們希望它能充當代理的職責,監聽代理方法的調用。為此我們可以通過對函數式響應式進行擴展來支持上面的需求。不過要做這件事情并不簡單,這里面要涉及多種Runtime特性,如方法交換、方法動態派發、isa交換等Runtime黑科技,要實踐它可能需要投入較大精力,花費較長時間。因本人能力與時間有限,沒有去編寫相應的代碼,若大家有興趣可以嘗試一下,而后期如果我做了相關的努力,也會公布出來。

為什么沒有Disposable

若我們接觸過RxSwiftReactiveSwift,我們會發現每次我們訂閱完一個Observable或者Signal后,會得到訂閱方法返回的一個專門用于回收資源的實例,比如RxSwift中的Disposable,我們可以通過在某個時機調用它的dispose方法,或者將其放入一個DisposeBag中來使得資源在最后得到充分的回收。

再來看回我們在上面實現的響應式框架,因為這個框架的實現非常簡單,并不會在訂閱后返回一個專門提供給我們釋放資源的實例,所以我們在使用它的時候要密切留意資源的存活與釋放問題。這里舉一個例子:

在上面,我們對函數式響應式進行針對UIControl的適配時,是通過一個中間層ControlTarget來完成的,為了保持這個ControlTarget實例的存活,使得它不會被自動釋放,我們先用一個集合來包裹住它,并將這個集合設置為目標UIControl的關聯對象。此時我們可以將這個中間層ControlTarget看做是這個事件流管道中的一個資源,這個資源的銷毀是由目標UIControl來決定的。

對于RxSwift來說,它實現對UIControl的擴展原理跟我們寫的差不多,也是通過一個中間層來完成,但是對于中間層資源的保活與銷毀,它采用的是另一種方法,我們可以看下這段RxSwift的源碼(為了簡單,刪掉了一些無關的代碼):

class RxTarget {
    private var retainSelf: RxTarget?

    init() {
        self.retainSelf = self
    }

    func dispose() {
        self.retainSelf = nil
    }
}

這個類型的保活方式十分巧妙,它利用自己對自己的循環引用來使得維持生存,而當調用dispose方法時,它將解開對自己的循環引用,從而將自己銷毀。

通過上面兩個例子的對比,我們可以知道,對于我們自己實現的響應式框架,我們需要把某些精力放在對資源的保活與釋放上,而像RxSwift,它則提供一個統一的資源管理方式,相比起來更加清晰優雅,大家有興趣可以實現一下這種方式。

相關鏈接

Github - ReactiveObjc
Github - ReactiveCocoa
Github - RxSwift

本文純屬個人見解,若大家發現文章部分有誤,歡迎在評論區提出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容