從零學習Swift 16: 函數式編程

Swift語言是支持函數式編程的,所以我們需要簡單了解一下函數式編程的概念.

在了解函數式編程的概念之前呢,先看看SwfitArray常用的幾個方法,因為這幾個方法在設計上都是按照函數式編程的規范去設計的.

Array的常見用法:
1.array.map 映射

public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

map方法會傳入一個閉包,并且返回一個數組.這個閉包會接收一個參數,并且返回一個泛型.參數就是遍歷數組中的每一個元素.返回值就是將數組元素映射成一個新值.


var arr1 = [1,2,3,4]
var arr2 = arr1.map {
    i in // i 就是數組元素1,2,3,4
    i * 2  //對數組元素 x2 ,并且放到一個新數組中
}

//簡寫
var arr3 = arr1.map { $0 * 2 }

map傳入的是一個閉包,也就是一個函數,所以我們可以直接傳入一個函數:


func double(_ num: Int) -> Int{
    num * 2
}

var arr4 = arr1.map(double)

2.array.filter 過濾

func filter(_ isIncluded: (T) throws -> Bool) rethrows -> [T]

filter傳入一個閉包參數,返回一個新數組.這個閉包會接收一個參數,此參數是遍歷數組中的每一個元素.返回值是一個Bool值,為true表示將元素放入新數組,false表示不放入新數組.


var arr1 = [1,2,3,4]

var arr2 = arr1.filter { (i) -> Bool in
    i % 2 == 0
}

//簡寫
var arr3 = arr1.filter{ $0 % 2 == 0}

3. array.reduce

public func reduce<T>(_ initialResult: T, _ nextPartialResult: (T, Output) -> T) -> Result<T, Just<Output>.Failure>.Publisher

reduce會遍歷數組中的每一個元素,我們拿到數組元素后可以進行相應的操作,并且操作的結果會帶到下一次遍歷中.

參數解析:
initialResult : T: 初始化參數,我們可以傳入一個任意類型的值,但要和數組元素類型相匹配.

_ nextPartialResult: (T, Output) -> T): 閉包.這個閉包會接收兩個參數,第一個參數是上一次遍歷的結果,如果是第一次遍歷則是初始化參數;第二個參數是遍歷數組的每一個元素.


var arr1 = [1,2,3,4]

var num = arr1.reduce(0) { (n, i) -> Int in
    print("- ", i)
    return n + i
}
//打印結果是 0 + 1 + 2 + 3 + 4 = 10
print(num)


//簡寫

var num2 = arr1.reduce(0){$0 + $1}

規則
mapflatMap

map的功能就是把數組元素通過一定規則映射為另一個元素.
那么flatMap呢?

我們看看下面代碼:


var arr = [1,2,3,4]

var arr1 = arr.map {
    i in
    Array.init(repeating: i, count: i)
}

print("arr1 - " , arr1)

var arr2 = arr.flatMap {
    i in
    Array.init(repeating: i, count: i)
}

print("arr22 - " , arr2)

運行結果如下:

運行結果

可以看到通過map映射后的數組里面存放了4個數組;而通過flatMap映射的后的數組還是一個數組.所以他們區別就很明顯了.

我們看看flatMap的方法聲明:


public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

可以看到flatMap返回值里面放的是閉包返回值的Element,也就是取出元素.而map的返回值是不管閉包返回值是什么,直接放到數組中.

mapcompactMap

compactMap:遍歷數組的每個元素,返回一個不包含nil,不包含可選項的新數組(也就是說如果是可選值,會解包后放入新數組).


var arr = ["1","a","3","b"]

var arr1 = arr.map {
    i in
    Int(i)
}

print("arr1 -: ",arr1)

var arr2 = arr.compactMap {
    i in
    Int(i)
}

print("arr2 -: ",arr2)

打印結果:

使用reduce實現map , filter功能:

我們還可以使用reduce實現和map,filter同樣的功能.

reduce實現map:

var arr = [1,2,3,4]

var arr2 = arr.map { i in
    i * 2
}

var arr3 =  arr.reduce([]) { (n, i) -> [Int] in
    n + [i * 2]
}

print("arr2 - ", arr2)
print("arr3 - ", arr3)

reduce實現filter:

var arr = [1,2,3,4]

