初识Swift的枚举,结构体,和类

本篇文章翻译自:Getting to Know Enums, Structs and Classes in Swift
原作:ray fix on March 8, 2016


在只有OC的日子里,我们只是封装类是不太能够满足我们的工作要求的。然而,在现代的iOS和Mac编程中,swift提供我们3种选择:枚举,结构体,和类。
配合这协议,这些类型能够创造不可思议的事情。尽管这些类型有着相似的能力,但也有很大的不同。
这篇tutorial的目的有三:

  • 带你体验枚举,结构体,和类的使用
  • 给你何时应该使用他们的直觉
  • 帮你理解他们各自是怎么工作的

首先,这篇tutorial假设你已经有了一些swift的基础知识和面向对象的编程经验。

全是关于类型

swift三大卖点:安全,快速和简洁。
安全意味着使用swift很难写出耗费内存和bugs隐藏很深的代码。swift让你的工作变得更加安全,因为它会在编译的时候提醒你产生了bug,而不是把你晾在哪儿直到运行时。
而且,因为swift可以让你更清楚的表达你的意图,优化器可以让你的代码运行得更加轻盈,快速。
因为建立在一些概念上,swift语言的核心是简单和高度正规化。尽管它有一些规则,但是你也可以使用它做一些amazing的事情。你能做到这些的关键是swift的类型系统:

types

尽管只有6个, swift的类型非常强大。没错,不像其他语言有很多内建的类型,swift只有6个。
这些类型由4个命名类型:协议,枚举,结构体,类和2个复合类型:元组,函数组成。
你可能会想到其他的基本类型---像 Bool, Int, UInt, Float, Double, Character, String, Array, Set, Dictionary, Optional等等。但是这些基础类型是由命名类型建立而来,可以看作是swift标准库的一部分。
本篇tutorial重点是枚举,结构体和类组成的命名模型类型。

可缩放矢量图形(SVG)的形状

作为一个演示示例,你会创建一个安全,快速,简单的SVG图形渲染框架。
SVG是基于XML的2D矢量图形格式。这个特点在1999年被W3C 确定为公共标准。

开始

File\New\Playground,创建一个playground,命名为Shapes, 并且设置为OS X平台(这个时候应该是MacOS了)。点击Next,选择保存路径,创建并保存文件。清空该文件,然后添加:

import Foundation

目标是渲染下面的东西:

<!DOCTYPE html><html><body><svg width='250' height='250'><rect x='110.0' y='10.0' width='100.0' height='130.0' stroke='Teal' fill='Aqua' stroke-width='5' /><circle cx='80.0' cy='160.0' r='60.0' stroke='Red' fill='Yellow' stroke-width='5'  /></svg></body></html>

相信我,如果用浏览器或者WebKit视图渲染,它会看起来好很多。

你需要一个类型来代表颜色,SVG使用CSS3颜色类型,这种类型可以指定为一个名称,RGB或者 HSL。了解这些特征可以浏览:http://www.w3.org/TR/css3-color/.
在SVG中定义一个色值的方式是把它指定为图形的一个属性。例如:fill = 'Gray'。swift中一种简单的方式是用字符串来表示色值, 如: let fill = "Gray"

使用字符串虽然简单有效,但是有几个不可忽视的缺点:

  1. 很容易发生错误。任何字符串即使它不代表色值,也能编译通过,但是在运行时,显示的效果就不对了嘛。例如:"Gray"如果拼写成"e",将达不到预期效果,但可以编译通过。
  2. 自动补全不会帮你找到有效的颜色名称。
  3. 如果作为参数你传递一个色值,那么字符串作为一个颜色类型不会表现的那么明显,自解释性较差。

枚举来帮忙

使用自定义类型来解决这个问题。如果你是从Cocoa Touch转过来的,你可能会封装一个像UIColor的类。当然使用类来设计是可以的,但是swift提供了更多的选择来定义你的模型。
我们还没有写任何东西,来想一想我们怎么用枚举来实现Color。
你可能会这么实现:

 enum ColorName {
    case Black
    case Silver
    case Gray
    case White
    case Maroon
    case Red
}

