Swift 3 SE-0023 API设计指南

本文由泊学翻译
摘自:https://github.com/Boxue/swift-api-design-guidelines/blob/master/SE-0023%20swift-api-guidelines.md

API设计原则


第一部分 基本原则

1.1 简以至用

这是最重要的设计目标。方法(method)和属性(property)只被声明一次,却会被无数次反复使用。API的简洁和清晰是要体现在它们被使用的时候。当评估一个设计的时候,仅仅去读它的声明是远远不够的,要反复在它的使用环境里推敲这个设计,并确保它足够简洁;

1.2 明确重于简短

尽管Swift代码可以很简短,但用最少的字符去编程并不是Swift的初衷。Swift中出现的简短代码只是强类型语言带来的一个副作用而已,因为可以在类型约束下推导出很多代码,因此省略了很多不必要的代码模板;

1.3 用文档的风格写注释

在每一个声明前,你应该如此。写文档时获得的灵感可以对你的设计思路有极大启发,所以,不要想着编码后回来补文档。

实际上,如果你无法三三两两就描述出API要完成的任务,你的API也许设计错了

另外,对于文档注释,还有以下补充说明:

1.3.1 使用[Swift Markdown方言]编写注释
1.3.2 在任何声明之前,(让注释)以一个摘要开始

通常,应该让读者可以根据这个摘要和声明完全理解API的功能。例如:

/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection
  • 专注于写摘要

这是(所有注释部分中)最重要的构成。很多出色的注释文档只有一条出色的摘要。

  • 使用一个单一的语句片段

如果可能,尽量避免在摘要中使用一个完整的句子。使用一个表意清晰的语句片段即可,并且使用点号(即 .)表示结束。

  • 采用“描述一个函数或方法会做那些操作、返回什么”这样的句式

如果函数只是简单返回内容,就忽略掉做什么的部分;如果函数返回Void
,就忽略返回值的描述,例如:

/// Inserts `newHead` at the beginning of `self`.
mutating func prepend(newHead: Int)

/// Returns a `List` containing `head` followed by the elements
/// of `self`.
func prepending(head: Element) -> List

/// Removes and returns the first element of `self` if non-empty;
/// returns `nil` otherwise.
mutating func popFirst() -> Element?

当然,popFirst
方法出现了一些例外,我们使用了两句话描述了摘要。当遇到这种情况时,使用分号分隔开多个语句。

  • 描述一个下标具体访问的元素类型,例如:
/// Accesses the `index`th element.
subscript(index: Int) -> Element { get set }
  • 描述一个init方法究竟创建了什么**,例如:
/// Creates an instance containing `n` repetitions of `x`.
init(count n: Int, repeatedElement x: Element)
  • 对于其它的声明来说,描述它们是什么,例如:
