Swift中的自定义类型

WechatIMG32.jpeg

在实际编程中,很多时候,我们都需要使用比IntString这类简单类型更复杂的类型,例如,需要两个Double表达的2D坐标位置;需要两个String表达的人的姓和名等等。因此,如果我们的程序里真的可以使用叫做locationname的类型,程序的可读性就会提高很多。于是,Swift允许我们根据实际的需要,定义自己的类型系统。并把这种自定义的类型,叫做named types

根据实际的应用场景,Swift提供了4种不同的named typesstructclassenumprotocol。首先,我们来看表达值类型(value type)的struct

如何来理解这个value type呢?在前面我们也提到过,有时,我们需要把没有任何意义的Double,近一步抽象成一个点坐标的location。而无论是Double还是location,它们所表达的,都是“某种值”这样的概念,而这,就是值类型的含义。

定义一个struct

先来看一个例子,我们要计算平面坐标里某个点距点A的距离是否小于200,算起来很简单,勾股定理就搞定了:

let centerX = 100.0
let centerY = 100.0
let distance = 200.0
func inRange(x: Double, y: Double) -> Bool {
    // sqrt(n) pow(x, n)
    let distX = x - centerX
    let distY = y - centerY
    
    let dist = 
        sqrt(pow(distX, 2) + pow(distY, 2))
    
    return dist < distance
}

其中sqrt(n)用来计算n的平方根,pow(x, n)用来计算x的n次方,它们都是Swift提供的库函数。定义好inRange之后,我们就可以像:

inRange(100, y: 500)
inRange(300, y: 800)

来调用inRange。但是这样有一个不足,当我们需要比较很多个点和Center的距离的时候,这些数字并不能明确告诉我们它们代表的位置的意义,甚至我们都无法知道它们代表一个数字。如果我们可以像这样来比较位置:

inRange(location1)
inRange(myHome)

相比数字,它们看上去就会直观的多,而这,就是我们需要自定义struct类型最直接的原因。

初始化struct

我们可以像这样定义一个struct类型:

struct StructName {/* struct memberes*/}

struct Location {
    let x: Double
    var y: Double
}

在我们的例子里,struct members就是两个Double来表示一个位置的xy坐标。定义好struct类型之后,我们就可以像这样定义location变量并访问和修改location members

var pointA = Location(x: 100, y: 200)
pointA.x
pointA.y
pointA.y = 500

有了这些内容之后,我们就可以修改之前的inRange函数了,让它接受一个Location类型的参数:

func inPointRange(point: Location) -> Bool {
    // sqrt(n) pow(x, n)
    let distX = point.x - centerX
    let distY = point.y - centerY
    
    let dist = sqrt(pow(distX, 2) + pow(distY, 2))
    
    return dist < distance
}

然后,我们用来比较位置的代码,就易懂多了:

inPointRange(pointA)
Struct initializer

除了使用:

var pointA = Location(x: 100, y: 200)

这样的方式初始化Location之外,我们几乎可以使用任意一种我们“希望”的方式,例如:

var pointA = Location("100,200")

为了实现这个功能,我们需要给Location添加自定义的initializer:

struct Location {
    let x: Double
    var y: Double
    
    // Initializer
    init(stringPoint: String) {
        // "100,200"
        let xy = stringPoint.characters.split(",")
        x = atof(String(xy.first!))
        y = atof(String(xy.last!))
    }
}

这样,我们就可以使用特定格式的字符串,初始化Location了。但是,这时,我们会看到编译器告诉我们之前pointA的定义发生了一个错误:

这是由于struct initializer创建规则导致的。

Initialization rules

Memberwise initializer

对于我们一开始的Location定义:

struct Location {
    let x: Double
    var y: Double
}

它没有自定义任何init方法,Swift就会自动创建一个struct memberwise initializer。因此,我们可以逐个成员的去定义一个Location

var pointA = Location(x: 100, y: 200)

“Memberwise只是默认为我们提供了一个按成员初始化的方法,它并不会自动为成员设置默认值。”

特别提示
在我们自定义了init方法之后,Swift就不会自动为我们创建memberwise initializer了,这也就是编译器会报错的原因,不过我们可以手工打造一个:

struct Location {
    let x: Double
    var y: Double
    
    // Initializer
    init(x: Double, y: Double) {
        self.x = x;
        self.y = y;
    }

    init(stringPoint: String) {
        // "100,200"
        let xy = stringPoint.characters.split(",")
        x = atof(String(xy.first!))
        y = atof(String(xy.last!))
    }
}

我们手工打造的memberwise版本和String大同小异。只是,由于我们在init的参数中,使用了和member同样的名字,在init的实现中,我们使用了关键字self来表示要创建的struct本身,来帮助编译器区分member x和参数x,这和之前我们用pointA.x的道理是一样的。

这里,有一个小细节。之前我们讲函数的时候,说过函数的第一个参数是省略outername的,但是在init里,第一个参数的outername是不会省略的,我们必须指定每一个参数的outername

Member default values

如果我们希望给structmember设置一个默认值,而不用每次创建的时候去指定它们,我们可以像下面这样:

struct Location {
    let x = 100.0
    var y = 100.0
}

在这里,如果我们可以明确给成员设置默认值,就可以省掉type annotation的部分了,type inference会正确的推导成员的类型。

Default initializer

如果我们为struct的每一个成员都设置了默认值,并且,我们没有自定义init方法,Swift就会自动为我们生成一个default initializer。有了它,我们就可以这样创建Location了:

let center = Location()

“如果,我们没有为每一个member设置默认值而直接使用default initializer,编译器会报错。”

特别提示
memberwise initializer一样,如果我们自定义了init方法,那么这个默认的initializer就不存在了。但是,我们同样可以手工打造一个:

struct Location {
    let x = 100.0
    var y = 100.0
    
    // Default initializer
    init() {}

    // Initializer
    init(x: Double, y: Double) {
        self.x = x;
        self.y = y;
    }

    init(stringPoint: String) {
        // "100,200"
        let xy = stringPoint.characters.split(",")
        x = atof(String(xy.first!))
        y = atof(String(xy.last!))
    }
}

Default initializer最简单,看上去,就像一个什么都不做的方法一样。

Methods in struct

假设我们希望让Location水平移动一段距离,我们可以这样定义一个函数:

var pointA = Location(x: 100, y: 200)

func moveHorizental(dist: Double, inout point: Location) {
    point.x = point.x + dist
}

moveHorizental(10.0, point: PointA)

尽管达成了目的,但这样做有很多问题。首先,x是一个Location内部的成员,这个代表水平坐标的名字无需被Location的使用者关心;其次,移动坐标点本身就是一个只和Location自身计算有关的行为,我们应该像下面这样来完成这个任务:

var pointA = Location(x: 100, y: 200)
pointA.moveHorizental(10.0)

这就是我们要为struct类型定义methods的原因,它让和类型相关的计算表现的更加自然。定义method很简单,和定义函数是类似的:

struct Location {
    let x = 100.0
    var y = 100.0
    
    mutating func moveHorizental(dist: Double) {
        self.x = self.x + dist;
    }
}

由于作为一个值类型,Swift默认不允许我们在method里修改成员的值,如果我们要修改它,需要在对应的方法前面使用mutating关键字。之后,我们就可以这样:

pointA.moveHorizental(10.0)
来水平移动pointA了。

Struct extension

如果我们使用的Location不是自己定义的,但是我们仍旧希望在自己的项目里扩展Location的操作,Swift也能帮我们达成,这个机制,叫做extension。例如,我们希望给Location添加垂直移动的方法:

extension Location {
    mutating func moveVertical(dist: Double) {
        self.y += dist
    }
}

这里,同样我们要使用mutating。之后,在我们的项目里,就可以针对Location类型的变量,使用moveVertical方法了:

pointA.moveVertical(10.0)

接下来,我们再举一个更明显的例子,我们甚至可以扩展SwiftString类型,例如判断一个字符串中字符的个数是奇数还是偶数:

extension String {
    func isEven() -> Bool {
        return self.characters.count % 2 == 0 ? true : false
    }
}

"An even string".isEven()

Struct is a value type

