Swift 4.2 更新指北(译)

一、概述

好消息,Swift 4.2 在 Xcode 10 beta 版上可以使用了,在 Swift 4.1 的基础上更新了很多语言特性,为 Swift 5 中 ABI 稳定做好准备。

这篇文章包含了 Swift 4.2 中的重大的改变。因为 Swift 4.2 需要 Xcode 10,所以请下载安装最新的 Xcode 测试版本。

二、准备

Swift 4.2 和 Swift 4.1 源码兼容,但是和其他发布版本的二进制不兼容。Swift 4.2 是 Swift 5 实现 ABI 稳定(不同的 Swift 版本编译的应用程序和库之间实现兼容)的一个中间阶段。ABI 的特性在集成进最终的 ABI 之前会接收社区的大量反馈。

三、语言演进

在这个版本中有很多新的语言特性。例如,随机数生成,动态成员查找等等

3.1 随机数生成

3.1.1 随机数生成

arc4random_uniform(_:) 返回一个 0 - 9 之间的随机数字。这种实现方式有两个问题:

  • 需要引入 Foundation 框架,在 Linux 下无法工作。
  • Linux 上的随机数生成会产生模偏差(有取模的过程,更容易随机到小的数)。
// Swift 4.1
let digit = Int(arc4random_uniform(10))

Swift 4.2 在标准库中添加了随机数的 API SE-0202

// Swift 4.2
/ 1  
let digit = Int.random(in: 0..<10)
​
// 2
if let anotherDigit = (0..<10).randomElement() {
 print(anotherDigit)
} else {
 print("Empty range.")
}
​
// 3
let double = Double.random(in: 0..<1)
let float = Float.random(in: 0..<1)
let cgFloat = CGFloat.random(in: 0..<1)
let bool = Bool.random()

注:randomElement() 如果 range 是空,返回 nil

3.1.2 数组随机

Swift 4.1 数组随机也是采用 C 函数的形式,这种方式会存在上面提到的问题,而且会存在 Int 和 Int32 转换的问题。

let playlist = ["Nothing Else Matters", "Stairway to Heaven", "I Want to Break Free", "Yesterday"]
let index = Int(arc4random_uniform(UInt32(playlist.count)))
let song = playlist[index]

Swift 4.2 采用了更加简单直接的方式。

if let song = playlist.randomElement() {
 print(song)
} else {
 print("Empty playlist.")
}
3.1.3 洗牌算法

Swift 4.1 不包含任何集合的洗牌算法,所以要采用比较曲折的方式来实现。

// 1
let shuffledPlaylist = playlist.sorted{ _, _ in arc4random_uniform(2) == 0 }
​
// 2
var names = ["Cosmin", "Oana", "Sclip", "Nori"]
names.sort { _, _ in arc4random_uniform(2) == 0 }

Swift 4.2 提供了更加高效更加优雅的实现 Shuffling Algorithms

let shuffledPlaylist = playlist.shuffled()
names.shuffle()

注:使用 shuffled() 来创建一个洗牌后的数组。使用 shuffle) 来将数组洗牌。

3.2 动态成员查找

Swift 4.1 使用下面的方式实现自定义下标操作。

class Person {
  let name: String
  let age: Int
  private let details: [String: String]
  
  init(name: String, age: Int, details: [String: String]) {
    self.name = name
    self.age = age
    self.details = details
  }
  
  subscript(key: String) -> String {
    switch key {
      case "info":
        return "\(name) is \(age) years old."
      default:
        return details[key] ?? ""
    }
  }
}

let details = ["title": "Author", "instrument": "Guitar"]
let me = Person(name: "Cosmin", age: 32, details: details)
me["info"]   // "Cosmin is 32 years old."
me["title"]  // "Author"

Swift 4.2 使用动态成员查找来提供点语法来实现下标调用 Dynamic Member Lookup

