WWDC2015 -- protocol-oriented programming in swift(Session 408)(面向协议编程)

标签(空格分隔): WWDC


Classes Are Awesome

  • 封装
  • 访问控制
  • 抽象
  • 命名空间
  • 语法表达
  • 延伸性

Type Are Awesome

  • 访问控制
  • 抽象
  • 命名空间

这些是让程序员管理复杂事件的要素。
但是用structs与enums可以实现以上功能。


Three Beefs about Class

1.Implicit Sharing


当A与B同时指向一个对象的时候,经常会发生错误。

  1. 为了减少在代码中的错误疯狂的使用copy。
  2. 使用过多的copy带来的性能上的影响。
  3. 当使用Dispatch_queue时,提供了一个比赛场景,因为线程共享了一个的可变状态。
  4. 所以你需要为了保护你的常量加上lock。
  5. 这个lock使得性能更加低下。
  6. 甚至造成死锁。
  7. 产生Bug!

NOTE
It is not safe to modify a mutable collection while enumerating through it. Some enumerators may currently allow enumeration of a collection that is modified, but this behavior is not guaranteed to be supported in the future.

官方的说明。
但是这种情况不会在Swift上出现,因为Swift的所有集合都是值类型。


2.Class Inheritance

  1. 需要正确选择一个好的父类。
  2. 单一的继承,得到父类中所有的信息。
  3. 必须在Class创建时继承,而不是在之后拓展。
  4. 如果父类储存了许多属性。
  5. 子类也必须要储存属性。
  6. 初始化负担加重。
  7. 注意不能修改父类中的常量。
  8. ovewride时,需要知道是什么方法,或者怎么去写(什么时候不能使用override)。

3.Lost Type Relationships

class Ordered {
  func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}

首先类必须实现方法,不然会报错。

func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
  var lo = 0, hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
return lo }

这是一个二叉树的搜索方法,类型为Order。

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < other.value
  }
}

class Label : Ordered { var text: String = "" ... } 

现在创建一个Number类,以及一个Label类,他们都集成了Ordered。
在Number类中重载使用方法precedes不能使用子类参数value。
因为这里不能不是所有的Ordered的子类都会有value属性。

所以需要改为

  override func precedes(other: Ordered) -> Bool 
  {   
  return value < (other as! Number).value 
  } 

这样的问题来自于Class之间对于自身和其他类的类型并没有建立关系。
类型关系的缺失经常是由于抽象地使用类。


一个更好的抽象机制

支持值类型 (and classes)
支持静态类型关系 (和动态调度) 非完全统一的
支持逆袭建模
不在模型上引入实例数据
不在模型上引入初始化负担
使实现的内容更加清晰

这不就是Protocal的优点吗?


用Protocal开始编码

首先将上面的代码进行转换

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
struct Number : Ordered {
  var value: Double = 0
  func precedes(other: Ordered) -> Bool {
    return self.value < (other as! Number).value
  }
}

这样就再也不需要一个基类,实现方法的时候也不需要override,同时不希望number作为一个类去使用,改成了struct类型。

 func precedes(other: Number) -> Bool 
 {    
 return self.value < other.value  
 } 

此时需要解决潜在的静态类型的安全漏洞,因为other参数可能是另外的类型,所以现在设置为Number类型,这样就不需要再做类型判断,但是这样又与协议中的Ordered类型相互矛盾。于是我们现在将Ordered中的类型设置为self。

protocol Ordered {
  func precedes(other: Self) -> Bool
}
struct Number : Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
    return self.value < other.value
  }
}

这种设计叫做Self-requirement,当你在protocol中看到self时,它是作为一个遵守了这种模型类型的占位符。所以现在代码也变得有效了。


func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int 
{   var lo = 0  
    var hi = sortedKeys.count   
    while hi > lo { 
    let mid = lo + (hi - lo) / 2    
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }   
    else { hi = mid } 
}  
return lo 
} 

回到刚才的二叉树搜索方法上,这种方法在Ordered是Class的时候是有效的,在protocol中加入Self前也是有效的,现在加入self后会报错,protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements。本来我们可以处理由numbers和label组成的[Ordered]数组,但是现在编译器要求我们的类型一致,就像这样:

func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int {
  var lo = 0
  var hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
return lo }

这么处理后,只能对单一的Ordered类型处理,看起来特别严格,好像缺失了很多灵活性。但是想想看,对于不同类型同时存在的处理我们往往是去阻止,而不是真正意义上的处理。事实上,单一类型的数组才是我们想要的东西。