/// A collection that supports equally efficient insertion/removal
/// at any position.
struct List {

/// The element at the beginning of `self`, or `nil` if self is
/// empty.
var first: Element?...
1.3.3 这条是可选的,(在summary之后),用一系列段落和列表项来对声明进行补充说明

段落之间用一个空行表示,并且在每一段中使用完整的语句描述。例如:

/// Writes the textual representation of each ← Summary
/// element of `items` to the standard output.
/// ← Blank line/// The textual representation for each item `x` ← Additional discussion
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed  ⎫
/// between items.                             ⎟
/// - Parameter terminator: text to be printed ⎬ Parameters section
/// at the end.                                ⎟
///                                            ⎭
/// - Note: To print without a trailing        ⎫
/// newline, pass `terminator: ""`             ⎟
///                                            ⎬ Symbol commands
/// - SeeAlso: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`.   ⎭
public func print(
  items: Any..., separator: String = " ", terminator: String = "\n")

一些流行的开发工具,例如:Xcode,可以对文档中的下列关键字做特殊处理,并突出显示它们:
Attention | Author | Authors | Bug Complexity | Copyright | Date | Experiment Important | Invariant | Note | ParameterParameters | Postcondition | Precondition | Remark Requires | Returns | SeeAlso | Since Throws | Todo | Version | Warning


第二部分 命名规则

2.1 为了更清晰的用法而改进

2.1.1 包含所有为了避免歧义的单词

包含所有当阅读代码时会引起歧义的单词。例如:定义一个在集合中删除元素的方法:

extension List { 
    public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)

如果忽略掉函数签名中的at,remove就会被人误解为在集合中搜索x并且删除,而不是把x当成要删除元素的索引。

employees.remove(x) // unclear: are we removing x?
2.1.2 忽略不需要的单词

每一个单词都应该为它自己的表意尽职尽责。用更多单词来表明含义或避免混淆是没问题的。但是,对于那些读者已经可以顺利推断出含义的词,应该被忽略,特别是那些仅仅用来重复类型信息的词。例如:

public mutating func removeElement(member: Element) -> Element?
allViews.removeElement(cancelButton)

在这个例子里,Element没有在调用时传递更多有用的信息。把API设计成这样会更好:

public mutating func remove(member: Element) -> Element?
allViews.remove(cancelButton) // clearer

有时,重复类型信息是必要的,它可以避免混淆,但大多数时候,我们应该用一个单词来描述参数承担的角色而不是它的类型。具体参考下一条。

2.1.3 根据变量、参数以及关联类型(associated type)的角色为它们命名,而不是根据它们的类型约束为它们命名

例如:

var string = "Hello"
protocol ViewController { 
    associatedtype ViewType : View
}
class ProductionLine { func restock(from widgetFactory: WidgetFactory)
}

像上面这样把类型当成名称来用并不会提高代码的简洁性和可读性。我们应该基于一个实体的角色来为它命名,像这样:

var greeting = "Hello"

protocol ViewController { 
    associatedtype ContentView : View
}
class ProductionLine { 
    func restock(from supplier: WidgetFactory)
}

如果associatedtypeprotocol关联性非常强,甚至protocol的名称就是associatedtype要承担的角色。此时,为了避免名称冲突,应该在associatedtype的名称末尾添加Type后缀,例如:

protocol Sequence { 
    associatedtype IteratorType : Iterator
}
2.1.4 为弱类型信息参数承担的角色提供补偿

特别是一个参数的类型是NSObjectAnyAnyObject或者是IntString这样的基本类型时,类型自带信息和使用它们的上下文不能充分表达它们的使用意图。例如,对于下面这个例子来说,看声明是清晰的,但用起来,确是含糊的。

func add(observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) // vague

为了解决这个问题,在每个弱类型信息参数前面,加上一个表示角色的名词

func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) // clear

2.2 尽全力做到使用连贯

2.2.1 倾向于那些在使用时可以形成正确英语语法的名字

例如,下面这些表达方式是正确的:

x.insert(y, at: z)           “x, insert y at z”
x.subViews(havingColor: y)   “x's subviews having color y”
x.capitalizingNouns()        “x, capitalizing nouns”

而下面这些则是错误的:

x.insert(y, position: z)
x.subViews(color: y)
x.nounCapitalize()

通常,对于那些在调用时,不影响方法核心语义的参数,为了表意的连贯,让他们的参数有更简单的形式也是可接受的。例如:

AudioUnit.instantiate( 
    with: description, 
    options: [.inProcess], completionHandler: stopProgressBar)
2.2.2 在工厂方法的名字前使用make

,例如:x.makeIterator()
2.2.2.1 对于构造函数和工厂方法来说,调用它们时形成的英文语句中不应包含第一个参数,例如,下面的代码是正确的:

let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)

在下面的代码中,API的作者尝试在方法被调用时,通过第一个参数的名字创建语法连贯的表达方式(但这样是错误的):

let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)

实际上,这条原则和下面对参数命名的原则一起,决定了绝大多数时候,(构造函数和工厂方法)的第一个参数总是会有external name的。除非,调用时体现的是一个“无损类型转换(full-width type conversion)”

let rgbForeground = RGBColor(cmykForeground)

