Scala破冰之旅

即使水墨丹青,何以绘出半妆佳人。

Scala是一门优雅而又复杂的程序设计语言,初学者很容易陷入细节而迷失方向。这也给我的写作带来了挑战,如果从基本的控制结构,再深入地介绍高级的语法结构,难免让人生厌。

为此,本文另辟蹊径,尝试通过一个简单有趣的例子,概括性地介绍Scala常见的语言特性。它犹如一个迷你版的Scala教程,带领大家一起领略Scala的风采。

问题的提出

有一名体育老师,在某次离下课还有五分钟时,决定玩一个游戏。此时有100名学生在上课,游戏的规则如下:

  1. 老师先说出三个不同的特殊数(都是个位数),比如3, 5, 7;让所有学生拍成一队,然后按顺序报数;

  2. 学生报数时,如果所报数字是「第一个特殊数(3)」的倍数,那么不能说该数字,而要说Fizz;如果所报数字是「第二个特殊数(5)」的倍数,要说Buzz;如果所报数字是「第三个特殊数(7)」的倍数,要说Whizz

  3. 学生报数时,如果所报数字同时是「两个特殊数」的倍数,也要特殊处理。例如,如果是「第一个(3)」和「第二个(5)」特殊数的倍数,那么也不能说该数字,而是要说FizzBuzz。以此类推,如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz

  4. 学生报数时,如果所报数字包含了「第一个特殊数」,那么也不能说该数字,而是要说Fizz。例如,要报13的同学应该说Fizz

  5. 如果数字中包含了「第一个特殊数」,需要忽略规则23,而使用规则4。例如要报35,它既包含3,同时也是57的倍数,要说Fizz,而不能说BuzzWhizz

  6. 否则,直接说出要报的数字。

形式化

3, 5, 7为例,该问题可形式化地描述为:

r1: times(3) => Fizz || 
    times(5) => Buzz ||
    times(7) => Whizz

r2: times(3) && times(5) && times(7) => FizzBuzzWhizz ||
    times(3) && times(5) => FizzBuzz  ||
    times(3) && times(7) => FizzWhizz ||
    times(5) && times(7) => BuzzWhizz

r3: contains(3) => Fizz

rd: others => string of others

spec: r3 || r2 || r1 || rd

其中,times(3) => Fizz表示:当要报的数字是3的倍数时,则说Fizz;其他以此类推。

建立测试环境

首先搭建测试环境,建立反馈系统。这里使用scalatest的测试框架,它也是作者偏爱的测试框架之一。

import org.scalatest.{FunSpec, Matchers}

class RuleSpec extends FunSpec with Matchers {
  describe("World") {
    it ("should not be work" ) {
      true should be(false)
    }
  }
}

运行测试用例,与预期相符,测试失败;证明测试环境可工作,删除该用例,然后开启新的旅程。

第一个测试用例

先建立了一个规则:new Times(3, "Fizz"),它表示如果是3的倍数,则报Fizz。此时,如果输入数字3*2,断言预期的结果为Fizz

it ("times(3) -> Fizz" ) {
  new Times(3, "Fizz").apply(3 * 2) should be("Fizz")
}
主构造函数

使用Scala中,直接在类定义的首部直接定义「主构造函数」,可以消除重复的样板代码。

class Times(n: Int, word: String) {
  def apply(m: Int): String = "Fizz"
}
类型的后缀修饰

Scala将类型的修饰放在后面,以便实现风格的「一致性」,包括:

  • 变量的类型修饰
  • 函数返回值的类型修饰
def apply(m: Int): String = "Fizz"
类型推演

定义变量时,可以通过初始化值的类型推演出变量类型。

val i = 0

等价于

val i: Int = 0

事实上,当函数体比较短小时,可以一眼看出函数返回值类型,也可以略去函数返回值的类型。例如Times.apply的返回值类型可以根据返回值自动推演为String类型。

def apply(m: Int) = "Fizz"

等价于

def apply(m: Int): String = "Fizz"
apply方法