在一开始,我们就讲到struct是一个value type,它用来表达某种和“值”有关的概念。除了语义上的描述之外,value type还有一个特点,就是当它被拷贝的时候,会把“整个值"拷贝过去

var pointA = Location(x: 100, y: 200)
var pointB = pointA
pointB.y = 500.0
pointA.y

从结果里,我们就能看到,所谓的“拷贝整个值”,就是指修改pointB完整复制了pointA的内容,而不是和A共享同一个内容。

Struct无处不在

其实,在Swift,我们已经使用了很多struct,甚至是哪些我们叫做“基础类型”的IntString。在Playground里,如果我们按住Command键单击某个类型,例如:Double,就会看到,它是一个struct

public struct Double {
    public var value: Builtin.FPIEEE64
    /// Create an instance initialized to zero.
    public init()
    public init(_bits v: Builtin.FPIEEE64)
    /// Create an instance initialized to `value`.
    public init(_ value: Double)
}

这和我们之前用过的诸如C、Objective-C是完全不同的。

作为Swift中的“named type”,从语法上来说,class和struct有很多相似的地方,例如:

struct PointValue {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}
class PointRef {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

都可以用来自定义类型
都可以有properties
都可以有methods
而实际上,作为一个reference typeclass具有很多struct不具备的表达能力,这些能力,都源自于它们要表达的内容不同。class表达一个“对象”,而struct表达一个值。我们通过一个例子来简单的理解下这种区别。

Value Type vs Reference Type

第一个区别是:class类型没有默认的init方法。如果我们不指定它,Swift编译器会报错。

为什么要如此呢?因为,class不简单表达一个“值”的概念。Swift要求我们明确通过init方法说明“打造”一个对象的过程。相反,struct表达一个自定义的“值”,在没有特别说明的情况下,一个值的初始化当然是把它的每一个member都按顺序初始化

第二个区别是:classstruct对“常量”的理解是不同的。我们分别定义一个PointValuePointRef的常量:

let p1 = PointVal(x: 0, y: 0)
let p2 = PointRef(x: 0, y: 0)

p1.x = 10
p2.x = 10

同样是常量,但是编译器会对p1.x = 10报错:

这是因为,p1作为一个值类型,常量的意义当然是:“它的值不能被改变”。但是p2作为一个引用类型,常量的意义则变成了,它不能再引用其他的PointRef对象。但是,它可以改变其引用的对象自身。

这就是引用类型代表的“对象”和值类型代表的“值本身”之间的区别,理解了这种区别,我们才能正确的理解struct和class的用法。

"相等的值"还是“相等的引用”?

如果我们定义下面两个变量:

let p2 = PointRef(x: 0, y: 0)
let p3 = PointRef(x: 0, y: 0)

尽管它们都表示同一个坐标点,但是它们相等么?针对引用类型,为了区分“同样的值”和“同样的对象”,Swift定义了Identity Operator:===和!==。

// Identity operator
if p2 === p3  {
    print("They are the same object")
}

if p2 !== p3  {
    print("They are not the same object")
}

它们分别用于比较两个引用类型是否引用相同的对象,返回的是一个Bool类型。

Method之于structclass

看似都是在structclass中定一个函数,但是method之于它们,也有着不同的含义。如果struct中的method要修改其成员,我们要明确把它定义为mutating

struct PointVal {
    var x: Int
    var y: Int

    mutating func moveX(x: Int) {
        self.x += x
    }
}

这是因为,在Swift看来一个PointVal的值,和我们在程序中使用的诸如:123这样的字面值是没有本质区别的,一个字面值理论上是不应该有修改其自身值的方法的。

“通常你需要修改一个struct的本意,是需要一个新的值。”

特别提示

 但是类对象不同,它的数据成员对他来说,只是一个用于描述其特征的属性,我们当然可以为其定义修改的方法:
class PointRef {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
    
