iOS Apprentice中文版-从0开始学iOS开发-第二十九课

在数组的方括号内必须写上类型,或者在Array后面的尖括号<>内写上类型。

对于字典而言,你一共需要提供两个类型,一个用于键,而另一个用于值。

Swift要求所有的变量和常量都必须有值。你可以在声明它们的时候给它们指定一个值,也可以通过init方法给它们分配值。

有时,你需要一个变量可以没有值,这种情况,你需要将变量声明为可选型:

var checklistToEdit: Checklist?

你不能直接使用这种类型的变量;你必须在使用它们之前,侦测一下其中是否有值,这个行为就叫做可选型解包:

if let checklist = checklistToEdit {
  // “checklist” now contains the real object
} else {
  // the optional was nil
}

下面例子中的变量age就是一个可选型,因为没有任何保证说字典中存在一个名为"Jony Ive"的键,所以age的类型是Int?,而不是Int:

if let age = dict["Jony Ive"] {
// 使用age变量
}

如果你100%的确定字典中存在一个叫做"Jony Ive"的键的话,那么你就可以对age变量进行强制解包:

var age = dict["Jony Ive"]!

你使用感叹号来通知Swift,‘这个可选型不会为nil,我用我的名誉打赌!’,当然,如果你错了的话,这个变量的值为nil,那么app就会挂掉,你也就名誉扫地了,所以你在使用强制解包的时候一定要小心。

另一种稍微安全点的强制解包方式叫做可选型链接。例如,下面的语句会在navigationController为nil时把app挂掉。

navigationController!.delegate = self

但是像这样做则不会把app挂掉:

navigationController?.delegate = self

位于问号后面的任何东西,都会在navigationController为nil时把它忽视掉。这个使用问号强制解包的语句等价于下面的语句:

if navigationController != nil {
  navigationController!.delegate = self
}

在声明可选型的时候,也可以用感叹号来代替问号,这样就是一个隐式解包可选型了:

var dataModel: DataModel!

这样的变量会带来潜在的危险,因为你可以向使用常规变量那样直接使用它,并不需要先解包。如果它的值为空,那么app就挂了,而常规变量为空时,编译器会提示你怎么做。

可选型平时被包裹起来,以避免app崩溃,但是使用了感叹号以后,就解除了可选型的安全级别。

然而,有时使用隐式解包可选型比使用可选型要方便一些。当你无法给一个变量初始值,也无法用init方法对其初始化的时候,你就会需要到这种隐式解包可选型。

如果你给了一个变量一个值后,就不应该在使它为nil,如果一个变量可以从有值变为nil,那么你最好还是使用用问号声明的可选型。

方法与函数(Methods and functions)

你已经学习过这样的一种对象了,它是所有app的基础组成部分,同时具有数据和功能。实例变量及常量提供数据,方法提供功能。

当你调用一个方法,app就会跳转到方法中,逐条的执行其中的语句,当方法中最后一条语句被执行完毕后,app就会会到之前离开的地方:

let result = performUselessCalculation(314)
print(result)
...
func performUselessCalculation(_ a: Int) -> Int {
  var b = Int(arc4random_uniform(100))
  var c = a / 2
  return (a + b) * c
}

方法经常会返回一个值给调用者,比如一个计算结果或者从一个集合中找到的一个对象。返回值的类型会写在->符号的后面。在上面的例子中,返回值的类型是Int。如果不存在->这个符号,那么就是说这个方法不返回任何值。

方法就是属于某一特定对象的函数,Swift中也存在独立的函数,比如print()或者arc4random_uniform()。

函数和方法的工作原理一样,一个可重复使用的功能块,但是函数不属于任何对象。像这种函数也被称为自由函数或者全局函数。

下面是一些关于方法的例子:

// 这个方法没有返回值及参数的方法
override func viewDidLoad()

// 这个方法有一个slider参数,但是一样没有返回值
// 关键字@IBAction意味着这个方法可以被连接到界面建造器的控件上
@IBAction func sliderMoved(_ slider: UISlider)

// 这个方法没有参数,但是有一个Int型的返回值
func countUncheckedItems() -> Int

// 这个方法有两个参数,cell和item,但是没有返回值
// 注意一下,第一个参数有一个外部名称for,而第二个参数有一个外部名称with
func configureCheckmarkFor(for cell: UITableViewCell,
                           with item: ChecklistItem)

