Swift基礎知識相關(二) —— 編碼和解碼(一)

版本記錄

版本號 時間
V1.0 2019.07.22 星期一

前言

這個專題我們就一起看一下Swfit相關的基礎知識。感興趣的可以看上面幾篇。
1. Swift基礎知識相關(一) —— 泛型(一)

開始

首先看下主要內容

主要內容:在本教程中,您將學習Swift中的所有編碼和解碼,探索自定義日期和自定義編碼等基礎知識和高級主題。

然后看些寫作環境

Swift 5, iOS 12, Xcode 10

iOS應用程序的一項常見任務是保存數據并通過網絡發送數據。 但在此之前,您需要通過稱為編碼或序列化(encoding or serialization)的過程將數據轉換為合適的格式。

在應用中使用之前,您還需要將通過網絡發送的已保存數據轉換為合適的格式。 該反向過程稱為解碼或反序列化(decoding or deserialization)

在本教程中,您將通過管理自己的toy store了解有關Swift編碼和解碼的所有信息。 您將在此過程中探索以下主題:

  • snake case and camel case之間切換。
  • 定義自定義編碼key。
  • 使用keyed, unkeyed and nested containers
  • 處理嵌套類型,日期,子類和多態類型。

有很多東西可以了解,所以是時候開始了!

注意:本教程假定您具有JSON的基本知識。 如果您需要快速瀏覽,請查看此 cheat sheet

打開起始項目,通過轉到View ? Navigators ? Show Project Navigator,確保在Xcode中可以看到Project navigator。 打開Nested types

ToyEmployee添加Codable遵守:

struct Toy: Codable {
  ...
}
struct Employee: Codable {
  ...
}

Codable本身不是協議,而是另外兩個協議的別名:EncodableDecodable。 正如您可能猜到的那樣,這兩個協議聲明類型可以編碼為不同的格式并從其中解碼。

您不需要再做任何事情,因為ToyEmployee的所有存儲屬性(stored properties)都是可編碼(codable)的。 默認情況下,Swift標準庫和基礎類型(Swift Standard Library and Foundation )中的許多基本類型(例如,StringURL)都是可編碼的。

注意:您可以將可編碼類型編碼為各種格式,例如Property Lists (PLists)XMLJSON,但是對于本教程,您只能使用JSON。

添加JSONEncoderJSONDecoder來處理toysemployeesJSON編碼和解碼:

let encoder = JSONEncoder()
let decoder = JSONDecoder()

這就是使用JSON所需的全部內容。 第一次編碼和解碼挑戰的時間!


Encoding and Decoding Nested Types

Employee包含一個Toy屬性 - 它是一個嵌套類型(nested type)。 編碼employee的JSON結構與Employee結構匹配:

{
  "name" : "John Appleseed",
  "id" : 7,
  "favoriteToy" : {
    "name" : "Teddy Bear"
  }
}
public struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
}

JSONfavoriteToy中嵌套name,所有JSON鍵與EmployeeToy存儲屬性相同,因此您可以根據數據類型層次結構輕松理解JSON結構。 如果您的屬性名稱與您的JSON字段名稱匹配,并且您的屬性都是Codable,那么您可以非常輕松地轉換為JSON或從JSON轉換。 你現在就試試。

Gifts部門為員工提供他們喜歡的玩具作為生日禮物。 添加以下代碼以將員工的數據發送到禮品部門:

// 1
let data = try encoder.encode(employee)
// 2
let string = String(data: data, encoding: .utf8)!

以下是此代碼的工作原理:

  • 1) 使用encode(_:)employee編碼為JSON(我告訴過你這很簡單!)。
  • 2) 從編碼data創建一個字符串以使其可視化。

注意:按Shift-Return可將playground運行到當前行,或單擊藍色play按鈕。 要查看結果,可以將值打印print到調試器控制臺,或單擊結果側欄中的Show Result按鈕。

編碼過程生成有效數據,因此禮品部門可以重新創建員工:

let sameEmployee = try decoder.decode(Employee.self, from: data)