// 1
@dynamicMemberLookup
class Person {
  let name: String
  let age: Int
  private let details: [String: String]
  
  init(name: String, age: Int, details: [String: String]) {
    self.name = name
    self.age = age
    self.details = details
  }
  
  // 2
  subscript(dynamicMember key: String) -> String {
    switch key {
      case "info":
        return "\(name) is \(age) years old."
      default:
        return details[key] ?? ""
    }
  }
}

// 3
me.info   // "Cosmin is 32 years old." 
me.title  // "Author"

使用步骤:

  • 标记 Person 为 @dynamicMemberLookup 使下标可以使用点语法
  • 遵守 @dynamicMemberLookup 实现 subscript(dynamicMember:) 方法
  • 使用点语法调用之前定义的下标

注:编译器会在运行时动态评估下标的调用,这样就可以写出像 Python 或者 Ruby 等脚本语言一样类型安全的代码。

动态成员查找不会和类的属性混淆。

me.name // "Cosmin"
me.age // 32

可以使用点语法而非下标来调用 name 和 age。而且派生类可以继承基类的动态成员查找。

@dynamicMemberLookup
class Vehicle {
  let brand: String
  let year: Int
  
  init(brand: String, year: Int) {
    self.brand = brand
    self.year = year
  }
  
  subscript(dynamicMember key: String) -> String {
    return "\(brand) made in \(year)."
  }
}

class Car: Vehicle {}

let car = Car(brand: "BMW", year: 2018)
car.info  // "BMW made in 2018."

可以通过协议拓展给已有类型添加动态成员查找

// 1
@dynamicMemberLookup
protocol Random {}

// 2
extension Random {
  subscript(dynamicMember key: String) -> Int {
    return Int.random(in: 0..<10)
  }
}

// 3
extension Int: Random {}

// 4
let number = 10
let randomDigit = String(number.digit)
let noRandomDigit = String(number).filter { String($0) != randomDigit }

3.3 枚举实例集合

Swift 4.1 默认没有提供访问枚举实例集合的方式,所以实现方式不是很优雅。

enum Seasons: String {
  case spring = "Spring", summer = "Summer", autumn = "Autumn", winter = "Winter"
}

enum SeasonType {
  case equinox
  case solstice
}

let seasons = [Seasons.spring, .summer, .autumn, .winter]
for (index, season) in seasons.enumerated() {
  let seasonType = index % 2 == 0 ? SeasonType.equinox : .solstice
  print("\(season.rawValue) \(seasonType).")
}

为了解决这个问题,Swift 4.2 给枚举类型添加了实例数组。

// 1
enum Seasons: String, CaseIterable {
  case spring = "Spring", summer = "Summer", autumn = "Autumn", winter = "Winter"
}

enum SeasonType {
  case equinox
  case solstice
}

// 2
for (index, season) in Seasons.allCases.enumerated() {
  let seasonType = index % 2 == 0 ? SeasonType.equinox : .solstice
  print("\(season.rawValue) \(seasonType).")
}

如果枚举中包含 unavailable,需要将 availablecase 手动维护协议中的 allCases

enum Days: CaseIterable {
  case monday, tuesday, wednesday, thursday, friday
  
  @available(*, unavailable)
  case saturday, sunday
  
  static var allCases: [Days] {
    return [.monday, .tuesday, .wednesday, .thursday, .friday]
  }
}

allCases 中只能添加 weekdays,因为 saturdaysunday 被标记为各个平台不可用。
枚举实例数组中也可以添加有关联值的实例。

enum BlogPost: CaseIterable {
  case article
  case tutorial(updated: Bool)
  
  static var allCases: [BlogPost] {
    return [.article, .tutorial(updated: true), .tutorial(updated: false)]
  }
}

3.4 新的序列方法

Swift 4.1 中的 Sequence 定义了查找指定元素的第一个索引位置或者满足指定条件的第一个元素的方法。