    func moveX(x: Int) {
        self.x += x
    }
}

赋值 Vs 复制

我们要讲到的structclass的最后一个区别是:

var p1 = PointVal(x: 0, y: 0)
var p4 = p1
p4.x = 10
p1.x // p1.x still 0

var p2 = PointRef(x: 0, y: 0)
var p5 = p2
p2.x = 10
p5.x // p5.x = 10

值类型的变量赋值,会把变量的值完整的拷贝,因此修改p4.x不会影响p1.x
引用类型的变量赋值,会复制一个指向相同对象的引用,因此修改p2.x会影响p5.x

不再只是“值替身”的enum

很多时候,我们需要用一组特定的值,来表达一个公共的含义。例如用1,2,3, 4表示东、南、西、北:

let EAST  = 1
let SOUTH = 2
let WEST  = 3
let NORTH = 4

或者用一个字符串表示一年的月份:

let months = ["January", "February", "March", "April", "May", "June", 
    "July", "August", "September", "October", "November", "December"]

在上面这些例子里,无论是用数字表示方向,还是用字符串表示月份,它们都有一个共同的问题:我们让一个类型承载了本不属于他的语意。因此我们无法安全的避免“正确的类型,却是无意义的值”这样的问题。例如:数字5表示什么方向呢?Jan.可以用来表示一月么?还有JAN呢?因此,面对“把一组有相关意义的值定义成一个独立的类型”这样的任务,Swift为我们提供了一个叫做:enumeration的工具。

Enumeration并不是一个新生事物,几乎任何一种编程语言都有和enumeration类似的语法概念。但是Swiftenumeration做了诸多改进和增强,它已经不再是一个简单的“值的替身“。它可以有自己的属性、方法,还可以遵从protocol

定义一个Enumeration

我们这样来来定义一个enum:

enum { 
    case value 
    case value 
}

例如,我们可以把开始提到过的方向和月份定义成enum:

enum Direction {
    case EAST
    case SOUTH
    case WEST
    case NORTH
}

enum Month {
    case January, Februray, March, April, May, June, July,
        August, September, October, November, December
}

这样,我们就用enum定义了两个新的类型,用来表示“方向”和“月份”,它们是两个有限定值的类型。然后,我们可以像下面这样,使用它们代表的值:

let north = Direction.NORTH
let jan = Month.January

直观上看,使用enum比直接使用数字和字符串有很多“天生”的好处:一来我们可以借助Xcode的auto complete避免输入错误;二来,使用enum是类型安全的,需要使用方向和月份内容的时候,不会发生“类型正确,值却无意义的情况”。

理解Enumeration的“各种”value

Swift里,enum的值,可以通过不同的方式表达出来。而不像Objective-C,只能通过一个整数来替代。

Enumeration case自身就是它的值

例如在上面的例子里,当我们当我们使用Direction.NORTH时,我们就已经在访问一个enum的值了,它的case就是它的值本身,我们无需特意给它找一个“值替身”来表示。另外,如果通过type inference可以推导出enum的类型,我们可以在读取值的时候,省掉enum的名字:

func direction(val: Direction) -> String {
    switch val {
    case .NORTH, .SOUTH:
        return "up down"
    case .EAST, .WEST:
        return "left right"
    }
}

这个例子里,有两个地方值得注意:

因为val的类型可以通过type inference推导出是Direction,因此,在case里,我们可以省略掉enum的名字;
对于一个enum来说,它全部的值就是它所有的case,因此在一个switch...case...里,只要列举了enum所有的case,它就被认为是exhausitive的,因此,我们可以没有default分支;

Raw values

Objective-C不同,Swiftenum默认不会为它的case“绑定”一个整数值。如果你一定要这么做,你可以手工进行“绑定”。而这样“绑定”来的值,叫做raw values

enum: Type { 
    case value 
    case value 
}

enum Direction: Int {
    case EAST
    case SOUTH
    case WEST
    case NORTH
}

当我们这样定义Direction之后,Swift就会依次把EAST / SOUTH / WEST / NORTH“绑定”上0 / 1 / 2 / 3。我们也可以像下面这样给所有的case单独指定值:

enum Direction: Int {
    case EAST = 2
    case SOUTH = 4
    case WEST = 6 
    case NORTH = 8
}

或者,我们可以给所有的case指定一个初始值:

enum Month: Int {
    case January = 1, Februray, March, April, May, June, July,
        August, September, October, November, December
}

这样,Swift就会自动为其他月份“绑定”对应的整数值了。如果我们要读取enum的raw value,可以访问case的rawProperty方法:

let north = Direction.NORTH.rawValue
let jan = Month.January.rawValue

或者,我们可以通过一个rawValue来生成一个enumvalue值:

let north = Direction(rawValue: 4)

if let n = Direction(rawValue: 4) {
    print("North!")
}

但是,不一定所有传入的值都是一个合法的rawValue。因此,Direction(rawValue:)是一个failable initializer,它返回的类型是一个Optional<Dierection>

Associated values

Raw value的各种机制和方式,传统且易于理解。但是,这并不是给enum“绑定”值的唯一办法,在Swift里,我们甚至可以给每一个case“绑定”不同类型的值。我们管这样的值叫做associated values

例如,我们定义一个表达HTTP action的enum

enum HTTPAction {
    case GET
    case POST(String)
    case PUT(Int, String)
}

我们在每一个需要有associated valuecase后面放上和case对应的值的类型,就可以了。然后,我们可以这样来使用HTTPAction

var action1 = HTTPAction.GET
var action2 = HTTPAction.POST("BOXUE")

switch action1 {
case .GET:
    print("HTTP GET")
case let .POST(msg):
    print("\(msg)")
case .DELETE(let id, let value):
    print("\(id)=\(value)")
}

这个例子里,有两点是应该注意的:

不是每一个case必须有associated value,例如.GET就只有自己的enum value
当我们想“提取”associated value的所有内容时,我们可以把letvar写在case后面,例如.POST的用法;
当我们想分别“提取associated value中的某些值时,我们可以把let或var写在associated value里面,例如.DELETE的用法;

Optional是一个enumeration

其实,有了associated value之后就不难想象,Swift中的Optional,是基于enum实现的了。可以把一个Optional理解为包含两个case的enum,一个是.None,表示空值;一个是.Some用来表示非空的值。下面这两种定义optional的方式是一样的:

var address: String? = nil
var address1: Optional<String> = nil

如果我们按住option然后点击Optional,就可以看到Xcode提示我们Optional是一个enum

var address: Optional<String> = .Some("Beijing")

switch address {
case .None:
    print("No address")
case let .Some(addr):
    print("\(addr)")
}

而当address为.None时,它和nil是相等的:

address = .None

if address == nil {
    print(".None is equal to nil")  
}

自定义 properties

struct Location {
    var x = 100.0
    var y = 100.0
}

class PointRef {
    var x: Int
    var y: Int
}

enum Direction: Int {
    case EAST = 2
    case SOUTH = 4
    case WEST = 6
    case NORTH = 8
}

其中,我们已经在structclass中使用了自定义类型的属性。实际上,除了像定义变量一样的使用属性,Swift为自定义类型的属性提供了更多功能

Stored properties

顾名思义,这种属性是用来真正存储值的,就像之前我们为LocationPointRef定义的xy一样,它们有以下特点:

可以分别使用letvar定义成常量或变量;
init方法要确保每一个stored property被初始化;
可以在定义property的时候指定初始值;
实际占用内存空间;
使用obj.property的形式读取;
使用obj.property = val的形式赋值;
只有structclass可以定义stored property
每一个stored property都表示了某一个自定义类型对象的某种特点,但是有些属性是需要访问的时候被计算出来的,而不是定义之后一成不变的。这种属性叫做computed properties

Computed properties

顾名思义,作为properties,它用来表示对象的某种属性。但是,它的值在每次被访问的时候,要被计算出来,而不是内存中读取出来。我们来看一个例子:

struct MyRect {
    var origin: Point
    var width: Double
    var height: Double
}
var rect1 = MyRect(origin: Point(1, 1), width: 200, height: 100)

我们使用原点以及宽高定义了一个矩形。除了这些“原始”的矩形属性之外,当我们想访问矩形的“中心”时,我们就可以定义一个computed property:

struct MyRect {
    var origin: Point
    var width: Double
    var height: Double