var arr2 = arr.filter {
    i in
    i % 2 == 0
}

var arr3 = arr.reduce([]) { (n, i) -> [Int] in
    i % 2 == 0 ? n + [i] : n
}

print("arr2 -: ",arr2)
print("arr3 -: ",arr3)

lazy的優化:

我們看下面代碼:


var arr = [1,2,3]

var arr2 = arr.map {
    i -> Int in
    print("mapping \(i)")
    return i * 2
}

print(arr2)

一運行程序,arr的所有元素都會被映射成新的元素[2,4,6],即使我們沒有用到數組arr2.這樣肯定是不合理的,如果說arr中有很多元素,并且映射過程也比較復雜,那么就會造成額外的開銷.

所以swift添加了一個lazy方法,等用到某個元素的時候才會去映射:


var arr = [1,2,3]

var arr2 = arr.lazy.map {
    i -> Int in
    print("mapping \(i)")
    return i * 2
}



print("--- start map ----")
    
print("mapped \(arr2[0])")
print("mapped \(arr2[1])")
print("mapped \(arr2[2])")
    
print("--- end map ----")


optionalmap

var num1: Int? = 10

var num2 = num1.map { $0 * 2 }

print(num2)


var num3: Int? = nil
var num4 = num3.map { $0 * 2 }

print(num4)

上面的代碼,可選類型在進行map時,map會先判斷可選類型是否為nil,如果為nil就直接返回nil,根本不會調用閉包;如果不為nil,才會調用閉包,并且映射的結果包裝成可選項返回.

所以下面兩行代碼是等價的:


var num1: Int? = 10

var num2 = num1.map { $0 * 2 }

var num3 = num1 != nil ? (num1! * 2) : nil

所以,只要涉及到判斷可選項是否為nil的操作都可以使用map:

示例一: 字符串拼接變量


var num: Int? = 10

var text1 = num != nil ? "num is \(num!)" : "no num"
print(text1) // num is 10

var text2 = num.map{"num is \($0)"} ?? "no num"
print(text2) // num is 10

示例二: 通過姓名從數組中找到某個人


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

var persons = [
Person(name: "張三", age: 18),
Person(name: "李四", age: 18),
Person(name: "王五", age: 18),
]

//原始方法
func findPersonWithName1(_ name: String?) -> Person?{
    let index = persons.firstIndex { $0.name == name }
    
    return index != nil ? persons[index!] : nil
}

var p1 = findPersonWithName1("王五")

print(p1!)

//使用 map
 
func findPersonWithName2(_ name: String?) -> Person?{
    persons.firstIndex { $0.name == name }.map { persons[$0] }
}

var p2 = findPersonWithName2("王五")

print(p2!)


Optionalmap 和 flatMap的區別:

var num1: Int? = 10

var num2 = num1.map { Optional.some($0 * 2)}

print(num2) //Optional(Optional(20))

var num3 = num1.flatMap { Optional.some($0 * 2)}

print(num3) //Optional(20)

flatMapmap的功能是一樣的,只不過flatMap如果發現映射的結果本身就是可選項類型,那么它就不會再封裝一層可選項;而map不管結果是什么都會再封裝成可選類型.

知道了mapflatMap的區別.我們看看flatMap具體有什么作用:

示例一: 字符串轉Date


var dateFmt = DateFormatter()
dateFmt.dateFormat = "yyyy-MM-dd"
var dateStr: String? = "2020-07-02"

var date1 = dateStr != nil ? dateFmt.date(from: dateStr!) : nil

print(date1!)

如果要把一個字符串轉成Date日期類型,按照之前做法就像上面那樣.
使用flatMap也能實現,并且更簡潔:


var date2 = dateStr.flatMap {dateFmt.date(from: $0)
}

//由于 dateStr.flatMap 要求傳入一個 String -> T?  的閉包
//剛好 dateFmt.date() 就是傳入一個 String 返回一個 Date
//完全符合,所以我們可以直接傳入 dateFmt.date 函數進去

var date3 = dateStr.flatMap(dateFmt.date)

為什么這里要用flatMap而不是map呢?
因為 dateFmt.date() 返回的是一個可選項類型 Date? , 所以 flatMap 不會對返回結果再進行一次可選項包裝

示例二: 字典轉模型


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