上面的实现跟C语言的枚举很相似。然而,不像C语言的枚举,swift可以让你指定每一种Case的类型.
指定为支持存储类型的枚举可以看做是RawRepresentable类型。因为它自动遵守了RawRepresentable协议。
因此,你可以指定ColorName为String类型,并且为每一个case赋值,如下:

ColorName: String {
    case Black = "Black"
    case Silver = "Silver"
    case Gray = "Gray"
    case White = "White"
    case Maroon = "Maroon"
    case Red = "Red"
}

然而,swift为String类型的枚举做了些特殊的工作。你不必指明每一种case等于啥,编译器会自动让字符串(指rawValue)跟名称一致。这意味着你只需要写上case名就可以了:

enum ColorName: String {
    case Black 
    case Silver
    case Gray
    case White
    case Maroon
    case Red
}

你还可以使用逗号隔开每一种case来减少代码量,这时仅使用一个case关键字:

enum ColorName: String {
   case Black, Silver, Gray, White, Maroon, Red
}

现在你有了第一个自定义类型,所有的好事从这里就开始了。例如:

let fill = ColorName.Grey // 错误:拼写错误的颜色名称不能通过编译,很好! 
let fill = ColorName.Gray //  正确的名称可以自动补全,编译通过🤗

关联值

用ColorName来命名颜色很好,但是你可能回想起CSS颜色不止这一种表示方式,还是:RGB, HSL等等。你怎么模型化它们呢?
swift的枚举类型非常适合来模型化有多种可能的类型。例如:CSS颜色,每一种case可以与它的数据结伴出现。这些数据被称为关联值。
使用枚举定义一个CSSColor:

enum CSSColor {
    case Named(ColorName)
    case RGB(UInt8, UInt8, UInt8)
}

有了这个定义,CSSColor可以有2种状态

  1. 它可以是Named, 这种情况下,关联值是ColorName类型的值
  2. 也可以是RGB,这种情况下,关联值是3个UInt8类型(0-255)组成的元组,他们分别表示red, green, 和blue。

注:为了简洁,示例中我们省略了RGBA, HSL和HSLA的场景。

枚举的协议和方法

如果你想要能够打印多个CSSColor的实例,像其他命名类型一样,可以遵守协议。你的类型可以通过遵守CustomStringConvertible协议来使用print语句。
跟swift标准库协作的关键是遵守标准库的协议。为CSSColor扩展协议:

extension CSSColor: CustomStringConvertible {
    var description: String {
        switch self {
        case .Named(let colorName):
            return colorName.rawValue
        case .RGB(let red, let green, let blue):
            return String(format: "#%02X%02X%02X", red, green, blue)
        }
    }
}

这使得CSSColor遵守CustomStringConvertible协议。这样就告诉swift,CSSColor可以被转换为字符串。我们通过实现description计算属性告诉它怎么转换。
这个实现中,self要分情况去确定枚举类型是Named,还是RGB。在每一种case下,你可以转化为对应格式的字符串。Named case下返回字符串名称,而RGB case下,返回特定格式的red, green, blue的值。

let color1 = CSSColor.Named(.Red)
let color2 = CSSColor.RGB(0xaa, 0xaa, 0xaa)
print("color1 = \(color1), color2 = \(color2)")

不像只使用字符串来表示颜色,编译器会在编译时进行类型检查,且结果证明准确无误。

枚举类型的构造器

就像swift的类和结构体一样,你也可以为枚举类型添加构造器。例如:你可以用Gray来创建一个构造器。

extension CSSColor {
    init(gray: UInt8) {
        self = .RGB(gray, gray, gray)
    }
}

playground中添加代码:

let color3 = CSSColor(gray: 0xaa)
print(color3)

现在你可以很方便创建Gray颜色了。