在這里,您已經使用decode(_:from :)data解碼回Employee ......您已經讓您的員工非常開心。 按藍色play按鈕以運行Playground并查看結果。

是時候進行下一次挑戰!


Switching Between Snake Case and Camel Case Formats

禮品部門API已經從camel caselooksLikeThis)轉換到snake caselooks_like_this_instead)以格式化其JSON的鍵。

但是EmployeeToy的所有存儲屬性都只使用camel case的情況! 幸運的是,Foundation為您提供服務。

打開Snake case vs camel case并在創建編碼器和解碼器之后添加以下代碼,然后再使用它們:

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase

在這里,您將keyEncodingStrategy設置為.convertToSnakeCase以對employee進行編碼。 您還將keyDecodingStrategy設置為.convertFromSnakeCase以解碼snakeData

運行playground并檢查snakeString。 在這種情況下,編碼的employee看起來像這樣(雙關語):

{
  "name" : "John Appleseed",
  "id" : 7,
  "favorite_toy" : {
    "name" : "Teddy Bear"
  }
} 

JSON中的格式現在是favorite_toy,并且您已將其轉換回Employee結構中的favoriteToy。 你再次保存了(員工的出生日!)


Working With Custom JSON Keys

禮品部門再次更改其API以使用與您的EmployeeToy存儲屬性不同的JSON key

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "name" : "Teddy Bear"
  }
}

現在,APIgift取代了favoriteToy

這意味著JSON中的字段名稱將不再與您的類型中的屬性名稱匹配。 您可以定義自定義編碼鍵(custom coding keys )以提供屬性的編碼名稱。 您可以通過向類型添加特殊枚舉來完成此操作。 打開custom coding keys并在Employee類型中添加此代碼:

enum CodingKeys: String, CodingKey {
  case name, id, favoriteToy = "gift"
}

CodingKeys是上面提到的特殊枚舉。 它符合CodingKey并具有String原始值。 這里是您將favoriteToy映射到gift的地方。

如果此枚舉存在,則只有此處出現的情況將用于編碼和解碼,因此即使您的屬性不需要映射,它也必須包含在枚舉中,如nameid在此處所示。

運行playground并查看編碼的字符串值 - 您將看到正在使用的新字段名稱。 由于自定義編碼密鑰custom coding keys,JSON不再依賴于您存儲的屬性。

是時候進行下一次挑戰!


Working With Flat JSON Hierarchies

現在,Gifts部門的API不希望其JSON中有任何嵌套類型,因此它們的代碼如下所示:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : "Teddy Bear"
}

這與您的模型結構不匹配,因此您需要編寫自己的編碼邏輯并描述如何編碼每個EmployeeToy存儲的屬性。

首先,打開Keyed containers。 您將看到一個聲明為EncodableEmployee類型。 它也在擴展中聲明為Decodable。 這種拆分是為了保持你使用Swift結構體獲得的free member-wise初始化程序。 如果在主定義中聲明了init方法,則會丟失該方法。 在Employee中添加此代碼:

// 1
enum CodingKeys: CodingKey {
  case name, id, gift
}

func encode(to encoder: Encoder) throws {
  // 2
  var container = encoder.container(keyedBy: CodingKeys.self)
  // 3  
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  // 4
  try container.encode(toy.name, forKey: .gift)
}

對于您在上面看到的簡單情況,編譯器會自動為您實現encode(to :)。 現在,你自己做了。 這是代碼正在做的事情:

  • 1) 創建一組編碼鍵來表示您的JSON字段。 因為您沒有進行任何映射,所以您不需要將它們聲明為字符串,因為沒有原始值。
  • 2) 創建KeyedEncodingContainer。 這就像您可以在編碼時存儲屬性的字典。
  • 3) 將nameid屬性直接編碼到容器中。
  • 4) 使用禮品密鑰將toy的名稱直接編碼到容器中

運行playground并檢查編碼字符串的值 - 它將匹配本節頂部的JSON。 能夠選擇對哪些鍵進行編碼的屬性為您提供了很大的靈活性。