有无Protocols的两个世界

Without Self Requirement With Self Requirement
func precedes(other: Ordered) -> Bool func precedes(other: Self) -> Bool
作为一种类型使用 只能作为一种泛型约束使用
func sort(inout a: [Ordered]) func sort<T : Ordered>(inout a: [T])
Think “多样化” Think “单一化”
每一个模型都与其他类型有关联 模型在交互上是自由的
动态调度 静态调度
有更少的优化度 有更多的优化度

挑战!将使用Class改为使用Protocol

准备

首先设定一个struct类型的Renderer,用print方式直观实现方法。

struct Renderer { 
func moveTo(p: CGPoint){ print("moveTo(\(p.x), \(p.y))") }    
func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") }    
func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)     {    
print("arcAt(\(center), radius: \(radius),"+" startAngle: \(startAngle), endAngle: \(endAngle))")   } 
} 

然后创建一个Drawable协议为我们的元素提供一个共同的接口。

protocol Drawable {   func draw(renderer: Renderer) } 

创建一个Polygon形状,因为是值类型,所以使用了struct,并包含了一个点的数组。通过调用renderer的方法,遍历数组中所有的点画出形状。

struct Polygon : Drawable { 
func draw(renderer: Renderer) 
    {   
    renderer.moveTo(corners.last!)    
    for p in corners 
            {    
                renderer.lineTo(p)    
            }  
    }  
var corners: [CGPoint] = [] 
}

同理,创建一个Circle。需要中心点和半径。也通过调用renderer的方法。

struct Circle : Drawable { 
    func draw(renderer: Renderer) 
    { 
    renderer.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)  
    }  
    var center: CGPoint  
    var radius: CGFloat
} 

最后创建一个Diagram,其中的elements类型是Drawable。因为所有的Darwable都是值类型,所以这里的Darwable类型的数组也是值类型。

struct Diagram : Drawable { 
    func draw(renderer: Renderer) {    
    for f in elements
         {     
    f.draw(renderer)  
         }   
    }  
    var elements: [Drawable] = [] 
}

根据以上内容,我们可以进行测试。

var circle = Circle(center: CGPoint(x: 187.5, y: 333.5), radius: 93.75)

var triangle = Polygon(corners: [ CGPoint(x: 187.5, y: 427.25), CGPoint(x: 268.69, y: 286.625), CGPoint(x: 106.31, y: 286.625)])

var diagram = Diagram(elements: [circle, triangle])

diagram.draw(Renderer())

先将三个值初始化,再讲Renderer()传入diagram的方法中。


Renderer的小修改

首先复制一份Renderer

struct Renderer { 
    func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") } 
    func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") } 
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
    { print("arcAt(\(center), radius: \(radius)," + " startAngle: \(startAngle), endAngle: \(endAngle))")  } 
} 
struct Renderer { 
    func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") } 
    func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") } 
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
    { print("arcAt(\(center), radius: \(radius)," + " startAngle: \(startAngle), endAngle: \(endAngle))")  } 
} 

将第一个Renderer从struct改为protocol

protocol Renderer {  
    func moveTo(p: CGPoint)   
    func lineTo(p: CGPoint)  
    func arcAt(center: CGPoint, radius: CGFloat,startAngle: CGFloat, endAngle: CGFloat) 
}

将第二个Renderer修改为TestRenderer ,遵守Renderer协议。

struct TestRenderer:Renderer { 
    func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") } 
    func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") } 
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
    { print("arcAt(\(center), radius: \(radius)," + " startAngle: \(startAngle), endAngle: \(endAngle))")  } 
} 

以上只是一个简单对Renderer部分做了处理。
最后再修改一下调用方式即可。

diagram.draw(TestRenderer())

用CGContext测试

extension CGContext : Renderer {  
    func moveTo(p: CGPoint) { }  
    func lineTo(p: CGPoint) { }   
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) { } 
}

现在可以为CGContext拓展协议。
如果这个Renderer是class,就不能这么做。
这样做之后CGContext就拥有了Renderer中所以基础的东西,并且需要全部是实现。

extension CGContext : Renderer {
  func moveTo(p: CGPoint) {
    CGContextMoveToPoint(self, position.x, position.y)
  }
  func lineTo(p: CGPoint) {
    CGContextAddLineToPoint(self, position.x, position.y)
  }
  func arcAt(center: CGPoint, radius: CGFloat,
             startAngle: CGFloat, endAngle: CGFloat) {
    let arc = CGPathCreateMutable()
    CGPathAddArc(arc, nil, c.x, c.y, radius, startAngle, endAngle, true)
    CGContextAddPath(self, arc)
  }
}