枚举类型的命名空间

命名类型可以作为一个命名空间让代码组织良好,降低复杂度。你创建了ColorName和CSSColor,且ColorName只在CSSColor的上下文中用到。
如果你把ColorName隐藏到CSSColor模块中,岂不是更完美?
从playground中移除ColorName,取而代之的是:

extension CSSColor { 
        enum ColorName : String { 
              case Black, Silver, Gray, White, Maroon, Red, Purple, Fuchsia,   Green,Lime, Olive, Yellow, Navy, Blue, Teal, Aqua 
    }
}

这样就把ColorName移动到CSSColor的扩展中了。现在ColorName被雪藏,被定义成CSSColor的内部类型。

swift一个重大特性是声明顺序通常是无所谓的。编译器会扫描的文件好几次,把顺序弄明白,而不像C/C++/OC需要前向声明(用到的类型必须之前声明过)。
然而,如果你收到一个关于ColorName的一个错误提示说它是未声明的类型。移除上面的扩展,重新定义ColorName就是了。有时候,playground会对定义的顺序比较敏感,即使是真的没有关系。

枚举可以被设置成不能初始化的纯粹命名空间。例如:你很快就会用到数学常量pi执行一些运算。当然,你可以使用Foundation的 M_PI宏,但是你最好定义自己的让工作尽可能轻便。(Arduino 微控制器,我们来了!😈)

enum Math {
    static let pi = 3.1415926535897932384626433832795028841971694
}

因为Math中不包含任何case,且在扩展中添加case是非法的,那么Math将不能实例化。你永远不会误用Math为一个变量或者参数。
声明pi为一个static常量,你不必实例化。当你需要pi值的时候,你仅需要使用Math.pi就好,而不用记住一大串数字。

持股枚举(我要成股东了👻)

swift的枚举类型比其他语言的枚举要强大的多,例如: C或OC。正如你所见,你可以扩展他们,创建构造器方法,提供命名空间,和封装一些操作。
目前,你已经使用枚举模块化了CSS colors了。我们能这么做得益于我们理解了CSS colors,修整了W3C的规范。
从一些常见的情形中挑选一个,使用枚举类型很合适,例如:一周的七天,硬币的正反面,状态机的状态。swift的Optional也是用枚举实现的,它是带有关联值的.None或者.Some的一种情况。
换句话说,如果你想要CSSColor能够扩展到其他没有在W3C规范中定义的颜色空间模块中,枚举类型不是首先。好吧,这会把我们带到下一个swift命名类型---结构体

结构体

因为你想要SVG的使用者能够定义他们自己的图形,那么使用枚举来定义图形类型不是一个好的选择。
新的枚举case之后不能添加到扩展中。那么重任就落到类和结构体身上了。
swift标准库团队建议当你创建新的模型的时候,你首先应该使用协议设计接口,你想要你的图形是Drawable类型,所以添加以下代码到playground中:

protocol Drawable {
    func draw(context: DrawingContext)
}

协议定义了图形为Drawable类型。它有一个作图方法可以用来画DrawingContext类型的图形。
说到DrawingContext,当然它是另一个协议。

protocol DrawingContext {
    func draw(circle: Circle)
    // 更多基础类型,马上到来
} 

DrawingContext知道怎么绘制纯粹的几何图形:圆形,矩形和其他基础类型。

注意:我们没有指定作图技术具体是哪一种,但是你实现的时候,要把他们都考虑进去---可能是SVG, HTML5Canvas, Core Graphics, OpenGL, Metal等等。

现在可以来定义一个遵守Drawable协议的圆形了。

struct Circle: Drawable {
    var strokeWidth = 5
    var strokeColor = CSSColor.Named(.Red)
    var fillColor = CSSColor.Named(.Yellow)
    var center = (x: 80.0, y: 160.0)
    var radius = 60.0
    
    func draw(context: DrawingContext) {
        context.draw(self)
    }
}