apply方法是一个特殊的方法,它可以简化方法调用的形式,使其行为更贴近函数的语义。在特殊的场景下,能够改善代码的表达力。

it ("times(3) -> Fizz" ) {
  new Times(3, "Fizz").apply(3 * 2) should be("Fizz")
}

等价于:

it ("times(3) -> fizz" ) {
  new Times(3, "Fizz")(3 * 2) should be("Fizz")
}

实现Times

因为Times的逻辑较为简单,可以快速实现它。

class Times(n: Int, word: String) {
  def apply(m: Int): String = 
    if (m % n == 0) word else ""
}
万物皆是对象

Scala并没有针对「基本类型」(例如int),「数组类型」(例如int[])定义特殊的语法,它将世间万物都看成对象。

其中,m % n等价于m.%(n),而%只不过是Int的一个普通方法而已。

面向表达式

Scala是一门面向表达式的语言,它所有的程序结构都具有值,包括if-else表达式。更有甚则,函数调用也可以认为是表达式求值的过程,函数原型末尾的=号更显式地表达了这个意图。

使用case类

可以将Times设计为case类。

case class Times(n: Int, word: String) {
  def apply(m: Int): String =
    if (m % n == 0) word else ""
}

当构造一个Times实例时,可以使用其「伴生对象」提供的工厂方法,从而略去new关键字,简化代码实现。

it ("times(3) -> fizz" ) {
  Times(3, "Fizz")(3 * 2) should be("Fizz")
}

实现Contains

有了Times实现的基础,可以很轻松地实现Contains的测试用例。

it ("contains(3) -> fizz" ) {
  Contains(3, "Fizz")(13) should be("Fizz")
}

依次类推,Contains可以快速实现为:

case class Contains(n: Int, word: String) {
  def apply(m: Int): String =
    if (m.toString.contains(n.toString)) word else ""
}

此时,测试通过了。

省略括号

m.toString等价于m.toString()。按照惯例,如果函数没有副作用,则可以略去小括号;相反,如果产生副作用,则显式地加上小括号用于警示。

如果函数定义时就没有使用小括号,用于表达函数是无副作用的;此时如果用户画蛇添足,添加多余的小括号,将产生编译错误。

实现默认规则

对于默认规则,它只是简单地将输入的数字转变为字符串表示形式。

it ("default rule" ) {
  Default()(2) should be("2")
}

其中,Default可以快速实现为:

case class Default() {
  def apply(m: Int): String = m.toString
}
定制伴生对象

上述实现中,case class Default(),及其调用点Default()(2),不能略去()。这非常讨厌,可以自行定制伴生对象的apply工厂方法,改善表达力。

class Default {
  def apply(m: Int) = m.toString
}

object Default {
  def default = new Default
}

这里使用了default替代apply的工厂方法,一方面消除了函数参数个数的歧义,另一方面保证了原有的语义。此时,可以删除测试用例中冗余的()

import Default._

it ("default rule" ) {
  default(2) should be("2")
}

值得庆幸的是,default并非Scala的保留字。

实现AllOf

接下来,实现具有两个之间具有「逻辑与」关系的复合规则。先建立一个简单的测试用例:

it ("times(3) && times(5) -> FizzBuzz" ) {
  AllOf(Times(3, "Fizz"), Times(5, "Buzz"))(3*5) should be("FizzBuzz")
}

为了快速通过测试,可以先打桩实现。

case class AllOf(times: Times*) extends Rule {
  def apply(n: Int): String = "FizzBuzz"
}
变长参数

times: Times*表示变长的Times列表,表示可以向AllOf的构造函数传递任意多的Times实例。

事实上,times: Times*的真正类型为scala.collection.mutable.WrappedArray[Times],所以times: Times*拥有普通集合类的一般特征,例如调用map, foreach, foldLeft等方法。

快速实现AllOf

case class AllOf(times: Times*) {
  def apply(n: Int): String = {
    val result = new StringBuilder
    times.foreach ( (t: Times) =>
      result.append(t.apply(n))
    )
    result.toString
  }
}
高阶函数