    var center: Point {
        let centerX = origin.x + self.width / 2
        let centerY = origin.Y + self.height / 2

        return Point(x: centerX, y: centerY)
    }
}

这样,我们就能“动态”读取到一个矩形的中心了:

rect1.center
rect1.height = 200
rect1.center
```
从`Playground`结果里,我们可以看到`center`会随着`height`的改变而改变。在我们的这个例子里我们只是读取了一个`computed property`,我们可以对`computed property`赋值么?

答案是可以的,但是由于`computed property`并不实际占用内存,因此我们要把传入的值“拆”给`class`的各种`stored properties`。并且,一旦我们需要给`computed property`赋值,我们就要在定义它的时候,明确它的`get`和`set`方法,像下面这样:
```
struct MyRect {
    var origin: Point
    var width: Double
    var height: Double

    var center: Point {
        get {
            let centerX = origin.x + self.width / 2
            let centerY = origin.Y + self.height / 2

            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            self.origin.x = newCenter.x - self.width / 2
            self.origin.y = newCenter.y - self.height / 2
        }
    }
}
```
对于上面的例子,有几点值得注意:
我们使用get和set关键字分别表示一个`computed propterty`的读取和赋值操作;
当我们对一个`computed property`赋值的时候,由于它被拆分成多个`stored property`,因此我们在拆分的过程中总要做一些假设。在我们的例子里,我们假设当`center`变化时,矩形的宽高不变,移动了原点。

然后,我们可以通过下面的代码来测试结果:
```
var center = rect1.center
rect1.origin
center.x += 100
rect1.center = center
rect1.origin
```
从`Playground`的结果可以看到,`rect1`的`orgin`向右移动了`100`。以上就是`computed property`的用法,接下来,我们回到`stored property`,如果我们想在`stored property`赋值的时候自动过滤掉“非法值”,或者在`stored property`赋值后自动更新相关的`property`怎么办呢?在`Swift`里,我们可以给`stored property`添加`observer`。

###Property observer

`Swift`给`stored property`提供了两个`observer`:`willSet`和`didSet`,它们分别在`stored property`被赋值前和后被调用。定义`observer`和定义`computed property`是类似的。首先我们来看`willSet`,我们在`width`被赋值之前,向控制台打印一个信息:
```
struct MyRect {
    var origin: Point
    var width: Double {
        willSet(newWidth) {
            print("width will be updated")
        }
    }
}

rect1.width = 300

这时,打开控制台,就可以看到willSet的输出了。接下来,我们来看didSet的用法,如果我们希望width大于0的时候才更新并且保持宽高相等,可以这样:


struct MyRect {
    var origin: Point
    var width: Double {
        didSet(oldWidth) {
            if width <= 0 {
                width = oldWidth
            }
            else {
                self.height = width
            }
        }
    }
}

rect1.width=-300
rect1.height
rect1.width=300
rect1.height
```
从结果我们就可以看到,只有在`width大于0`的时候,`width`和`height`才会被更新。在上面的这两个例子里,有以下几点是我们要特别强调的:

在`didSet``里,我们可以直接使用width读取MyRect的width属性`,但是我们必须使用`self`读取其它的属性;
`willSet`和`didSet`不在对象的`init`过程中生效,仅针对一个已经完整初始化的对象在对属性赋值的时候生效;
如果我们不指定`willSet`和`didSet`的参数,`Swift`默认使用`newValue`和`oldValue`作为它们的参数;

###Type property

首先我们定义一个`enum`表示各种形状:
```
enum Shape {
    case RECT
    case TRIANGLE
    case CIRCLE
}
```
然后,我们可以给`MyRect`添加一个属性表示它对应的形状,由于这个属性对于MyRect的所有对象都是一样的,都应该是`Shape.RECT`,这时,我们就可以为`MyRect`添加一个`type property`:
```
struct MyRect {
    var origin: Point
    var width: Double
    var height: Double

    // Type property
    static let shape = Shape.RECT
}
```
我们使用`static`关键字为`named type`定义`type property`,定义好之后,我们不能通过具体的对象访问`type property`,而是要直接使用类型的名字。例如:
```
// WRONG!!! rect1.shape
let shape = MyRect.shape
```
这就是`type property`的用法,它可以帮助我们很方便的描述一个类型所有对象的属性。

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

推荐阅读更多精彩内容