在结构体有几个存储属性:

  • strokewidth: 描边宽度
  • strokeColor: 描边颜色
  • fillColor: 填充色
  • center: 圆心位置
  • radius: 圆半径

结构体跟类的工作方式有很大不同,也许最大的差别:结构体是值类型,而类是引用类型。

值类型 VS. 引用类型

值类型是一个分离的,独立的实体。
值类型比较突出的代表是整形,每一种编程语言中都有。如果你想了解值类型是怎么工作的,那么可以想一想整形是怎么做的。例如:

Int:
var a = 10
var b = a
a = 30 //b的值仍然是10
a == b //false 
Circle(结构体类型)
var a = Circle()
a.radius = 60.0
var b = a
a.radius = 1000.0 //b.radius仍然是60.0

如果Circle是Class类型,那么它就具有了引用语意了。也就意味着它会引用共有的对象。

Circle(使用class定义

var a = Circle() //Class类型
a.radius = 60.0
var b = a
a.radius = 1000.0 //b.radius现在变成了1000.0

当你使用值类型创建一个对象时,拷贝就发生;当使用引用类型,变量会引用相同的对象。类和结构体在这个点有很大的不同。

矩形模型

添加如下代码到playground,创建矩形类型,完善做图库。

struct Rectangle: Drawable {
    var strokeWidth = 5
    var strokeColor = CSSColor.Named(.Teal)
    var fillColor = CSSColor.Named(.Aqua)
    var origin = (x: 100.0, y: 10.0)
    var size = (width: 100.0, height: 130.0)
    
    func draw(context: DrawingContext) {
        context.draw(self)
    }
}

你需要更新DrawingContext协议,以便它能够知晓如何去绘制矩形。在playground中更新DrawingContext:

protocol DrawingContext {
    func draw(circle: Circle)
    func draw(rectangle: Rectangle)
    // 更多基础类型马上到来 ...
}

Circle和Rectangle都遵守Drawable协议。他们都会去做DrawingContext协议规定的工作。
现在是时候创建一个绘制SVG style的具体模型。

final class SVGContext: DrawingContext {
    private var commands: [String] = []
    
    var width = 250
    var height = 250
    
 //1
    func draw(circle: Circle) {
        commands.append("<circle cx='\(circle.center.x)' cy='\(circle.center.y)\' r='\(circle.radius)' stroke='\(circle.strokeColor)' fill='\(circle.fillColor)' stroke-width='\(circle.strokeWidth)'  />")
    }
    
 //2
    func draw(rectangle: Rectangle) {
         commands.append("<rect x='\(rectangle.origin.x)' y='\(rectangle.origin.y)' width='\(rectangle.size.width)' height='\(rectangle.size.height)' stroke='\(rectangle.strokeColor)' fill='\(rectangle.fillColor)' stroke-width='\(rectangle.strokeWidth)' />")
    }
    
    
    var SVGString: String {
        var output = "<svg width='\(width)' height='\(height)'>"
        for command in commands {
            output += command
        }
        output += "</svg>"
        return output
    }
    
    var HTMLString: String {
        return "<!DOCTYPE html><html><body>" + SVGString + "</body></html>"
    }
    
}

SVGContext是一个类,包含了一个私有String类型数组commands。在section 1和2处,遵守DrawingContext协议,draw方法会添加正确的XML字符串来渲染图形。
最后,你需要一个文档类型来装载这些Drawable实例:

struct SVGDocument {
    var drawables: [Drawable] = []
    
    var HTMLString: String {
        let context = SVGContext()
        for drawable in drawables {
            drawable.draw(context)
        }
        return context.HTMLString
    }
    
    
    mutating func append(drawable: Drawable) {
        drawables.append(drawable)
    }
}

这里,HTMLString是SVGDocument的一个计算型属性,它会创建一个SVGContext实例,并从该实例中返回HTMLString。

展示一下SVG

我们终于可以画出一个SVG了,playground添加如下内容:

var document = SVGDocument()
let rectangle = Rectangle()
document.append(rectangle)

let circle = Circle()
document.append(circle)

let HTMLString = document.HTMLString
print(HTMLString)

创建一个默认的圆形和矩形,并把它们放到文件中,然后打印XML。

让我们来瞧瞧SVG吧:

import WebKit
import XCPlayground
let view = WKWebView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
view.loadHTMLString(HTMLString, baseURL: nil)
XCPlaygroundPage.currentPage.liveView = view

这里需要playground做一些tricky工作---设置web view来显示SVG,按下Command-Option-Return在辅助编辑器中显示web view。



这酸爽,你get到了吗?

目前为止,你用结构体(值类型)和协议实现了Drawable模块。
现在我们用类,这会要求你去定义一个基类和衍生类。传统的面向对象的处理图形问题的方法是把draw()方法写在基类里。
即使现在你不用它,但是知道这种方法还是有用的。它会像这样:

hierarchy

代码层面,它看起来像这样 --- 这只是为了示范,不用把这些加到playground中:

class Shape {
    var strokeWidth = 1
    var strokeColor = CSSColor.Named(.Black)
    var fillColor = CSSColor.Named(.Black)
    var origin = (x: 0.0, y: 0.0)
    func draw(context: DrawingContext) { fatalError("not implemented") }
}

class Circle : Shape {
    override init() {
        super.init()
        strokeWidth = 5
        strokeColor = CSSColor.Named(.Red)
        fillColor = CSSColor.Named(.Yellow)
        origin = (x: 80.0, y: 80.0)
    }
    
    var radius = 60.0
    override func draw(context: DrawingContext) {
        context.draw(self)
    }
}

class Rectangle : Shape {
    override init() {
        super.init()
        strokeWidth = 5
        strokeColor = CSSColor.Named(.Teal)
        fillColor = CSSColor.Named(.Aqua)
        origin = (x: 110.0, y: 10.0)
    }
    
    var size = (width: 100.0, height: 130.0)
    override func draw(context: DrawingContext) {
        context.draw(self)
    }
}

为了让面向对象编程更加安全,swift引入了override关键词。他要求编程者知道什么时候重载方法。它会防止重载已经存在的方法或者你认为恰当但实际不恰当的重载。当你在使用一个新版本的库并且理解其中的变化是很重要的事情。

然而,这个面向对象的方法有几点不足:

  • 第一个问题:注意到基类中实现的draw()方法为了避免被误用,调用了fatallError()提醒衍生类应该重载这个方法。不幸的是,这个检查发生在运行时,而不是编译时。

  • 第二个问题:为了保证正确性,Circle和Rectangle类不得不处理基类的实例化方法。但是这会让实例化过程变得复杂。

  • 第三个问题: 随着需求的发展,基类很难维持基类的地位
    例如:你想要添加一个Line类型。为了能够跟已经存在的系统协同工作,它不得不衍生于Shape,这会事情看起来不正常。而且,你的Line类需要初始化基类的fillColor属性,对于一条线显然是没有意义的。

line

基于此,又可以重构你的层级关系使之变得更加合理。然而实际中,既要求修改基类,而又不影响到既存方法是不太可能的。而且通常一开始,很难修改正确。

最后,类具有引用语意,这个之前已经讨论过。尽管ARC大多数时候会处理引用,但你也得十分小心不要引入循环引用,否则会发生内存泄露。如果你把相同的图形添加到数组中,且修改了其中一个的颜色值,另一些的色值也会变化,这真的让小伙伴们惊呆了。

为什么还要用类呢?

鉴于以上的不足,为什么我还要用类呢?
首先,它们允许你采用已经非常成熟的Cococa和Cocoa Touch框架。另外,引用类型还是有很重要的用途的。例如:如果一个对象的拷贝操作很耗内存,那么把它封装成类不失为一个好的选择。
那么,该怎么选择呢?区别值类型和引用类型是很有帮助的,关于这个话题,可以参考:Reference vs. Value Types in Swift.

计算型属性

所用的命名类型你都可以自定义setter和getter方法,而不必对应一个存储属性。
如果你想要在Circle中添加一个直径的getter和setter方法,通过半径很容易实现。
只需要添加如下代码:

extension Circle {
    var diameter: Double {
        get {
            return 2 * radius
        }
        
        set {
            radius = newValue/2
        }
    }
}

这里基于半径实现了一个新的计算型属性。当你要获得直径时,它返回半径的2倍。当你设置直径时,它会设置半径为直径的1/2。
但是通常情况下,你只会想要实现一个特别的getter方法。这种情况下,你不必写get{}语句块,直接写函数体即可。周长和面积就是很好的例子嘛。
添加如下代码:

    var area: Double {
        return radius * radius * Math.pi
    }
    
    var perimeter: Double {
        return 2 * radius * Math.pi
    }

不像类,结构体方法默认情况下不允许修改结构体,或者说改变存储型属性。但是你可以把方法声明成mutating。
例如:

    func shift(x: Double, y: Double) {
        center.x += x
        center.y += y
    }

我们试图给Circle定义一个shift()方法,它可以移动circle的位置。但是编译器会在两行赋值语句处抛出错误。

ERROR: Left side of mutating operator has immutable type ‘Double'

我们可以通过添加关键字mutating来解决问题:

    mutating func shift(x: Double, y: Double) {
        center.x += x
        center.y += y
    }

这就告诉了编译器方法改变了结构体。

逆向建模和类型约束

swift有一个重要特性:逆向建模。它可以让你在不知晓源码的情况下,扩展类型的行为。
这里有一个比较常见的例子:假设你正在使用SVG的代码,你想像Circle一样给Rectangle添加area和perimeter属性。那么,可以添加代码:

extension Rectangle {
    var area: Double {
        return size.width * size.height
    }
    
    var perimeter: Double {
        return 2 * (size.width + size.height)
    }
}

之前,你向已经存在的模型添加了2个方法,现在,你将这些方法整合到一个正式的新协议。

protocol ClosedShapeType {
    var area: Double { get }
    var perimeter: Double { get }
}

接下来,你要通过添加如下代码,告知Circle和Rectangle遵守这个协议:

extension Circle: ClosedShapeType {}
extension Rectangle: ClosedShapeType {}

你可以定义一个函数,例如:计算一个数组中元素的周长的总和(当然这些数组元素要遵守ClosedShapeType协议, 元素可以是结构体,枚举,类)。

func totalPerimeter(shapes: [ClosedShapeType]) -> Double {
    return shapes.reduce(0){$0 + $1.perimeter}
}
totalPerimeter([circle, rectangle])

这里使用了reduce来计算直径的总和。你可以在An Introduction to Functional Programming.了解更多它是如何工作的。

延伸阅读

完整的playground在这里获得.
在这篇tutorial, 你了解到了枚举,结构体,和类 --- swift的命名模型类型
三者相似点:封装,初始化方法,都可以有计算型属性,遵守协议,都可以逆向建模。
然而,它们也有不同点:

枚举是值类型,由一组case构成,每一种case可以有不同的关联值。枚举类型的值对应着定义中的一个case。它们不能有存储属性。

结构体,像枚举类型一样也是值类型,但是可以有存储属性。

类,像结构体一样有存储属性。它们也可以构建层级结构,在层级中重载属性和方法。鉴于此,显式的基类的初始化方法是必须的。不像结构体和枚举类型,类是引用类型,即共享、语义。

获得更多信息,可以移步之前提到过的系列文章第二部分,Reference vs. Value Types in Swift.
希望你享受本次swift命名模型类型的旋风之旅。如果你乐于接受挑战,可以考虑建立一个SVG渲染库的完整版本。你已经有了一个很好的开始。看好你哟😜, 欢迎交流。

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

推荐阅读更多精彩内容