// 这个方法有两个参数, tableView和section. 并且有一个Int型的返回值。
// 第一个参数前的下划线代表这个参数没有外部名称。
override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int

// 这个方法有两个参数, tableView和indexPath.
// 问号代表它返回一个为可选型的IndexPath对象。
override func tableView(_ tableView: UITableView,
                    willSelectRowAt indexPath: IndexPath) -> IndexPath?

在一个对象上调用一个方法,语法是object.method(parameters)。例如:

// Calling a method on the lists object:
lists.append(checklist)
// Calling a method with more than one parameter:
tableView.insertRows(at: indexPaths, with: .fade)

你可以把调用方法想象为从一个对象向另一个对象传递消息:“嗨 lists,我从checklist对象中向你发送了append的消息。”

你调用消息所属的对象被称为消息的接收者。

从同一个对象中调用方法非常常见,下面的例子中,loadChecklists()调用了sortChecklists()。它们都是DataModel对象中的成员:

class DataModel {
fun loadChecklists() {
...
sortChecklists()
}
fun sortChecklists() {
...
  }
}

有时你会写为下面这个样子:

fun loadChecklists() {
...
self.sortChecklists()
}

关键字self清晰的表明了DataModel对象自己是这个消息的接受者。

⚠️:在我们的课程中,调用方法的时候,我省略了self关键字,因为并不是必须要这样做。Object-C开发者会非常乐意在每个地方都写上self,所以你也许会见到它在Swift中也被大量使用。到底写与不写,这是程序员间可以引发战争的一个话题,但是无论如何,app并不是太关心这点。

在一个方法的内部,你也可以使用self关键字来引用这个对象自己:

@IBAction fund cancel() {
delegate?.itemDetailViewControllerDidCancel(self)
}

这里cancel()方法将对象自身的引用发送给delegate,所以delegate知道谁发送了这个itemDetailViewControllerDidCancel()消息。

同时注意一下这里的可选型链接。这个delegate属性是个可选型,所以它可以为nil。在调用方法前使用一个问号来确保delegate为nil时,app不会挂掉。

方法经常会具有一个或多个参数,所以你可以让它们接收不同数据源上的数据工作。一个被限定了数据源的方法,可能不会非常有价值。看看下面的sumValuesFormArray()方法,它没有参数:

class MyObject {
  var numbers = [Int]()
  fun sunValuesFromArray() ->Int{
  var total = 0
  for number in numbers {
   total += number
  }
  return total
 }
}

这里,numbers是一个实例变量。方法sumValuesFromArray()被这个实例变量绑定死了,如果这个变量不存在,那么这个方法就没用了。

假设你在这个app中添加了第二数组,也想要应用上面的计算,那么其中一个方法是把这个方法复制一遍,重新命名为一个新的方法来处理这个新的数组。这样做确实可行,但是你也从此和聪明绝缘了。

另一个好一点的选择是,给这个方法一个参数,使得你可以传送任何你想要计算的数组,这样,这个方法就从实例变量中解放出来了:

func sumValues(from array: [Int])-> Int {
  var total = 0
  for number in array {
  total += number
  }
  return total
}

现在你可以用任何整数型的数组作为它的参数了。

这并不是说方法不应该使用实例变量,只是说你想要一个方法的应用更加广泛,那么给它一个参数是个很好的选择。

方法的参数经常会有两个名字,一个外部名称,一个内部名称,例如:

fun downloadImage(for searchResult: SearchResult,withTimeout timeout: TimeInterval,andPlaceOn button: UIButton) {
...
}

这个方法有三个参数:searchResult,timeout和button。这些是内部名称,你在方法的内部用这些名称来调用参数。

方法的外部名称是方法名称的一部分。所以这个方法的全名是downloadImage(for,withTimeout,andPlaceOn),Swift中的方法名称经常会特别的长。

调用这个方法的时候,你需要使用外部名称:

downloadImage(for:result,withTimeout:10,andPlaceButton)

有时,你会看到一个方法它的第一个参数没有外部名称,取而代之的是一个下划线:

override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int)-> Int

这种情况经常出现在委托方法中,它是Object-C的遗留物,第一个参数的内部和外部名称都会被包含在方法名称中,比如在Object-C中downloadImage()方法的全名会是downloadImageForSearchResult。像这样的命名方式,以后会非常少见。如果是在Object-C中,这个方法的名称会是tableViewTableVIew,非常古怪是吧,而Swift 中,以下划线代替外部名称时,方法名称中就可以省略这个参数的外部名称,在Swift中,这个方法的全名是tableView(numberOfRowsInSection)。这样是不是容易明白多了?Swift在对方法命名时更加灵活,但它还是会保留一些旧的惯例。