func dic2Model(_ dic: [String: Any]) -> Person?{
    guard let name = dic["name"] as? String,
        let age = dic["age"] as? Int else {
        return nil
    }
    return Person(name: name, age: age)
}

var json: Dictionary? = ["name": "張三", "age" : 18]

var p1 = json != nil ? dic2Model(json!) : nil

//dic2Model返回的是可選項類型,flatMap 不會再封裝一層可選項
var p2 = json.flatMap { dic2Model($0) }

print(p1)
print(p2)


函數式編程:

比如說現在有這樣一個需求,用函數實現這樣的運算[(3 + 8) * 7 / 2] - 1

大家首先想到的肯定會這樣做:


func add(_ v1: Int, _ v2: Int) -> Int{
    v1 + v2
}

func sub(_ v1: Int, _ v2: Int) -> Int{
    v1 - v2
}

func multiple(_ v1: Int, _ v2: Int) -> Int{
    v1 * v2
}

func divide(_ v1: Int, _ v2: Int) -> Int{
    v1 / v2
}




print(sub(divide(multiple(add(3, 8), 7), 2), 1))

定義4個方法,分別是+ - * /,這樣的確可以滿足需求,但是在調用方法時代碼的可讀性很差,讓人看不明白.如果用函數式編程,就會使代碼更直觀更易懂.

我們對上面代碼進行兩步改造,實現函數式編程.

第一步: 把上面接受兩個參數的函數,升級為只接受一個參數,返回一個函數:

比如對add函數的升級:


func add(_ v1: Int, _ v2: Int) -> Int{
    v1 + v2
}

升級為:


func add(_ v1: Int) -> (Int) -> Int{
  {
    (v2: Int) -> Int in
    v2 + v1
      }
}

現在升級后的函數同樣可以實現+運算:


print(add(2)(3)) //5

如上圖所示,2是函數add的參數,3是閉包的參數,在閉包體內將他們相加,并返回一個閉包.

所以可以簡化如下:


func add(_ v1: Int) -> (Int) -> Int{{ $0 + v1 }}

依次對其他方法簡化:


func add(_ v1: Int) -> (Int) -> Int{{ $0 + v1 }}
func sub(_ v1: Int) -> (Int) -> Int{{ $0 - v1 }}
func multiple(_ v1: Int) -> (Int) -> Int{{ $0 * v1 }}
func divide(_ v1: Int) -> (Int) -> Int{{ $0 / v1 }}


//  [(3 + 8) * 7 / 2] - 1
print(sub(1)(divide(2)(multiple(7)(add(8)(3)))))

可以看到簡化后的函數可讀性還是很差.所以我們還要進行升級.

第二步: 函數合成.把兩個函數組合成一個函數.上一個函數的返回值作為下一個函數的參數:


func compose(_ f1: @escaping (Int) -> Int, _ f2: @escaping (Int) -> Int) -> (Int) -> Int{
{
    (v1: Int) -> Int in
    f2(f1(v1))
    }
}


var fn = compose(add(8), multiple(7))

print(fn(3))

上面的compose函數就實現了把add , multiple兩個函數組合成一個函數并返回.實現了( 3 + 8) * 7的效果.

可能有人到這里有點懵,它到底是怎么做到的呢?

我們好好梳理一下:

首先compose會返回一個函數fn,我們調用fn(3),就是把這個3作為參數傳遞給了v1,compose的兩個參數分別是add(8) , multiple(7).在compose返回值的閉包里會先進行f1(v1)運算,也就是3 + 8.然后把結果作為參數傳遞給了f2,也就是mutiple(7)(11).所以最后的結果是77.

所以現在compose函數能進行( 3 + 8 ) * 5運算,也就是能連接兩個運算符.那我們完全可以把compose函數定義為運算符,比如這樣:


infix operator >>> : AdditionPrecedence
func >>>(_ f1: @escaping (Int) -> Int, _ f2: @escaping (Int) -> Int) -> (Int) -> Int{
{
    (v1: Int) -> Int in
    f2(f1(v1))
    }
}

這樣就定義了一個像 + 一樣的運算符,我們可以把一連串的運算組合起來:


// [(3 + 8) * 7 / 2] - 1
var fn = add(8) >>> multiple(7) >>> divide(2) >>> sub(1)