let ages = ["ten", "twelve", "thirteen", "nineteen", "eighteen", "seventeen", "fourteen",  "eighteen", "fifteen", "sixteen", "eleven"]

if let firstTeen = ages.first(where: { $0.hasSuffix("teen") }), 
   let firstIndex = ages.index(where: { $0.hasSuffix("teen") }), 
   let firstMajorIndex = ages.index(of: "eighteen") {
  print("Teenager number \(firstIndex + 1) is \(firstTeen) years old.")
  print("Teenager number \(firstMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.2 为了实现一致性重构了方法名

if let firstTeen = ages.first(where: { $0.hasSuffix("teen") }), 
   let firstIndex = ages.firstIndex(where: { $0.hasSuffix("teen") }), 
   let firstMajorIndex = ages.firstIndex(of:  "eighteen") {
  print("Teenager number \(firstIndex + 1) is \(firstTeen) years old.")
  print("Teenager number \(firstMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.1 也没有定义查找指定元素的最后一个索引的位置和满足指定条件的的最后一个元素等方法。在 Swift 4.1 中我们可能采用下面的方法来处理。

// 1
let reversedAges = ages.reversed()

// 2
if let lastTeen = reversedAges.first(where: { $0.hasSuffix("teen") }), 
   let lastIndex = reversedAges.index(where: { $0.hasSuffix("teen") })?.base, 
   let lastMajorIndex = reversedAges.index(of: "eighteen")?.base {
  print("Teenager number \(lastIndex) is \(lastTeen) years old.")
  print("Teenager number \(lastMajorIndex) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.2 添加了相应的方法,使用方式如下

if let lastTeen = ages.last(where: { $0.hasSuffix("teen") }), 
   let lastIndex = ages.lastIndex(where: { $0.hasSuffix("teen") }), 
   let lastMajorIndex = ages.lastIndex(of: "eighteen") {
  print("Teenager number \(lastIndex + 1) is \(lastTeen) years old.")
  print("Teenager number \(lastMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

3.5 检测序列元素

Swift 4.1 中没有检查序列中所有元素是否满足某个指定条件的方法。不过你可以实现你自己的方法,例如下面检测集合中的元素是否都是偶数。

let values = [10, 8, 12, 20]
let allEven = !values.contains { $0 % 2 == 1 }

Swift 4.2 添加了新的方法,很好的简化了代码,提升了可读性。

let allEven = values.allSatisfy { $0 % 2 == 0 }

3.6 条件遵守更新

Swift 4.2 给拓展和标准库中添加一些条件遵守方面的改进。

3.6.1 拓展中的条件遵守

Swift 4.1 不能在拓展中自动合成 Equatable 的协议实现。例子如下:

// 1
struct Tutorial : Equatable {
  let title: String
  let author: String
}

// 2
struct Screencast<Tutorial> {
  let author: String
  let tutorial: Tutorial
}

// 3 
extension Screencast: Equatable where Tutorial: Equatable {
  // 必须自己实现 == 方法,Swift 4.1 不会自动合成
  static func ==(lhs: Screencast, rhs: Screencast) -> Bool {
    return lhs.author == rhs.author && lhs.tutorial == rhs.tutorial
  }
}

// 4
let swift41Tutorial = Tutorial(title: "What's New in Swift 4.1?", author: "Cosmin Pupăză")
let swift42Tutorial = Tutorial(title: "What's New In Swift 4.2?", author: "Cosmin Pupăză")
let swift41Screencast = Screencast(author: "Jessy Catterwaul", tutorial: swift41Tutorial)
let swift42Screencast = Screencast(author: "Jessy Catterwaul", tutorial: swift42Tutorial)
let sameScreencast = swift41Screencast == swift42Screencast

Swift 4.2 只需要遵守协议,不需要实现。因为编译器会添加一个默认的Equatable协议实现。

extension Screencast: Equatable where Tutorial: Equatable {}

这个特性也同样支持 HashableCodable

// 1
struct Tutorial: Hashable, Codable {
  let title: String
  let author: String
}

struct Screencast<Tutorial> {
  let author: String
  let tutorial: Tutorial
}

// 2
extension Screencast: Hashable where Tutorial: Hashable {}
extension Screencast: Codable where Tutorial: Codable {}

// 3
let screencastsSet: Set = [swift41Screencast, swift42Screencast]
let screencastsDictionary = [swift41Screencast: "Swift 4.1", swift42Screencast: "Swift 4.2"]

let screencasts = [swift41Screencast, swift42Screencast]
let encoder = JSONEncoder()
do {
  try encoder.encode(screencasts)
} catch {
  print("\(error)")
}
3.6.2 条件遵守运行时查询

Swift 4.2 实现条件遵守的动态查询。可以从下面的例子看出。

// 1
class Instrument {
  let brand: String
  
  init(brand: String = "") {
    self.brand = brand
  }
}

// 2
protocol Tuneable {
  func tune()
}

// 3
class Keyboard: Instrument, Tuneable {
  func tune() {
    print("\(brand) keyboard tuning.")
  }
}

// 4
extension Array: Tuneable where Element: Tuneable {
  func tune() {
    forEach { $0.tune() }
  }
}

// 5
let instrument = Instrument()
let keyboard = Keyboard(brand: "Roland")
let instruments = [instrument, keyboard]

// 6
if let keyboards = instruments as? Tuneable {
  keyboards.tune()
} else {
  print("Can't tune instrument.")
}

注:上面在条件遵循的运行时检测中,会输出 "Can't tune instrument.",因为 Instrument类型不遵守 Tuneable 协议,如果是两个 Keyboard 类型就可以。
更多关于 Conditional Conformance 的内容,参考 Swift 4.1 更新指北(译)

3.6.3 Hashable 在标准库中条件遵守增强

在 Swift 4.2 中可选值、数组、字典和区间当他们的元素是 Hashable 的话,他们也是 Hashable

struct Chord: Hashable {
  let name: String
  let description: String?
  let notes: [String]
  let signature: [String: [String]?]
  let frequency: CountableClosedRange<Int>
}

let cMajor = Chord(name: "C", description: "C major", notes: ["C", "E",  "G"], 
                   signature: ["sharp": nil,  "flat": nil], frequency: 432...446)
let aMinor = Chord(name: "Am", description: "A minor", notes: ["A", "C", "E"], 
                   signature: ["sharp": nil, "flat": nil], frequency: 440...446)
let chords: Set = [cMajor, aMinor]
let versions = [cMajor: "major", aMinor: "minor"]

3.7 Hashable 增强

Swift 4.1 中一般会像下面这样实现自定义哈希函数:

class Country: Hashable {
  let name: String
  let capital: String
  
  init(name: String, capital: String) {
    self.name = name
    self.capital = capital
  }
  
  static func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name && lhs.capital == rhs.capital
  }
  
  var hashValue: Int {
    return name.hashValue ^ capital.hashValue &* 16777619
  }
}

let france = Country(name: "France", capital: "Paris")
let germany = Country(name: "Germany", capital: "Berlin")
let countries: Set = [france, germany]
let countryGreetings = [france: "Bonjour", germany: "Guten Tag"]

因为countriesHashable ,所以可以添加到集合或者字典中。但是 hashValue 的实现很难理解并且也不高效。Swift 4.2 通过定义了一个通用的哈希函数来解决这个问题。

class Country: Hashable {
  let name: String
  let capital: String
  
  init(name: String, capital: String) {
    self.name = name
    self.capital = capital
  }
  
  static func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name && lhs.capital == rhs.capital
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(name)
    hasher.combine(capital)
  }
}

Country 中使用 hash(into:) 来替代 hashValue。这个函数使用 combine() 将属性注入到 hasher中。
注:现在实现上很容易,并且性能要比之前的版本高。

3.8 集合中移除元素

在 Swift 4.1 中,想要从集合中移除一个指定的元素,通常会使用 filter(_:) 的实现方式,在 Swift 4.2 添加了 removeAll(_:)

// Swift 4.1
var greetings = ["Hello", "Hi", "Goodbye", "Bye"]
greetings = greetings.filter { $0.count <= 3 }
// Swift 4.2
greetings.removeAll { $0.count > 3 }

3.9 更改布尔值

在 Swift 4.1 中,我们通常会这样实现

extension Bool {
  mutating func toggle() {
    self = !self
  }
}

var isOn = true
isOn.toggle()

Swift 4.2 给 Bool 增加了 toggle()方法

3.10 新的编译器指令

Swift 4.2 定义了表述代码问题的编译器指令

// 1
#warning("There are shorter implementations out there.")

let numbers = [1, 2, 3, 4, 5]
var sum = 0
for number in numbers {
  sum += number
}
print(sum)

// 2
#error("Please fill in your credentials.")

let username = ""
let password = ""
switch (username.filter { $0 != " " }, password.filter { $0 != " " }) {
  case ("", ""):
    print("Invalid username and password.")
  case ("", _):
    print("Invalid username.")
  case (_, ""):
    print("Invalid password.")
  case (_, _):
    print("Logged in succesfully.")
}
  • #warning 用来输出警告信息,表示实现未完全完成
  • #error 强制其他开发者填入 usernamepassword

3.11 新的指针函数

withUnsafeBytes(of:_:)withUnsafePointer(to:_:) 在 Swift 4.1 中只能用于变量,所以必须拷贝一份。Swift 4.2 中该函数支持常量,不需要再保存值。

// Swift 4.1
let value = 10
var copy = value
withUnsafeBytes(of: &copy) { pointer in print(pointer.count) }
withUnsafePointer(to: &copy) { pointer in print(pointer.hashValue) }
// Swift 4.2
withUnsafeBytes(of: value) { pointer in print(pointer.count) }
withUnsafePointer(to: value) { pointer in print(pointer.hashValue) }

3.12 Memory Layout 更新

Swift 4.2 使用 keypath 查找存储属性的内存布局 [SE-0210],具体做法如下:

// 1
struct Point {
  var x, y: Double
}

// 2
struct Circle {
  var center: Point
  var radius: Double
  
  var circumference: Double {
    return 2 * .pi * radius
  }
  
  var area: Double {
    return .pi * radius * radius
  }
}

// 3
if let xOffset = MemoryLayout.offset(of: \Circle.center.x), 
   let yOffset = MemoryLayout.offset(of: \Circle.center.y), 
   let radiusOffset = MemoryLayout.offset(of: \Circle.radius) {
  print("\(xOffset) \(yOffset) \(radiusOffset)")
} else {
  print("Nil offset values.")
}

// 4
if let circumferenceOffset = MemoryLayout.offset(of: \Circle.circumference), 
   let areaOffset = MemoryLayout.offset(of: \Circle.area) {
  print("\(circumferenceOffset) \(areaOffset)")
} else {
  print("Nil offset values.")

注:可以通过 keypath 返回存储属性的内存偏移。计算属性返回 nil,因为没有存储关联。

3.13 模块中的内联函数

在 Swift 4.1 中,不允许在自己的模块中定义内联函数。依次选择 View ▸ Navigators ▸ Show Project Navigator, 右键单击 Sources and 选择 New File。重命名文件为 FactorialKit.swift 并且替换为下面代码块中的代码。

public class CustomFactorial {
  private let customDecrement: Bool
  
  public init(_ customDecrement: Bool = false) {
    self.customDecrement = customDecrement
  }
  
  private var randomDecrement: Int {
    return arc4random_uniform(2) == 0 ? 2 : 3
  }
  
  public func factorial(_ n: Int) -> Int {
    guard n > 1 else {
      return 1
    }
    let decrement = customDecrement ? randomDecrement : 1
    return n * factorial(n - decrement)
  }
}

在 Swift 4.2 中定义为内联的函数会更加高效,所以将 FactorialKit.swift 的代码替换如下。

public class CustomFactorial {
  @usableFromInline let customDecrement: Bool
  
  public init(_ customDecrement: Bool = false) {
    self.customDecrement = customDecrement
  }
  
  @usableFromInline var randomDecrement: Int {
    return Bool.random() ? 2 : 3
  }
  
  @inlinable public func factorial(_ n: Int) -> Int {
    guard n > 1 else {
      return 1
    }
    let decrement = customDecrement ? randomDecrement : 1
    return n * factorial(n - decrement)
  }
}

四、其他更新

下面是 Swift 4.2 中的一些其他改变

4.1 Swift Package Manager 更新
4.1.1 定义 Package 的 Swift 版本

Swift 4.1 在 Package.swift 中定义了swiftLanguageVersions,所以可以在 packages 中定义主版本。

let package = Package(name: "Package", swiftLanguageVersions: [4])

Swift 4.2 中也能通过SwiftVersion定义小版本 [SE-0209]

let package = Package(name: "Package", swiftLanguageVersions: [.v4_2])

能够通过.version(_:)定义之后的版本

let package = Package(name: "Package", swiftLanguageVersions: [.version("5")])
4.1.2 Packages 定义本地版本

在 Swift 4.1 中,可以使用仓库链接为 Package 定义依赖。如果有相互关联的 Package,就会产生额外的问题,所以 Swift 4.2 中引入了本地路径而提案[SE-0201]。

4.1.3 给 Package 添加系统库 Target
4.1.4 Swift 4.1 中系统模块包需要分仓库,这样包管理很难用 ,所以 Swift 4.2 使用系统库 Target 来实现 [SE-0208]
4.2 移除隐式解包可选值

在 Swift 4.1 中,你可以在嵌套类型中使用隐式解包可选值。

let favoriteNumbers: [Int!] = [10, nil, 7, nil]
let favoriteSongs: [String: [String]!] = ["Cosmin": ["Nothing Else Matters", "Stairway to Heaven"], "Oana": nil] 
let credentials: (usermame: String!, password: String!) = ("Cosmin", nil)

Swift 4.2 从数组、字典和元祖中移除了隐式解包可选值 SE-0054

let favoriteNumbers: [Int?] = [10, nil, 7, nil]
let favoriteSongs: [String: [String]?] = ["Cosmin": ["Nothing Else Matters", "Stairway to Heaven"], "Oana": nil] 
let credentials: (usermame: String?, password: String?) = ("Cosmin", nil)

五、未来愿景

可以从这个教程中下载最终的 Playground。Swift 4.2 在 Swift 4.1 诸多特性的基础上进一步做了改进,而且为了2019 年初 Swift 5 的 ABI 稳定做好准备。可以从 Swift CHANGELOG 或者 Swift standard library diffs 中了解更多关于这个版本的改变。也可以从 Swift Evolution 中看出 Swift 5 的改变。你可以给正在审查的提案提交反馈或者自己提交一个提案。到目前为止对 Swift 4.2 有什么喜欢或者不喜欢的地方。可以在论坛中参与讨论。

六、感谢原作者 Cosmin Pupăză

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 这两天的天气真热,热的连饭都吃不下了。 喜欢在吃饭的时候和同事聊聊天,看到新闻说,在印度这么热的天有1500人受不...
    安梓阅读 261评论 0 1
  • 明明什么也没有,可心里却像那江南的烟雨一样,湿漉漉的。
    阿荒姑娘阅读 48评论 0 0
  • 雨夜漫漫,无心睡眠。下雨的时候,总会有一种莫名哀怨的矫情情绪会从潜藏在身体的各个角落偷偷摸摸钻出来慢慢占据...
    喵美男阅读 233评论 0 0