详解在Building Better Apps with Value Types in Swift中。


易测的协议与泛型

如果用协议去解耦,任何东西都能够变得容易测试。
这中测试与使用模型很接近。但是模型本身是比较脆弱的。
你必须去将你的测试代码与在测试下的实现代码联系起来,因为模型很脆弱,所以不能在Swift的强静态类型系统下很好的运行。

struct Bubble : Drawable {
  func draw(r: Renderer) {
    r.arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    r.arcAt(highlightCenter, radius: highlightRadius,
        startAngle: 0, endAngle: twoPi)
  }
}
struct Circle : Drawable {
  func draw(r: Renderer) {
    r.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
} }

代码中startAngle: 0, endAngle: twoPi每个方法都用到了,如果想简化成这样:

struct Bubble : Drawable {
  func draw(r: Renderer) {
    r.circleAt(center, radius: radius)
    r.circleAt(highlightCenter, radius: highlightRadius)
  }
}
struct Circle : Drawable {
  func draw(r: Renderer) {
    r.circleAt(center, radius: radius)
  }
}

我们需要在协议中加上circleAt,直接将startAngle与Angle去除。

protocol Renderer {
  func moveTo(p: CGPoint)
  func lineTo(p: CGPoint)
  func circleAt(center: CGPoint, radius: CGFloat)
  func arcAt(
    center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
}

在有遵守Renderer协议的地方,我们可以用extension补充上去。

extension TestRenderer {
func circleAt(center: CGPoint, radius: CGFloat) {
  arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}
extension CGContext {
func circleAt(center: CGPoint, radius: CGFloat) {
  arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}

但是这么做特别奇怪,因为每一个地方都要补充相同的类容,很复杂。于是我们直接对协议做了扩展。

extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

这么完成之后就不需要再对其他准守了Renderer协议的地方再继续扩展。


协议的扩展

刚刚看到

extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

是对已经存在在Renderer协议中的方法进行了拓展,如果这个方法不存在在Renderer中呢?拓展了不存在在协议中的方法,那这个方法和拓展本来在协议中的方法的区别是什么?

现在我来做一个简单模型的示范:


protocol testProtocol{
    func a()
    func b()
}
extension testProtocol{
    func a(){
        print("a1")
    }
    func c(){
        print("c1")
    }
}

struct testStruct{