print(fn(3))

并且非常的容易看懂,符合我們的運算習慣.先 + 8,然后 x7,然后 ?2 ,最后 - 1.

但是這樣還不夠通用,因為現在只適用于Int類型,所以我們要使用泛型,讓這個運算符更加通用:

如上圖所示,紅色部分是運算符>>>的入口,它和f1的參數是同一類型;
綠色部分是f1的返回值,同時也是f2的參數,所以它倆是同一類型;
藍色部分是f2的返回值,同時也是運算符的返回值,所以它倆是同一類型.

所以最后結果就是下面這樣,最終的目的是從AC:


func >>><A,B,C>(_ f1: @escaping (A) -> B,
         _ f2: @escaping (B) -> C) -> (A) -> C{
{
    (v1: A) -> C in
    f2(f1(v1))
    }
}


柯里化

柯里化是函數編程中很重要的一個概念.我們先來看看什么是柯里化.

柯里化的定義:將一個接收多個參數的函數轉變為一系列只接受一個參數的函數

很明顯我們上面的運算操作就是將一個函數柯里化.

下面我們再練習一下分別將2個參數,3個參數的函數柯里化:

2個參數柯里化:


//兩個參數柯里化
func add(_ v1: Int,_ v2: Int) -> Int{
    v1 + v2
}

//柯里化后
func curringAdd(_ v1: Int) -> (Int) -> Int{
      {$0 + v1}
}

3個參數柯里化:


//三個參數柯里化
func sub(_ v1: Int,_ v2: Int, _ v3: Int) -> Int{
    v1 - v2 - v3
}

//柯里化后
func curringSub(_ v1: Int) -> (Int) -> (Int) -> Int{
    return{
        (v2) in
        return{
            (v3) in
            return v1 - v2 - v3
        }
    }
}

但是上面的柯里化函數只支持Int類型,如果我們想要通用其他類型,需要讓他們支持泛型.這樣我們就能把任何類型的函數自動柯里化.

將2個參數的柯里化函數泛型化:


func generics2arguments<A,B,C>(_ fn: @escaping (A,B) -> C) -> (A) -> (B) -> C{
    return{
        a in
        return{
            b in
            return fn(a,b)
            
        }
    }
}

print(generics2arguments(add)(10)(20))

將3個參數的柯里化函數泛型化:


func generics2arguments<A,B,C,D>(_ fn: @escaping (A,B,C) -> D) -> (A) -> (B) -> (C) -> D{
    return{
        a in
        return{
            b in
            return{
                c in
                fn(a,b,c)
            }
        }
    }
}

print(curringSub(10)(20)(30))

這樣我們就可以隨便傳入2個參數,3個參數的函數,然后自動將函數柯里化.

還可以將自動柯里化函數重載成運算符,使用的時候更方便:


prefix func ~<A,B,C,D>(_ fn: @escaping (A,B,C) -> D) -> (A) -> (B) -> (C) -> D{
    return{
        a in
        return{
            b in
            return{
                c in
                fn(a,b,c)
            }
        }
    }
}

這樣即使+ - x /傳統的寫法,也可以直接柯里化后參與運算:


//  [(3 + 8) * 7 / 2] - 1

//傳統寫法,沒有柯里化
func add(_ v1: Int, _ v2: Int) -> Int{
    v1 + v2
}

func sub(_ v1: Int, _ v2: Int) -> Int{
    v1 - v2
}

func multiple(_ v1: Int, _ v2: Int) -> Int{
    v1 * v2
}

func divide(_ v1: Int, _ v2: Int) -> Int{
    v1 / v2
}

prefix func ~<A,B,C>(_ fn: @escaping (A,B) -> C) -> (B) -> (A) -> C{
    return{
        b in
        return{
            a in
            return fn(a,b)
        }
    }
}



infix operator >>> : AdditionPrecedence
func >>><A,B,C>(_ f1: @escaping (A) -> B,
         _ f2: @escaping (B) -> C) -> (A) -> C{
{
    (v1: A) -> C in
    f2(f1(v1))
    }
}

//直接使用 ~ 把傳統方法自動柯里化后參與運算
var fn = (~add)(8) >>> (~multiple)(7) >>> (~divide)(2) >>> (~sub)(1)