一般地,可以传递或返回「函数值」的函数常称为「高阶函数」。例如foreach就是一个高阶函数,它通过传递(t: Times) => result.append(t.apply(n))的函数值实现容器的遍历。

其中,该函数字面值的类型为Function1[Times, StringBuilder],表示参数为Times,返回值为StringBuilder的一元函数。

对于此例子,如果你偏爱大括号,可以使用大括号替代小括号。

times.foreach { (t: Times) => 
  result.append(t.apply(n))
}

借助类型推演,还可以去除t的类型修饰。

times.foreach { t => result.append(t.apply(n)) }

其中,apply有特殊的调用语义,因此代码可以更简洁。

times.foreach { t => result.append(t(n)) }

甚至,可以略去一些冗余的语法符号。

times foreach { t => result append t(n) }

因为tforeach的函数体内有且仅出现一次,可以使用占位符简化实现。

times foreach { result append _(n) }
使用foldLeft

事实上,上述AllOf.apply实现可以简化为函数式中常见的「规约」操作。

case class AllOf(times: Times*) {
  def apply(n: Int): String =
    times.foldLeft("") { (acc, t) => acc + t.apply(n) }
}

因为acc, tfoldLeft的函数体中有且仅出现过一次,可以使用占位符代替。

case class AllOf(times: Times*) {
  def apply(n: Int): String =
    times.foldLeft("") { _ + _.apply(n) }
}

同样地,因为apply方法具有特殊的函数调用语义,可以进一步简化实现。

case class AllOf(times: Times*) {
  def apply(n: Int): String = 
    times.foldLeft("") { _ + _(n) }
}
剖析foldLeft

foldLeft实现在TraversableOnce特质中。

trait TraversableOnce[+A] {
  ...
  def foreach[U](f: A => U): Unit

  def foldLeft[B](z: B)(op: (B, A) => B): B = {
    var result = z
    foreach(x => result = op(result, x))
    result
  }
}

foldLeft使用函数式中一个重要的技术:「柯里化」。其中,z为迭代的初始值,op: (B, A) => B中第一个参数为「收集参数」,然后遍历容器中的所有元素,并依次实施op操作。

实现AnyOf

接下来,实现具有两个之间具有「逻辑或」关系的复合规则。先建立一个简单的测试用例:

it ("times(3) -> Fizz || times(5) -> Buzz" ) {
  AnyOf(Times(3, "Fizz"), Times(5, "Buzz"))(3*5) should be("Fizz")
}

为了快速通过测试,可以先打桩实现。

case class AnyOf(times: Times*) extends Rule {
  def apply(n: Int): String = "Fizz"
}
链式调用

鉴于AllOf的基础,可以快速地实现AnyOf的逻辑。

case class AnyOf(times: Times*) {
  def apply(m: Int): String =
    times.map(t => t.apply(m))
      .filterNot(s => s.isEmpty)
      .headOption
      .getOrElse("")
}

AnyOf.apply将每一个Times通过map转换为字符串,然后找到第一个不为空的字符串为止。

此时,测试通过了。首先,可以使用占位符简化实现。

case class AnyOf(times: Times*) {
  def apply(m: Int): String =
    times.map(_.apply(m))
      .filterNot(_.isEmpty)
      .headOption
      .getOrElse("")
}

其次,因为apply具有特殊的语义,实现可以进一步简化。

case class AnyOf(times: Times*) {
  def apply(m: Int): String =
    times.map(_(m)).filterNot(_.isEmpty).headOption.getOrElse("")
}

提取Rule

至此,发现Times, Contains, Default, AllOf, AnyOf都具有相同的结构,可抽象出Rule的概念。

trait Rule {
  def apply(n: Int): String
}

其中,traitScala实现对象组合的重要机制。

实现特质

Times通过extends Rule的方式实现Rule特质。

case class Times(n: Int, word: String) extends Rule {
  def apply(m: Int): String =
    if (m % n == 0) word else ""
}