2.2.2.2 根据函数或方法的副作用为它们命名

  • 没有副作用的方法或函数应该使用一个名词表达方式,例如:x.distance(to: y)i.successor()
  • 有副作用的方法或函数应该使用一个使役动词命名,例如:print(x)x.sort()x.append(y)
  • 总是成对命名mutating和non mutating方法。一个mutating方法通常会伴随一个non mutating变体,它们表达类似的语义,但是non mutating版本返回一个新对象,而不是在原有对象上就地处理;
    • 当方法原生通过动词表现时,用这个使役动词命名方法的mutating版本,在这个使役动词后面添加ed
      或ing
      后缀命名对应的non mutating版本;

    • 例如:mutating - x.sort(),non mutating - x.sorted()

    • 例如:mutating - x.append(y),non mutating - x.appedning(y)

    • 当方法原生通过名词表现时,使用名词来命名方法的non mutating版本,在名词前添加form定义mutating版本;

    • 例如:non mutating -x.union(z),mutating - y.formUnion(z)

    • 例如:non mutating - c.successor(i),mutating - y.formSuccessor(&i)

关于eding的补充说明
应优先考虑使用动词的过去分词为non mutating方法命名(通常是ed
结尾)

例如:

/// Reverses self in-place.
mutating func reverse()
/// Returns a reversed copy of self.
func reversed() -> Self
...
x.reverse()
let y = x.reversed()

当动词带有一个直接宾语导致添加ed会造成语法错误时,使用动词的现在进行时(添加ing)为non mutating方法命名
例如:

/// Strips all the newlines from self
mutating func stripNewlines()

/// Returns a copy of self with all the newlines stripped.
func strippingNewlines() -> String
...
s.stripNewlines()
let oneLine = t.strippingNewlines()

2.2.2.3 使用Bool语义的方法或属性时,当这是一个non mutating操作时,它们读上去应该像是对调用对象的断言
例如:x.isEmptyline1.intersects(line2)

2.2.2.4 如果protocol用于为某种事物进行定义,这个protocol应该使用名词命名
例如:Collecion

2.2.2.5 如果protocol用于表达某种事物的能力,这个protocol的名字,应该使用ableibleing后缀
例如:EquatableProgressReporting

2.2.2.6 其它的类型、属性、变量和常量应该使用名词;

2.3 正确使用专业术语

Term of Art
Term of Art - 指一个在特定领域或专业里,有明确特殊含义的名词或短语(fn)。

2.3.1 避免使用复杂晦涩的单词

如果有更为常用的单词表达同样的含义,就避免使用复杂晦涩的单词。例如,(当我们要表达“表皮”这个含义的时候),如果“skin”已经充分表达意图了,就不要使用“epidermis”。虽然专业术语是重要的沟通方式,但只有需要表达重要含义,使用普通词会导致信息丢失的时候,才使用专业术语。

2.3.2 如果使用term of art,就始终坚持它在特定领域的含义

唯一一个需要使用专业术语,而不是普通单词的场景,就是需要严谨准确的表达某些含义,而使用非术语会带来歧义的时候。
因此,如果API中使用了专业术语,就要坚持使用这个术语被广为接受的含义(fn)。

  • 不要让专家感到意外:不要使用专业术语单词的其它含义,这会让特定领域的专业人员赶到意外,甚至愤怒;
  • 不要让初学者感到困惑:当一些初学者尝试理解专业术语时,他们可能会在Web上找到这些单词的非专业领域含义(注:言外之意也就是仅在必要的时候才使用专业术语);
2.3.3 避免使用缩写

尤其是对于那些仅在特定领域里才使用的缩写,他们就像特定人群使用的暗号一样,高效并且有局限性。是否能正确理解它们的含义,完全取决于是否可以正确的“猜”出缩写的各个单词。

因此,当一定要使用缩写时,至少让你的缩写可以很方便的在搜索引擎找到(没有歧义)的解释。

2.3.4 尊重专业习惯