    func b(){
        print("b2")
    }
}
extension testStruct:testProtocol{
    func a(){
        print("a3")
    }
    func c(){
        print("c3")
    }
}

创建好之后我创建一个test对象,并且进行测试。

let test = testStruct()
test.a()
test.b()
test.c()

结果为:

a3
b2
c3

这看上去没什么奇怪的,甚至我们直接把extension testProtocol去除也没关系,但是我们再这么修改一下,如果swift知道它遵守了testProtocol呢?

let test:testProtocol = testStruct()
test.a()
test.b()
test.c()

结果为:

a3
b2
c1

为什么?因为a方式是必须的,所以调用了被定制的方法。而c方法不是必须的,所以在testStruct中只是覆盖了testProtocol的拓展实现内容。而现在,swift只知道test是testProtocol而不是testStruct,所以调用了testProtocol中实现的结果。
是不是说每一个方法都是必须的呢?对于大部分API来说是不一定的,所以最好的做法是覆盖协议中存在的必须实现的方法,而不是覆盖模型上的方法。


更多协议扩展的技巧

拓展的约束

extension CollectionType where Generator.Element : Equatable{
    public func indexOf(element: Generator.Element) -> Index? {
        for i in self.indices {
            if self[i] == element {
                return i }
        }
        return nil }
}

这样写会出错,因为在两个Generator.Element之间不能使用 == ,这时候只需要修改拓展条件,改为:

extension CollectionType where Generator.Element : Equatable

就可以解决。


protocol Ordered {
    func precedes(other: Self) -> Bool
}
func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int { return 1
}

let position = binarySearch([2, 3, 5, 7], forKey: 5)

我们寻找需要查找一个int类型的数字,但是编译器也会报错,因为int类型并没有遵守Ordered,这时候我们为了解决这个问题,加上了:

extension Int : Ordered {
  func precedes(other: Int) -> Bool { return self < other }
}

万一需要查找一个String类型呢?那又要加上

extension String : Ordered {
 func precedes(other: String) -> Bool { return self < other }
}

每一个类型都需要拓展一遍,而且再写一遍方法。但是Int和String都准守Compareble协议,我们可以直接拓展Compareble协议。

extension Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}
extension Int : Ordered {}
extension String : Ordered {}

省略了在Int和String中实现precedes方法的字段。
现在如果要查找Double类型呢?是否也要在加一次扩展?是事实就算不进行拓展,Double类型的对象也能实现precedes方法,事实上它就算能实现precedes方法也不能在没有被扩展的情况下用二分查找。那么这个precede还有什么意义吗?
为了解决这个问题,还是依然用了为拓展加上约束的方法。

extension Ordered where Self : Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}

这样就能精确的知道我们到底想要的是什么。


泛型的美化

这是一个二分查找的使用,可以使用在任何集合中,在Swift1中是这么写的。

func binarySearch<
  C : CollectionType where C.Index == RandomAccessIndexType,
  C.Generator.Element : Ordered
>(sortedKeys: C, forKey k: C.Generator.Element) -> Int {
  ...
}
let pos = binarySearch([2, 3, 5, 7, 11, 13, 17], forKey: 5)

看上去非常的糟糕而且丑陋,在Swift2中可以改成这样:

extension CollectionType where Index == RandomAccessIndexType,
Generator.Element : Ordered {
  func binarySearch(forKey: Generator.Element) -> Int {
    ...
} }
let pos = [2, 3, 5, 7, 11, 13, 17].binarySearch(5)

哪一种写法更好,这是可以一眼看出来的。


Building Better Apps with Value Types in Swift

func == (lhs: Polygon, rhs: Polygon) -> Bool {
  return lhs.corners == rhs.corners
}
extension Polygon : Equatable {}
func == (lhs: Circle, rhs: Circle) -> Bool {
  return lhs.center == rhs.center
    && lhs.radius == rhs.radius
}
extension Circle : Equatable {}

为什么需要所有的值类型都要能够使用==?
具体请看Building Better Apps with Value Types in Swift Session。

现在请看下面这段代码,如果不遵守Equatable协议:

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements == rhs.elements
}

这段代码就会出错,因为==不能作为操作符应用于两个Drawable之间,但是如果我们展开呢?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { $0 != $1 }
}

首先确定他们有同样多的元素,再讲两个array进行比较,好像是没有问题了,但是需要注意的是 != 并不能使用,因为!=不能作为操作符应用于两个Drawable之间,所以现在没有相等的操作符用于这两个Array之间了。

那我们能不能将Equatable用于所有的Drawable之中呢?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { $0 != $1 }
}
protocol Drawable : Equatable {
  func draw()
}

问题在于Equatable协议中的 == 。

protocol Equatable {
  func == (Self, Self) -> Bool
}

它有Self-requirements,这就意味着现在Drawable现在也有Self-requirements的特征。Self-requirements直接将Drawable放在了单一性,静态调度的世界,但是Diagram确实需要多态性的Drawable类型的数组,因为我们需要将polygons和circles放在相同的Diagram中,所以Drawable又是在多态性,动态调度的世界中。这就产生了矛盾。

现在怎么办?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { !$0.isEqualTo($1) }
}
protocol Drawable {
  func isEqualTo(other: Drawable) -> Bool
  func draw()
}
extension Drawable where Self : Equatable {
  func isEqualTo(other: Drawable) -> Bool {
    if let o = other as? Self { return self == o }
    return false
} }
  1. 在Drawable协议中添加了isEqualTo方法,$0==($1)改为 $0.isEqualTo($1)
  2. 将isEqualTo参数类型与Diagram中的elements一致,都是继承Drawable协议。
  3. 拓展Drawable协议,并加入约束条件。
  4. 先确定传入的参数是否是self类型,因为有了Equatable的限定,就可以使用就使用 == 操作符去判断,不是就返回false。

结尾

什么时候使用class?

当你想要implicit sharing 时

  • 拷贝或者比较实例没有意义的是有(如,window)
  • 实例生命周期与外部影响(如,临时文件)
  • 实例就像通过只写的方式流到外部状态(如,CGContext)
final class StringRenderer : Renderer {
  var result: String
  ...
}

比如这个。

  1. final。
  2. 继承的不是class。

不要与系统作对

  • 如果一个框架需要你传递一个对象或者子类,就需要。

也要谨慎

  • 在软件中不应该有太大的东西
  • 当纳入了class之外的元素,考虑不要用class

总结

使用协议而不是超类
拓展协议等于魔法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容