以此类推,Contains, Default, AllOf, AnyOf实现方式相同,不再重述。

隐式树

AllOf, AnyOf是一个「复合规则」,而Times, Contains, Default表示「原子规则」。它们之间构成了一棵「隐式树」,它们的关键在于抽象的Rule特质。

隐式树

工厂方法

因为Times, Contains, Default, AnyOf, AllOf都具有相同的句法结构,是一种典型的结构性重复设计,可以通过「工厂方法」消除它们之间的重复设计。

另外,为了简单函数调用的方式,可以使用Int => String的一元函数代替Rule特质。

重构测试用例

此时,可以定义一组新的测试用例集合,并使用describe分离用例组,并通过显示地导入所依赖的类型,与既有的用例集共存,互不干扰。

切忌删除既有的Rule特质,以及Times, Contains, Default, AllOf, AnyOf的实现,包括既有的测试用例;否则既有的测试用例失败,重构的安全网被撕破,将会让重构陷入一个极度危险的境界。

总之,重构应该保持小步快跑的基本原则。

按照TDD的规则,可以小步地,安全地逐一驱动实现各个工厂方法。

class RuleSpec extends FunSpec {
  ...
  describe("fizz buzz whizz: using factory") {
    import Rule.times
    
    it ("times(3) -> fizz" ) {
      times(3, "Fizz")(3 * 2) should be("Fizz")
    }
  }
}
实现工厂

times的工厂方法也较容易实现,可以通过搬迁Times的逻辑至此即可。

object Rule {
  def times(n: Int, word: String): Int => String =
    m => if (m % n == 0) word else ""
}

至此,times实现通过测试。

小步快跑

以此类推,通过小步地TDD的微循环,将其他工厂方法驱动实现出来。

class RuleSpec extends FunSpec {
  ...

  describe("fizz buzz whizz: using factory") {
    import Rule.{times, contains, default, allof, anyof}

    it ("times(3) -> fizz" ) {
      times(3, "Fizz")(3 * 2) should be("Fizz")
    }

    it ("contains(3) -> fizz" ) {
      contains(3, "Fizz")(13) should be("Fizz")
    }

    it ("default rule" ) {
      default(2) should be("2")
    }

    it ("times(3) && times(5) -> FizzBuzz" ) {
      allof(times(3, "Fizz"), times(5, "Buzz"))(3*5) should be("FizzBuzz")
    }

    it ("times(3) -> Fizz || times(5) -> Buzz" ) {
      anyof(times(3, "Fizz"), times(5, "Buzz"))(3*5) should be("Fizz")
    }
  }
}

最终,在Rule伴生对象中实现了所有方法。

object Rule {
  def times(n: Int, word: String): Int => String =
    m => if (m % n == 0) word else ""
    
  def contains(n: Int, word: String): Int => String = 
    m => if (m.toString.contains(n.toString)) word else ""
    
  def default: Int => String =
    m => m.toString
  
  def anyof(rules: (Int => String)*): Int => String = 
    m => rules.foldLeft("") { _ + _(m) }
    
  def allof(rules: (Int => String)*): Int => String = 
    m => rules.map(_(m)).filterNot(_.isEmpty).headOption.getOrElse("")
}

恭喜,通过所有测试。此时可以安全地删除Times, Contains, Default, AnyOf, AllOf, trait Rule,以及相关遗留的测试用例了。

类型别名

可以对Int => String定义「类型别名」,消除类型的重复定义。

object Rule {
  type Rule = Int => String

  def times(n: Int, word: String): Rule =
    m => if (m % n == 0) word else ""

  def contains(n: Int, word: String): Rule =
    m => if (m.toString.contains(n.toString)) word else ""

  def default: Rule =
    m => m.toString

  def anyof(rules: Rule*): Rule =
    m => rules.foldLeft("") { _ + _(m) }

  def allof(rules: Rule*): Rule =
    m => rules.map(_(m)).filterNot(_.isEmpty).headOption.getOrElse("")
}