解碼過程與編碼過程相反。 用這個替換可怕的fatalError("To do")

// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
// 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)

與編碼一樣,對于簡單的情況,編譯器會自動為您生成init(from :),但是您自己就是這樣做的。 這是代碼正在做的事情:

  • 1) 從解碼器獲取一個鍵控容器,它將包含JSON中的所有屬性。
  • 2) 使用適當的類型和編碼key從容器中提取name and id
  • 3) 提取禮物的名稱,并使用它來構建Toy并將其分配給正確的屬性。

添加一行以從平面JSON重新創建employee

let sameEmployee = try decoder.decode(Employee.self, from: data)

這一次,您選擇了哪些屬性來解碼哪些鍵,并有機會在解碼過程中進一步工作。 手動編碼和解碼功能強大,為您提供靈活性。 您將在接下來的挑戰中了解更多相關信息。


Working With Deep JSON Hierarchies

禮品部門希望確保員工的生日禮物只能是玩具,因此其API會生成如下所示的JSON:

{
  "name" : "John Appleseed",
  "id" : 7,
  "gift" : {
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
}

你在toygift里面的toy同時嵌入name。 與Employee層次結構相比,JSON結構添加了額外級別的縮進,因此在這種情況下您需要使用嵌套的鍵控容器(nested keyed containers)作為禮物。

打開嵌套的鍵控容器并將以下代碼添加到Employee

// 1  
enum CodingKeys: CodingKey {  
  case name, id, gift
}
// 2
enum GiftKeys: CodingKey {
  case toy
}
// 3
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  // 4  
  var giftContainer = container
    .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
  try giftContainer.encode(toy, forKey: .toy)
}

這就是上面代碼的工作原理:

  • 1) 創建top-level coding keys
  • 2) 創建另一組編碼鍵,您將使用它來創建另一個容器。
  • 3) 按照您習慣的方式對name and id進行編碼。
  • 4) 創建一個嵌套容器nestedContainer(keyedBy:forKey :)并用它編碼toy

運行playground并檢查編碼的字符串以查看多級JSON。 您可以使用盡可能多的嵌套容器,因為您的JSON具有縮進級別。 在現實世界的API中使用復雜而深入的JSON層次結構時,這很方便。

在這種情況下,解碼很簡單。 添加以下擴展名:

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    let giftContainer = try container
      .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
  }
}

let sameEmployee = try decoder.decode(Employee.self, from: nestedData)

您已使用嵌套解碼容器(nested decoding container)nestedData解碼為Employee


Encoding and Decoding Dates

禮品部門需要知道員工的生日才能發送禮物,因此他們的JSON看起來像這樣:

{
  "id" : 7,
  "name" : "John Appleseed",
  "birthday" : "29-05-2019",
  "toy" : {
    "name" : "Teddy Bear"
  }
}

日期沒有JSON標準,這對于每個與之合作過的程序員而言都是如此。 JSONEncoderJSONDecoder默認使用日期的timeIntervalSinceReferenceDate的雙重表示,這在并不常見。

您需要使用日期策略(date strategy)。 在try encoder.encode(employee)語句之前,將此代碼塊添加到日期(Dates)

// 1
extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "dd-MM-yyyy"
    return formatter
  }()
}
// 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)

這是代碼的作用:

  • 1) 創建與所需格式匹配的日期格式化程序。 它作為DateFormatter的靜態屬性添加,因為這是您的代碼的良好實踐,因此格式化程序是可重用的。
  • 2) 將dateEncodingStrategydateDecodingStrategy設置為.formatted(.dateFormatter),告訴編碼器和解碼器在編碼和解碼日期時使用格式化程序。

檢查dateString并檢查日期格式是否正確。 您已確保禮品部門將按時交付禮品 - 即將推出!

還有一些挑戰,你已經完成了。


Encoding and Decoding Subclasses

Gifts部門API可以根據類層次結構(class hierarchies)處理JSON:

{
  "toy" : {
    "name" : "Teddy Bear"
  },
  "employee" : {
    "name" : "John Appleseed",
    "id" : 7
  },
  "birthday" : 580794178.33482599
}

employee匹配沒有toy or birthday的基類結構。 打開Subclasses并使BasicEmployee符合Codable

class BasicEmployee: Codable {

這會給你一個錯誤,因為GiftEmployee還不是Codable。 通過向GiftEmployee添加以下內容來糾正:

// 1              
enum CodingKeys: CodingKey {
  case employee, birthday, toy
}  
// 2
required init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  birthday = try container.decode(Date.self, forKey: .birthday)
  toy = try container.decode(Toy.self, forKey: .toy)
  // 3
  let baseDecoder = try container.superDecoder(forKey: .employee)
  try super.init(from: baseDecoder)
}  

此代碼涵蓋解碼:

  • 1) 添加相關的編碼密鑰。
  • 2) 解碼特定于子類的屬性。
  • 3) 使用superDecoder(forKey :)獲取一個適合傳遞給超類的init(from :)方法的解碼器實例,然后初始化超類。

現在在GiftEmployee中實現編碼:

override func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(birthday, forKey: .birthday)
  try container.encode(toy, forKey: .toy)
  let baseEncoder = container.superEncoder(forKey: .employee)
  try super.encode(to: baseEncoder)
}

它是相同的模式,但您使用superEncoder(forKey :)為超類準備編碼器。 將以下代碼添加到playground的末尾以測試您的可編碼子類:

let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(), 
                                toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)

檢查giftString的值以查看您的工作! 您可以在應用程序中處理更復雜的類層次結構。 是時候進行下一次挑戰!


Handling Arrays With Mixed Types

Gifts部門API公開了適用于不同類型員工的JSON:

[
  {
    "name" : "John Appleseed",
    "id" : 7
  },
  {
    "id" : 7,
    "name" : "John Appleseed",
    "birthday" : 580797832.94787002,
    "toy" : {
      "name" : "Teddy Bear"
    }
  }
]

此JSON數組是多態的,因為它包含默認和自定義employees。 打開Polymorphic types,您將看到不同類型的員工由枚舉表示。 首先,聲明枚舉是Encodable

enum AnyEmployee: Encodable {

然后將此代碼添加到枚舉的主體:

  // 1
enum CodingKeys: CodingKey {
  case name, id, birthday, toy
}  
// 2
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  
  switch self {
    case .defaultEmployee(let name, let id):
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
    case .customEmployee(let name, let id, let birthday, let toy):  
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
      try container.encode(birthday, forKey: .birthday)
      try container.encode(toy, forKey: .toy)
    case .noEmployee:
      let context = EncodingError.Context(codingPath: encoder.codingPath, 
                                          debugDescription: "Invalid employee!")
      throw EncodingError.invalidValue(self, context)
  }
}

以下是此代碼的用途:

  • 1) 定義足夠的編碼密鑰以涵蓋所有可能的情況。
  • 2) 對有效員工進行編碼,并對無效員工拋出EncodingError.invalidValue(_:_ :)

通過將以下內容添加到playground的末尾來測試您的編碼:

let employees = [AnyEmployee.defaultEmployee("John Appleseed", 7), 
                 AnyEmployee.customEmployee("John Appleseed", 7, Date(), toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!

檢查employeesString的值以查看混合數組。

解碼有點復雜,因為在決定如何繼續之前,你必須弄清楚JSON中的內容。 將以下代碼添加到playground

extension AnyEmployee: Decodable {
  init(from decoder: Decoder) throws {
    // 1
    let container = try decoder.container(keyedBy: CodingKeys.self) 
    let containerKeys = Set(container.allKeys)
    let defaultKeys = Set<CodingKeys>([.name, .id])
    let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
   
    // 2
   switch containerKeys {
      case defaultKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        self = .defaultEmployee(name, id)
      case customKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        let birthday = try container.decode(Date.self, forKey: .birthday)
        let toy = try container.decode(Toy.self, forKey: .toy)
        self = .customEmployee(name, id, birthday, toy)
      default:
        self = .noEmployee
    }
  }
}
// 4
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData) 

這就是它的工作原理:

  • 1) 像往常一樣獲取一個鍵控容器,然后檢查allKeys屬性以確定JSON中存在哪些鍵。
  • 2) 檢查containerKeys是否與默認員工或自定義員工所需的密鑰匹配,并提取相關屬性;否則,建立一個.noEmployee。 如果沒有合適的默認值,您可以選擇在此處拋出錯誤。
  • 3) 將employeesData解碼為[AnyEmployee]