print(fn(3))

函數式編程中常用的概念:
1. 高階函數 Higher-Order Function

高階函數是至少滿足下列一個條件的函數:
1. 接收一個或者多個函數作為參數
2. 返回一個函數

通過高階函數的定義可以看出,上面的map , flatMap , reduce等都是高階函數,它們都是接收一個函數作為參數.

2. 柯里化 Currying

柯里化就是將一個接收多個參數的函數變成一系列只接受單個參數的函數.
柯里化的本質就是將一個接收多個參數的函數變成只接收一個參數,并且返回一個接收參數的閉包.通過返回的閉包達到接收多個參數的目的.

3. 函子 Functor

我們將支持map運算的數據類型稱為函子.

上面分析過map函數,map就是把數組中的值或者可選項包裝的值,映射成另一個值后,然后再返回數組或者可選項,也就是返回他本身的數據類型.

如圖:


// Array<Element>
func map<T>(_ transform: (Element) -> T) -> Array<T>


// Optional<Wrapped>
func map<U>(_ transform: (Wrapped) -> U) -> Optional<U>

Optionalmap流程圖:

Optional

Arraymap流程圖:

Array
4. 適用函子 Applicative Functor

對于任何一個函子,如果能支持以下運算,那么它就是一個適用函子:


//傳入任意類型,最后都能返回函子的數據類型.
func pure<A>(_ value: A) -> F<A>{
    value
}


//參數一: 泛型函數 fn
//參數二: 函子
//功能: 把參數 value 傳入 函數 fn , 最后得到一個 B,并且把 B ,包裝成函子原本的數據類型
func <*><A,B>(_ fn: F<(A) -> B>, value: F<A>) -> F<B>


第一種pure運算,ArrayOptional都支持,因為我們傳入任意類型,都能放到OptionalArray中.

第二種運算OptionalArray也同樣支持.
我們首先看一下Optional實現這種運算:


infix operator <*>: AdditionPrecedence
func <*><A,B>(_ fn: ((A) -> B)?, value: A?) -> B?{
    guard let f = fn, let num = value else {return nil}
    return f(num)
}


var num: Int? = 10
var fn: ((Int) -> Int)? = { $0 * 2 }

var result = fn <*> num
print(result) // Optional(20)

再使用Array實現這種運算:


infix operator <*>: AdditionPrecedence
func <*><A,B>(_ fns: [((A) -> B)], values: [A]) -> [B]{
    var arr: [B] = []
    for i in fns.startIndex ..< fns.endIndex{
        arr.append(fns[i](values[I]))
    }
    return arr
}

var fns = [{$0 + 1},{$0 + 2},{$0 + 3}]
var nums = [1,1,1]

let results = fns <*> nums

print(results) //[2, 3, 4]

ArrayOptional不同的是,Array是吧一些列的算法包裝起來存放到數組中,然后分別取出每一個算法和每一個值進行運算,最后再把每一次的運算結果存放到數組中.

如圖所示:

Array 是把 +3 操作封裝起來
5. 單子 Monad

對于任意數據類型,如果支持以下運算,那么就可以稱為是一個單子:


func pure<A>(_ value: A) -> F<A>

func flatMap<A,B>(_ value: F<A>,fn:((A) -> F<B>)) -> F<B>

ArrayOptional支不支持這兩種運算,它們是不是一個單子呢?

因為swift官方文檔中已經為ArrayOptional提供了flatMap的實現,我們看看官方實現:

Array.flatMap的官方實現:


public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

我們可以對其簡化如下:


public func flatMap(_ transform: (A)  -> Array<B>)  -> Array<B>

官方實現簡化后的代碼和上面的算法已經很相似了,但是還少了一個參數,其實這個參數就是我們調用flatMap的對象,因為上面的算法規則并不是面向對象的,所以我們可以把調用flatMap的對象當做參數補充進去,如下:


public func flatMap(value: Array<A> , _ transform: (A)  -> Array<B>)  -> Array<B>

同樣,對OptionalflatMap進行簡化:


//官方API
@inlinable public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

//簡化后
public func flatMap<A,B>(value: Optional<A>_ transform: (A) throws -> Optional<B>)  -> Optional<B>

可以看到,和運算規則是一樣的.

所以,ArrayOptional也是單子.

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