至此,设计已经相当干净了。

微妙的重复

如果将default稍微进行改造,很容易发现times, contains, default之间存在微妙的重复结构。

def times(n: Int, word: String): Rule =
  m => if (m % n == 0) word else ""

def contains(n: Int, word: String): Rule =
  m => if (m.toString.contains(n.toString)) word else ""

def default: Rule =
  m => if (true) m.toString else ""

它们各自拥有隐晦的「匹配规则」,当匹配成功时,执行相应的「转换规则」;其中,default的「匹配规则」比较特殊,因为它总是匹配成功。

因此,三者实现可归结为一种统一的抽象行为:

n => if (matcher) action(n) else ""

提取原子

接下来开始消除times, contains, default三者之间的重复逻辑。此时,先新建一组用例集合,使用describe隔离新老用例集,显式地import所依赖的类型,保证既有测试用例可用。然后按照TDD微循环驱动实现三者之间共同的本质操作:atom

Rule.atom, Matcher.times, Action.to可运行之前,切忌删除Rule.times,及其相应的测试用例。

class RuleSpec extends FunSpec {
  ...
  describe("using atom rule") {
    import Rule.atom
    import Matcher.times
    import Action.to

    it ("times(3) -> fizz" ) {
      atom(times(3), to("Fizz"))(3 * 2) should be("Fizz")
    }
  }
}
快速通过

为了快速通过这个新的测试用例,可以快速搬迁times, to, atom的代码实现。

object Matcher {
  def times(n: Int): Int => Boolean = _ % n == 0
}
object Action {
  def to(word: String): Int => String = _ => word
}

atom也可以快速地实现,当给定一个整数m,如果与matcher匹配成功,则执行action转换;否则返回空字符串。

def atom(matcher: Int => Boolean, action: Int => String): Rule =
  m => if (matcher(m)) action(m) else ""

依次类推,可以逐一搬迁Rule单键对象中的逻辑至MatcherAction,在此不再冗述。

匹配器:Matcher

事实上,Matcher是一个「一元函数」,入参为Int,返回值为Boolean,是一种典型的「谓词」。

OO的角度看,always是一个典型的Null Object

object Matcher {
  type Matcher = Int => Boolean

  def times(n: Int): Matcher = _ % n == 0
  def contains(n: Int): Matcher = _.toString.contains(n.toString)
  def always(bool: Boolean): Matcher = _ => bool
}
执行器:Action

Action也是一个「一元函数」,入参为Int,返回值为String。其本质类似于map操作,将定义域映射到值域。

OO的角度看,nop也是一个典型的Null Object

object Action {
  type Action = Int => String

  def to(str: String): Action = _ => str
  def nop: Action = _.toString
}
规则库:Rule

使用类型别名,atom的函数原型将更加清晰。

import Matcher.Matcher
import Action.Action

def atom(matcher: => Matcher, action: => Action): Rule =
  m => if (matcher(m)) action(m) else ""

此时所有测试通过了,可以安全地删除Rule伴生对象中times, contains, default的实现,只保留atom, anyof, allof三个核心的规则即可;同时,也可以删除遗留的测试用例集。

Rule的概念中分离Matcher, Action是「正交设计」的一个典范。它不仅让Rule的职责更加单一,而且使得Rule, Matcher, Action三个变化方向能够保持独立地变化,互不影响,相互正交。

最终,Rule实现如下,它只保留了atom, anyof, allof三个核心规则。

object Rule {
  import Matcher.Matcher
  import Action.Action
  
  type Rule = Int => String

  def atom(matcher: => Matcher, action: => Action): Rule =
    m => if (matcher(m)) action(m) else ""

  def anyof(rules: Rule*): Rule =
    m => rules.map(_(m)).filterNot(_.isEmpty).headOption.getOrElse("")

  def allof(rules: Rule*): Rule =
    m => rules.foldLeft("") { _ + _(m) }
}
按名传递