不要以牺牲专业习惯为代价,尝试让新手更容易理解专业术语。例如:

  • 把一段(内存)连续的数据结构定义为Array比定义成List要好的多。尽管对于新手来说,List可能更容易表现出“一列事物”这样的概念。在现代计算机中,Array是很基础的概念,所以每一个开发者都应该知道,或者立即去了解,什么是一个Array。尝试使用开发者都熟知的词汇,初学者会在提问和使用搜索引擎时,更容易获得帮助;
  • 在某个特定领域里,例如数学,使用sin(x)表示正弦函数依然是众所
    周知的事情,使用诸如
    verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
    这样的方式反而让人觉得不知所云(尽管这是在解释什么是正弦)。另外,尽管我们刚刚说过避免使用缩写,却又使用了sin(x)替代了sine(x)。是的,这次,专业习惯在决策考量中占据了更大的比重。数十年来,任何和数学打交道的从业者们已然习惯了使用sin(x),我们应该尊重这个习惯。

第三部分 约定俗成

3.1 一般习俗
3.1.1 使用文档明确标注哪些算法复杂度不是O(1)的computed property

人们通常会认为访问一个computed property不会带来严重的性能损耗,因为在使用它们的时候,和使用一个stored property是感受不到区别的。因此,当computed property会引起性能问题时,要明确告知开发者。

3.1.2 相比自由函数(free function),更倾向于使用方法和属性解决问题

仅在一些特定的情况使用自由函数。例如:
1.要完成的任务不针对特定的self对象:min(x, y, z)
2.函数用于执行通用性功能:print(x)
3.调用函数的语法在特定领域里属于“习惯性用法”:sin(x)

3.1.3 使用以下命名方式

类型和protocols使用UpperCamelCase进行命名,其它都使用lowerCamelCase命名。对于“首字母缩略词(Acronyms and Initialisms)”则采用以下原则:

  • 美式英语中常用的由全大写字母构成的的“首字母缩略词”应该根据使用的上下文环境,统一使用大写或小写字母,例如:
var utf8Bytes: [UTF8.CodeUnit]
var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer
  • 其它的“首字母缩略词”应按照一般单词处理,例如:
var radarDetector: RadarScanner
var enjoysScubaDiving = true
3.1.4 方法可以共享一个公共的名字

当方法表达相似的含义或它们用在不同的领域时,可以共享同一个名字。例如:

  • 下面的方法是被提倡的,因为它们本质上表达的含义是相同的:
extension Shape { 
   /// Returns `true` iff `other` is within the area of `self`. 
   func contains(other: Point) -> Bool { ... } 

   /// Returns `true` iff `other` is entirely within the area of `self`. 
   func contains(other: Shape) -> Bool { ... } 
   /// Returns `true` iff `other` is within the area of `self`. 
   func contains(other: LineSegment) -> Bool { ... }
}

并且,由于几何和集合属于两个不同的技术领域,因此,同一个程序中,在集合中使用contains也是没问题的:

extension Collection where Element : Equatable { 
   /// Returns `true` iff `self` contains an element equal to
   /// `sought`. 
   func contains(sought: Element) -> Bool { ... }
}

但是,在下面的例子中,不同的index方法表达完全不同的语义,它们应该使用不同的名字:

extension Database { 
   /// Rebuilds the database's search index
   func index() { ... } 

   /// Returns the `n`th row in the given table. 
   func index(n: Int, inTable: TableID) -> TableRow { ... }
}

最后,避免“通过方法的返回值”实施重载行为,它们会在使用type inference的时候,带来歧义。

extension Box { 
   /// Returns the `Int` stored in `self`, if any, and 
   /// `nil` otherwise. 
   func value() -> Int? { ... } 

   /// Returns the `String` stored in `self`, if any, and 
   /// `nil` otherwise. 
   func value() -> String? { ... }
}

3.2 关于参数

func move(from start: Point, to end: Point)
3.2.1 让参数名为文档服务

尽管在函数或方法被调用的时候,参数名并不会出现,但它们对解释函数或方法的用途有重要作用。
因此,选择那些可以让文档更易读的参数名。例如,下面的例子里,注释文档读起来就很自然:

/// Return an `Array` containing the elements of `self`
/// that satisfy `predicate`.
func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
/// Replace the given `subRange` of elements with `newElements`.
mutating func replaceRange(_ subRange: Range, with newElements: [E])

而下面这些名字编写的文档,既不易读,还会带来语法错误:

/// Return an `Array` containing the elements of `self`
/// that satisfy `includedInResult`.
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]

/// Replace the range of elements indicated by `r` with
/// the contents of `with`.
mutating func replaceRange(_ r: Range, with: [E])
3.2.2 使用默认参数简化绝大多数的应用场景

如果一个参数在绝大多数时候都会使用同一个值,应该考虑为它指定默认值。
参数默认值可以通过隐藏次要信息提高代码的可读性,例如:

let order = lastName.compare( 
    royalFamilyName, options: [], range: nil, locale: nil)

(如果后三个参数都有默认值),compare调用读起来就会简单的多:

let order = lastName.compare(royalFamilyName)

另外,如果你之前使用过方法家族(method families),你就更应该考虑使用是否可以用带有默认参数的方法来替代它们。这不仅可以帮助开发者更容易了解API的用法,也可以减轻对“家族成员”的认知负担。例如:

extension String { 
    /// ...description... 
    public func compare( 
        other: String, options: CompareOptions = [], 
        range: Range? = nil, locale: Locale? = nil
    ) -> Ordering
}

尽管上面的compare看上去并不简单,但它总比下面的这个方法家族看上去简单多了:

extension String { 
    /// ...description 1... 
    public func compare(other: String) -> Ordering 
    /// ...description 2... 
    public func compare(other: String, options: CompareOptions) -> Ordering 
    /// ...description 3... 
    public func compare( 
        other: String, options: CompareOptions, range: Range) -> Ordering 
    /// ...description 4... 
    public func compare( 
        other: String, options: StringCompareOptions, 
        range: Range, locale: Locale) -> Ordering
}

方法家族中的每一个成员都需要单独的文档注释,都需要开发者去了解。为了能在调用时确认使用的成员,开发者不仅需要了解家族中的每一个方法,还偶尔会被foo(bar: nil)foo()之间的差别吓一跳。因此,在一堆“看上去都差不多”的方法中,指出到底调用了哪个方法是一件让人头疼的事情。而用一个方法,合理搭配上默认参数,可以极大改进开发体验。

3.2.3 倾向于从参数列表的末尾开始安排带有默认值的参数

通常,如果参数没有默认值,它会对方法的执行语义有更为重要的影响。并且,把带有默认值的参数放在最后,可以让方法的调用方式更为一致。

@3.3 关于参数Label

func move(from start: Point, to end: Point)
x.move(from: x, to: y)
3.3.1 当区分参数没有意义时,忽略所有的参数label

例如在下面的两个方法里,区分参数时没意义的,它们的所有参数都不应该带有label:

min(number1, number2), zip(sequence1, sequence2)
3.3.2 如果初始化方法执行了不会带来信息丢失的类型转换,应该忽略初始化方法第一个参数的label

例如:Int64(someUint32)

如果函数的功能是执行类型转换,那么第一个参数应该总是源类型(source of conversion),例如:

extension String { 
    // Convert `x` into its textual representation in the given radix 
    init(_ x: BigInt, radix: Int = 10) ← Note the initial underscore
}
text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16)

如果类型转换会带来信息丢失,应该在参数前面添加一个描述信息处理方式的label,例如:

extension UInt32 { 
    /// Creates an instance having the specified `value`. 
    init(_ value: Int16)            ← Widening, so no label 
    /// Creates an instance having the lowest 32 bits of `source`. 
    init(truncating source: UInt64) 
    /// Creates an instance having the nearest representable 
    /// approximation of `valueToApproximate`. 
    init(saturating valueToApproximate: UInt64)
}
3.3.3 如果第一个参数在方法被调用时形成了介词短语,应该给第一个参数添加label