您可以根據具體類型對employeesData中的每個employee進行解碼,就像編碼一樣。

只留下兩個挑戰 - 下一個挑戰!


Working With Arrays

禮品部門為員工的生日禮物添加標簽;他們的JSON看起來像這樣:

[
  "teddy bear",
  "TEDDY BEAR",
  "Teddy Bear"
]

JSON數組包含小寫,大寫和常規標簽名稱。 這次你不需要任何密鑰,所以你使用一個unkeyed container

打開Unkeyed container容器并將編碼代碼添加到Label

func encode(to encoder: Encoder) throws {
  var container = encoder.unkeyedContainer()
  try container.encode(toy.name.lowercased())
  try container.encode(toy.name.uppercased())
  try container.encode(toy.name)
}

UnkeyedEncodingContainer就像你目前使用的容器一樣工作,除了......你猜對了,沒有鍵。 可以將其視為寫入JSON數組而不是JSON字典。 您將三個不同的字符串編碼到容器中。

運行playground并檢查labelString以查看您的數組。

這是解碼的外觀。 將以下代碼添加到playground的末尾:

extension Label: Decodable {
  // 1
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var name = ""
    while !container.isAtEnd {
      name = try container.decode(String.self)
    }
    toy = Toy(name: name)
  }
}
// 2
let sameLabel = try decoder.decode(Label.self, from: labelData)

這就是上面代碼的工作原理:

  • 1) 獲取解碼器的無鍵解碼容器(unkeyed decoding container),并使用decode(_ :)循環解碼,以解碼最終的,格式正確的標簽名稱。
  • 2) 使用未加密碼的解碼容器(unkeyed decoding container)labelData解碼為Label

您遍歷整個解碼容器,因為最后會出現正確的標簽名稱。

你最后一次挑戰的時間!


Working With Arrays Within Objects

禮品部門想要查看員工生日禮物的名稱和標簽,因此其API生成的JSON如下所示:

{
  "name" : "Teddy Bear",
  "label" : [
    "teddy bear",
    "TEDDY BEAR",
    "Teddy Bear"
  ]
}

您將標簽名稱嵌套在label內。 與前一個挑戰相比,JSON結構增加了額外的縮進級別,因此在這種情況下,您需要使用嵌套的無鍵容器(nested unkeyed containers)作為label

打開Nested unkeyed containers并將以下代碼添加到Toy

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  var labelContainer = container.nestedUnkeyedContainer(forKey: .label)                   
  try labelContainer.encode(name.lowercased())
  try labelContainer.encode(name.uppercased())
  try labelContainer.encode(name)
}

在這里,您將創建一個嵌套的無鍵容器,并使用三個標簽值填充它。 運行playground并檢查string以檢查結構是否正確。

如果JSON具有更多縮進級別,則可以使用更多嵌套容器。 將解碼代碼添加到playground頁面:

extension Toy: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
    var labelName = ""
    while !labelContainer.isAtEnd {
      labelName = try labelContainer.decode(String.self)
    }
    label = labelName
  }
}
let sameToy = try decoder.decode(Toy.self, from: data)

這遵循與以前相同的模式,通過數組并使用最終值來設置label的值,但是來自嵌套的未鍵控容器(nested unkeyed container)

恭喜您完成所有挑戰!

Encoding and decoding in Swift like a pro!

后記

本篇主要講述了Swift編碼和解碼,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容