matcher: => Matcher, action: => Action是按照by-name传递参数的,在实参传递形参过程中,并未对实参进行立即求值,而将求值推延至形参调用点。

也就是说,求值推延至if (matcher(m)) action(m)语句才展开调用的。

隐式树

"Composition Everywhere".

Rule是问题最核心的抽象,也是设计的灵魂所在。从语义上Rule分为两种基本类型,并且它们之间形成了隐式的「树型」结构,体现了「组合式设计」的强大威力。

  • 原子规则:atom
  • 复合规则: anyof, anyof

事实上,任何复杂的软件系统本质上是由众多的「原子」构成,并通过「组合规则」组装起来,从而形成万千的世界,这正是「组合式设计」的精髓所在。

对于本例,atom构成了系统最小的原子单位,anyof, allof定义了组合的规则,从而完美地解决了这个问题。

构建DSL

基于Rule, Matcher, Action的抽象,该问题可以使用DSL进行描述,具有很强的表现力。

import Rule._
import Matcher._
import Action._

object Game {  
  def spec(n1: Int, n2: Int, n3: Int): Rule = {
    val r_n1 = atom(times(n1), to("Fizz"))
    val r_n2 = atom(times(n2), to("Buzz"))
    val r_n3 = atom(times(n3), to("Whizz"))

    val r3 = atom(contains(n1), to("Fizz"))
    val r2 = allof(r_n1, r_n2, r_n3)
    val rd = atom(always(true), nop);

    anyof(r3, r2, rd)
  }
}
应用程序

基于DSL,构建应用程序也变得较为简单了。

object Main extends App {
  def start(n: Int)(n1: Int, n2: Int, n3: Int): Unit = {
    val saying = Game.spec(n1, n2, n3)
    (1 to n) foreach { n => println(s"${n} -> ${saying(n)}") }
  }

  start(100)(3, 5, 7)
}
完备用例集

而对于测试用例,以3, 5, 7为例,可以对测试用例进行整理,形成完备的用例集。此处使用「数据驱动」的方式组织用例,消除用例的重复代码,并改善表达力。

import org.scalatest.{Matchers, PropSpec}
import org.scalatest.prop.TableDrivenPropertyChecks

class RuleSpec extends PropSpec with TableDrivenPropertyChecks with Matchers {
  val specs = Table(
    ("n",         "expect"),
    (3,           "Fizz"),
    (5,           "Buzz"),
    (7,           "Whizz"),
    (3 * 5,       "FizzBuzz"),
    (3 * 7,       "FizzWhizz"),
    ((5 * 7) * 2, "BuzzWhizz"),
    (3 * 5 * 7,   "FizzBuzzWhizz"),
    (13,          "Fizz"),
    (35/*5*7*/,   "Fizz"),
    (2,           "2")
  )

  property("fizz buzz whizz") {
    val spec = Game.spec(3, 5, 7)
    forAll(specs) { spec(_) should be (_) }
  }
}

语义模型

归纳上述设计,可以得到问题的语义模型。

Rule:    Int => String
Matcher: Int => Boolean
Action:  Int => String

其中,Rule存在三种基本的类型:

Rule: atom | allof | anyof

三者之间构成了隐式的「树型结构」。

atom: (Matcher, Action) => String
allof: rule1 && rule2 ... 
anyof: rule1 || rule2 ... 

如果从OO的角度看,该问题的领域模型如下图所示。

领域模型

Github

FizzBuzzWhizz的实现可以在Github上找到。

总结

本文通过对FizzBuzzWhizz小游戏的设计和实现,首先尝试使用Scala的面向对象技术,然后采用函数式的设计;过程采用TDD小步快跑,演进式地完成了所有功能。

中间遇到了「特质」,「子类化多态」,「case类」,「类型别名」,「伴生对象」,「变长参数」,「惰性求值」,「高阶函数」,「柯里化」,「本地方法」等常用的技术。经过这个例子的实践,相信大家对Scala有了一个大体的印象和感觉,接下来让我们开启Scala的星际之旅吧。

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

推荐阅读更多精彩内容