并且,参数的label也应该用介词开头,例如:x.removeBoxes(havingLength: 12)
但是这条也有例外:当方法的前两个参数一起表达一个动作的抽象时,应该忽略第一个参数的label前面的介词。
例如,下面的这两个例子,它们的第一个参数都不应该以介词开头:

a.move(toX: b, y: c)
a.fade(fromRed: b, green: c, blue: d)

遇到这种情况时,我们应该把介词放到参数label前面,这样可以更清楚的表达要执行的动作,例如:

a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
3.3.4 否则,如果第一个参数(在方法调用时)形成了一个语法正确的短语,就忽略第一个参数的label

并且,应该在方法名后面添加必要的前置单词。例如:x.addSubview(y)
这条指南的另外一个含义是,如果第一个参数不能在调用时和函数名形成语法正确的短语,它就应该有一个label,例如:

view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted( 
    isOrderedBefore: Student.namePrecedes)

这里还有一点要强调的是,在调用时(忽略掉第一个参数的label)形成的短语不仅要语法正确,还要表达正确、无歧义的语义。例如,下面这些例子,虽然语法正确,但却是有歧义的(因此,我们不应该忽略第一个参数的label):

view.dismiss(false)      Don't dismiss? Dismiss a Bool?
words.split(12)          Split the number 12
3.3.5(除了第一个参数之外),为所有其它参数设置label

第四部分 特殊情况

4.1 在你的API里,为closure的参数和tuple成员设置label

为这些元素设置的label好处有二:
1.可以极大提升文档注释的表达能力;
2.让访问tuple成员的代码更易读;

例如:

/// Ensure that we hold uniquely-referenced storage for at least
/// `requestedCapacity` elements.
///
/// If more storage is needed, `allocate` is called with
/// `byteCount` equal to the number of maximally-aligned
/// bytes to allocate.
///
/// - Returns:
/// - reallocated: `true` iff a new block of memory
/// was allocated.
/// - capacityChanged: `true` iff `capacity` was updated.
mutating func ensureUniqueStorage( 
    minimumCapacity requestedCapacity: Int, 
    allocate: (byteCount: Int) -> UnsafePointer<Void>
) -> (reallocated: Bool, capacityChanged: Bool)

尽管从技术上来说,我们在closure中使用的是参数label(这里指byteCount),但在注释文档中,我们应该把它当做参数名称来使用。
在函数内部调用这个closure时,这个调用形成的表达方式,和调用一个“名称中不包含第一个参数(注:这应该是相对于名称中会带有介词的函数名而言的)”的函数是一致的:
allocate(byteCount: newCount * elementSize)

4.2 留意那些无约束类型带来的多态效果(unconstrained polymorphism)

特别是AnyAnyObject
或者泛型参数,它们很容易在方法重载时意外带来歧义。例如,考虑下面两个重载的方法:

struct Array { 
    /// Inserts `newElement` at `self.endIndex`. 
    public mutating func append(newElement: Element) 

    /// Inserts the contents of `newElements`, in order, at 
    /// `self.endIndex`.
    public mutating func append< 
        S : SequenceType where S.Generator.Element == Element
    >(newElements: S)
}

这两个方法形成了一个语义家族(semantic family),它们的参数看上去是截然不同的。但是,当ArrayElement
的类型是Any时,一个单一的元素有可能和一个元素集合的类型是相同的。
例如,在下面的例子里,我们究竟应该认为[2, 3, 4]是一个Any,还是把它理解为是一个[Any]呢?

var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?

为了避免这个歧义,我们可以给第二个重载版本的参数,添加一个label:

struct Array { 
/// Inserts `newElement` at `self.endIndex`. 
public mutating func append(newElement: Element) 

/// Inserts the contents of `newElements`, in order, at 
/// `self.endIndex`. 
public mutating func append< 
S : SequenceType where S.Generator.Element == Element 
> (contentsOf newElements: S)}

注意到新添加的参数label是如何和文档注释匹配的了么?这样,编写文档注释实际上也是在提醒作者每个API的用途。

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

推荐阅读更多精彩内容