在一个方法的内部,你可以做以下事情:

1、创建局部变量或者常量

2、进行基本的数学运算,比如加减乘除

3、将一个新的值放入变量(局部变量或实例变量)

4、调用其他方法

5、使用if或者switch作出判断

6、用for或者while进行循环处理

7、返回一个值给调用者

让我们来看看if和for语句的更多细节。

作出判断(Making decisions)

if语句的基本结构是这个样子的:

if count == 0 {
text = "No Items"
} else if count == 1 {
text = "1 Item"
} else {
text = "\(count) Items"
}

if后面的表达式称之为条件。如果条件为真,那么if后面花括号内的语句会被执行。如果没有一个条件为真,那么最后一个else后面的花括号内的语句会被执行。

你使用比较运算符来对两个值进行比较:

== 等于
!= 不等于
< 小于
<= 小于等于
大于 >
大于等于 >=

使用等于操作时,被比较的两个对象仅在相等时返回true,比如:

let a = "Hello,world"
let b = "Hello," + "world"
print(a == b) //打印结果为true

这个和Object-C有所不同,在Object-C中,必须两个对象是内存中的同一个实例,才会返回为true。而Switf中的==操作,仅仅是比较对象的值,而不管它在内存中是不是同一个对象,如果在Swift中像做这个操作的话,需要使用运算符 ===,三个等号。

你还可以使用逻辑操作符来连接两个表达式:

&& 与操作,a && b必须在a和b都为true时才返回true
||或操作符,a || b当a,b其中之一为true时,返回true

还有逻辑非操作符!,它的作用是将原本的true转为false,原本的false转为true。(不要和可选型弄混了,逻辑非操作符出现在对象的前面,而可选型的感叹号出现在对象的后面)

可以使用括号()来对表达式分组:

if ((this && that) || (such && so)) && !other {
...
}

它读作:

if ((this and that) or (such and so)) and not other {
...
}

为了看起来更加清晰一些,我们写的有层次一点:

if (
  (this and that)
  or
  (such and so)
)
and
  (not other)

当然,你弄的越复杂,越难记清楚自己在做什么!

Swift中还有一种非常强大的结构,可以用来做出判断,那就是switch语句:

switch condition {
  case value1:
    //语句
  case value2:
    //语句
  case value3:
    //语句
  default:
    //语句

它的效果和多个if else的效果是一致的,上面的代码等同于:

if condition == value1 {
  //语句
} else if condition == value2 {
  //语句
}else if condition == value3 {
  //语句
} else {
  //语句
}

相较之下,switch在这种情况中更加便利,而且意思清晰。而且Swift版的switc比Object-C版的更加强大。例如,你可以使用区间范围:

switch difference {
  case 0:
    title = "Perfect!"
  case 1..<5:
    title = "You almost had it!"
  case 5..<10:
    title = "Pretty good!"
  default:
    title = "Not even close..."

这里的..<是半开区间操作符。它可以创建两个值之间的区间,其中的值都是不重复的,半开区间1..<5等价于闭区间1...4。

你会在后面的课程中见到switch语句的实际用例。

注意一下,if语句中的reture会比方法中的returen更早的返回:

fun divide(_ a: Int, by b: Int) ->Int {
if b == 0 {
  print("不能除以0")
  return 0
}
return a / b
}

对于没有返回值的方法而言,if中的return甚至可以结束掉方法:

fun performDifficultCalculation(list: [Double]) {
if list.count < 2 {
    print("样本过少")
    return
  }
//这里执行复杂的运算
}

在这个例子中,return的意思就是“我们退出方法吧”。任何return后面的语句都会被忽略掉。

你也可以把上面的方法写成下面这个样子:

fun performDifficultCalculation(list: [Double]) {
if list.count < 2 {
    print("样本过少")
  } else {
//这里执行复杂的运算
 }
}

像这种只有两种可能的情况下,上面两个方法的作用一样,使用哪个都可以,我个人比较喜欢第二种。

有时你会看到下面这个样子的代码:

fun someMethod() {
  if condition1 {
    if condition2 {
      if condition3 {
        //语句
      } else {
        //语句
     }
  } else {
  //语句
 } else {
  //语句
 }
}

这种代码非常难读,我喜欢将它们重构为下面这个样子:

fun someMethod() {
 if !condition1 {
//语句
}

if !condition2 {
//语句
}

if !condition3 {
//语句
}

//语句
}

这两段代码的作用其实是一样的,但是后一种更加容易理解。(注意一下,第二种写法中使用了!逻辑非来转换了表达式的意思)

Swift中有一种特殊的语句,guard来帮助你处理这种复杂的情况,用guard重写一下上面的方法就是:

fun someMethod() {
  guard condition1 else {
  //语句
  return
  }

guard condition21 else {
  //语句
  return
  }

...

你要自己尝试这些方法,比较看看哪种可读性最好,哪种看起来最好,这样慢慢的你就会很有经验了。

循环(Loops)

你之前已经见识过了,如何用for in来历遍一个数组:

for item in items {
if !item.checked {
count += 1
}
}

也可以写作:

for item in items where !item.checked {
  count += 1
}

for in中的语句会对每个items数组中的对象执行一遍。

注意一下,变量item的仅在for语句中有效,你不能在外面引用它,它的生命期比局部变量还要短。

有些语言,也包括Swift 2,中的for语句是这个样子的:

for var i = 0;i<5;++i {
print(i)
}

当你运行这个代码,会得到如下结果:

0
1
2
3
4

然而,在Swift 3种,这种for循环已经被抛弃了,取而代之的是,你可以直接使用区间范围,就像下面这样:

for i in 0 ... 4 {
print(i)
}

顺便说一下,也可写作:

for i in stride(from: 0,to: 5,by: 1) {
print(i)
}

stride函数创建了一个专门的对象来代表从1到5,每次增加1。如果你只想要偶数,你可以把by参数改为2。如果你给by参数一个负数的话,那么stride就可以实现倒着数的功能。

for语句并不是唯一的执行循环的语句,另一个非常强大的循环结构就是while语句:

while something is true {
//语句
}

while语句会一直保持循环,知道条件为false为止。还可以使用下面这种形式:

repeat {
 //语句
} while something is true

在这种情况中,条件是在语句执行后才判断的,所以括号内的语句至少也会被执行一次。

你可以使用while语句重写一下循环Checklists中的对象:

var count = 0
var i = 0
while i < items.count {
 let item = items[i]
 if !item.checked {
 count += 1
 }
 i += 1
}

这些循环结构的作用大致相同,只是看起来有些不一样。每一种都可以使你重复执行一段语句,直到条件不符合为止。

然而,使用while会比for in要看起来复杂一些,所以大多数时候,我们都会使用for in。

使用for in、while、repeat并没有什么不同,只是可读性上有所区别。

⚠️:上面例子中的item.count和count是两种不同的东西,只是名字一样。item.count中的count是数组items中的属性用于返回数组中元素的个数;后面的一个count是一个局部变量,用于对没有激活对勾符号的item对象计数。

就你可以在方法中使用return退出方法一样,你可以使用break来提前退出循环:

var found = false
for item in array {
  if item == searchText {
    found = true
    break
  }
}

这个例子中,for语句在数组中循环,直到找到第一个与searchText的值相当的值后,将found设置为true,然后退出循环,不再查看数组中剩下的对象。因为你已经找到了你想要的东西,所以没有必要把整个数组都循环完毕。

还存在一个contiue语句,和break的作用正好相反。它的作用是立即跳到下一个迭代中,当你使用contiue时,你的意思就是“目前这个item已经结束了,我们去看看下一个吧!”

在函数编程中,循环经常会被map,filter或者reduce替代。它们是一些操作集合的函数,对集合中每一个元素执行一段代码,并且返回一个新的集合作为结果。

例如,在数组上使用filter,会保留符合某些条件的元素。比如要得到未激活对勾符号的ChecklistItem对象,你可以这样写:

var uncheckedItems = items.filter { item in !item.checked}

这样写比循环看起来要简单多了。函数编程是一个非常大的话题,所以在这里我们不会展开太多。

对象(Objects)

将功能和数据结合在一起的可重用单元,都是对象。

数据是由对象中的实例变量和实例常量组成。我们经常以对象的属性形式引用它们。功能由对象的方法提供。

在你的Swift程序中,你使用过已存在的对象,比如String,Array,Date,UITableView,以及你自己创建的对象。

定义一个新的对象,你需要一个新的Swift文件,比如MyObject.swift,并且包含一个类(class)。比如:

class MyObject {
 var text: String
 var count = 0
 let maximum = 100
 
init() {
    text = "Hello World"
 }

fun doSomething() {
  //语句
 }
}

在class的花括号内,你添加了属性(实例变量和实例常量)和方法。

属性有两种类型:

1、存储属性,它们通常是实例变量和实例常量。

2、计算属性,不存储东西,而是执行某些逻辑

下面是一个关于计算属性的例子:

var indexOfSelectedChecklist: Int {
  get {
    return UserDefaults.standard().integerForKey("ChecklistIndex")
  }
 set {
   UserDefaults.standard().set(newValue,forKey: "ChecklistIndex")
 }
}

indexOfSelectedChecklist属性并不存储一个值,取而代之的是,每次有人使用这个属性时,它执行get或者set内的代码。另一个选择是,分别写一个setIndexOfSelectedChecklist()和getIndexOfSelectedChecklist()方法,但是这样读起来不是很好。

关键字@IBOutlet的意思是,这个属性可以被界面建造器中的用户接口元素引用,比如label和button。这种属性通常都被声明为weak和可选型。类似的,@IBAction关键字被用于和用户交互时被触发的方法。

这里有三种类型的方法:

1、实例方法

2、类方法

3、init方法

你已经知道了方法就是属于某一个对象的函数。调用这种类型的方法你首先需要一个这个对象的实例:

let myInstance = MyObject()  //创建一个对象的实例
myInstance.doSomething()   //调用方法

你也可以创建一个类方法,这样就可以在没有实例的情况下使用这个方法。事实上,类方法经常被当作“工厂”方法使用,用来创建新的实例:

class MyObject {
...
class fun makeObject(text: String)-> MyObject {
    let m = MyObject()
    m.text = text
    return m
  }
}

let MyInstance = MyObject.makeObject(text: "Hello world")

init方法,或者叫做初始化设置,在创建一个新的对象实例的过程中被使用。你也可以使用init方法来取代上面的那个工厂方法:

class MyObject {
...
init(text: String) {
    self.text = text
 }
}

init方法的主要目的是将对象中的实例变量填满。任何没有值的实例变量和实例常量都必须在init方法中被给予一个值。

Swift不允许变量或者常量没有值(可选型例外),并且init方法是你给变量或者常量赋值的最后一次机会。

对象可以拥有一个以上的init方法;具体使用哪一个要依据具体情况而定。

例如,一个UITableViewController,从故事模版中自动被读取时,使用init?(coder)初始化,手工从nib文件中读取时,使用init(nibName,bundle)初始化,或者没有从故事模版和nib文件中构造时,使用init(style)初始化。有时你会用到这个,而有时你会用到那个。

当对象不再被使用时,你可以提供一个deinit方法。在对象被破坏掉前调用它。

顺便说一下,class并不是Swift中唯一定义对象的方法。还存在其他类型的对象,比如structs和enums。你会在后面学到这些,所以在这里,我们就点到为止了。

协议(Protocols)

一个协议就是一组方法名称的列表:

protocol MyProtocol {
  func someMethod(value: Int)
  func anotherMethod()-> String
}

协议就类似于工作列表。它列出了你的公司中每个具体职位的工作。

但是列表自己本身并不工作,它仅仅是打印出来给大家看的东西。所以你需要雇佣具体的员工来完成列表上的工作。而这些员工,就是具体的对象。

对象需要被指明自己需要遵守的协议:

class MyObject: MyProtocol {
 ...
}

这样,这个对象就需要完成协议中列出的所有方法。(否则,就炒了它)

此时,你就可以引用这个对象,同时还有协议:

var m1: MyObject = MyObject()
var m2: MyProtocol = MyObject()

对于代码中任何使用m2变量的部分,它是否是MyObject对象并不重要。m2类型是MyProtocol,不是MyObject。

所有你的代码看到的是,m2是某个遵守MyProtocol协议的对象,但是具体是什么样的对象并不重要。

换而言之,你并不关心你雇用的员工,是不是兼职其他工作,只要他和你需要的东西不冲突,你就可以雇佣他。

我们已经快速的回顾了一遍你所遇到的Swift语法知识。在结束了这些理论之后,是时候开始我们的app开发了。

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

推荐阅读更多精彩内容