Swift 3.0

swift30快速入门

常量与变量

控制流

函数和闭包

对象和类

swift30-将元组加到数组中

swift-数组元组字典优缺点

iOS的MVC设计模式

swift3.0快速入门

swift中的代码是在全局作用域下,这些代码直接作为整个项目的入口,所以这里并不需要main函数。(Swift没有main函数,从top level code的上方开始往下执行(就是第一个非声明语句开始执行[表达式或者控制结构,类、结构体、枚举和方法等属于声明语句]),不能存在多个top level code文件(否则编译器无法确定执行入口,事实上swift隐含一个main函数,这个main函数会设置并调用全局 “C_ARGC C_ARGV”并调用由top level code构成的top_level_code()函数);)

Swift通过import引入其他类库(和Java比较像);

Swift语句不需要双引号结尾(尽管加上也不报错),除非一行包含多条语句(和Python有点类似);

print("Hello, world!")

1

1

常量与变量

使用let声明一个常量,使用var声明一个变量。常量的值在编译阶段并不需要被编译器所知道,但常量在最初定义时必须赋值。

varmyVariable =42myVariable =50letmyConstant =42

1

2

3

1

2

3

你可以为变量或常量指定你所所想的类型。然而你不不需要每次都这么做。当你定义了一个变量或常量时,编译器可以推测这个变量或常量的类型。

letimplicitInteger =70letimplicitDouble =70.0

1

2

1

2

例如上面两行,编译器会将implicitInteger的类型视为整型,因为这个常量最初的值为一个整数。

当然也可以显示说明变量或常量的类型。形式如下:

letexplicitDouble:Double=70

1

1

练习:

letexplicitDouble: Double =70letexplicitFloat:Float=4letimplicitInteger =70print("explicitDouble=",explicitDouble);print("explicitFloat=",explicitFloat);print("implicitInteger=",implicitInteger);

1

2

3

4

5

6

1

2

3

4

5

6

运行结果:

一个值不会随意改变类型,如果你需要改变一个值得类型,需要强制类型转换。举例说明:

letlabel ="The width is "letwidth =94letwidthLabel = label +String(width)

1

2

3

1

2

3

有一种更加简单的方法将其它类型转换成字符串类型。那就是使用\()。

letapples =3letoranges =5letappleSummary ="I have \(apples) apples."letfruitSummary ="I have \(apples + oranges) pieces of fruit."

1

2

3

4

1

2

3

4

通过[ ]符号创建数组。在swift中,既可以通过数组下标访问数组元素,也可以通过关键字访问数组元素。

例子:

varshoppingList = ["catfish","water","tulips","blue paint"]shoppingList[1] ="bottle of water"varoccupations = ["Malcolm":"Captain","Kaylee":"Mechanic",]occupations["Jayne"] ="Public Relations"foritem1inshoppingList{print(item1)}foritem2inoccupations{print(item2)}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

创建一个空数组:

letemptyArray = [String]()

1

1

创建一个空的字典:

letemptyDictionary = [String: Float]()

1

1

如果数组或者字典的类型可以被推测,可以用如下表达式:

shoppingList =[]occupations =[:]

1

2

1

2

控制流

if 和 switch都可以用来做条件判断,for-in,for,while,repeat-while都可以用来做循环。swift中条件语句的判断语句的括号和循环语句的括号是可以省略的,而它们之后的{ }是不能省略的。

letindividualScores = [75,43,103,87,12]varteamScore =0forscoreinindividualScores {ifscore >50{        teamScore +=3}else{        teamScore +=1}}print(teamScore)

1

2

3

4

5

6

7

8

9

10

1

2

3

4

5

6

7

8

9

10

if和let一起用可能导致值丢失,所以必须把这些量定义为可选值(optional)。

通过在类型后面加一个 ? 来将变量声明为 Optional 的。如果不是 Optional

的变量,那么它就必须有值。如果可能没有值的话,我们使用 Optional 并且将它设置为 nil 来表示没有值。

如果可选值为nil,则条件判断为false,花括号中的代码会被跳过。反之,可选值去包装并且赋给let后面的常量,使得去包装的值可以在代码块内部访问。

varoptionalString:String? ="Hello"varnotoptionalString:String? print(optionalString == nil)print(notoptionalString == nil)varoptionalName:String? ="John Appleseed"vargreeting ="Hello!"ifletname = optionalName {    greeting ="Hello, \(name)"}print(greeting)varoptionalname:String?varsmiling ="Hey!"ifletname = optionalname {    smiling ="Hello, \(name)"}print(smiling)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

参考:Swift 可选值(Optional Values)介绍

也可以使用??操作符来处理可选值。

letnickName:String? = nilletfullName:String="John Appleseed"letinformalGreeting ="Hi \(nickName ?? fullName)"print(informalGreeting)

1

2

3

4

1

2

3

4

在swift中,switch支持任意类型的数据和多种比较,不再局限于整数或者只是比较像等。

参考:Swift Switch介绍

在 for-in中通过提供一对关键字-值实现对字典的迭代。字典是一个无序的集合,所以他们的键和值在一个任意的顺序迭代。

let interestingNumbers = ["Prime": [2,3,5,7,11,13],"Fibonacci": [1,1,2,3,5,8],"Square": [1,4,9,16,25],]var largest =0for(kind, numbers)ininterestingNumbers {fornumberinnumbers {ifnumber> largest {            largest =number}    }}print(largest)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

1

2

3

4

5

6

7

8

9

10

11

12

13

14

while和do while的用法:

varn =2whilen <100{    n = n *2}print(n)varm =2repeat {    m = m *2}whilem <100print(m)

1

2

3

4

5

6

7

8

9

10

11

1

2

3

4

5

6

7

8

9

10

11

可以通过使用..<来控制循环变量的值,间接限制循环的次数,不是直接限制循环的次数。

vartotal =0foriin0..<4{    total += i}print(total)

1

2

3

4

5

1

2

3

4

5

运行结果为6。

函数和闭包

使用func 声明一个函数。通过函数名称和参数调用一个函数。使用->区分参数名和函数返回的类型。

Swift的历史版本中出现过在调用函数时不需要指定任何函数参数(或者从第二个参数开始指定参数名),在调用方法时则必须从第二个参数开始必须指定参数名等多种情况,而在Swift3.0中不管是函数还是方法都必须从第一个参数开始必须指定参数名(当然可以使用“_”明确指出调用时省略参数)。

swift3.0的表示法:

func greet(_ person:String,onday:String)->String{return"Hello \(person), today is \(day)."}greet("John",on:"Wednesday")

1

2

3

4

1

2

3

4

由于用的在线编译器不支持swift3.0,所以我用以前的语法跑了一遍。

func greet(person: String, day: String)->String {return"Hello \(person), today is \(day)."}print(greet("Bob",day:"Tuesday"))

1

2

3

4

1

2

3

4

使用元组可以返回多个值,元组中的元素既可以通过下标访问,也可以通过名字访问。

func calculateStatistics(scores: [Int]) -> (min: Int,max: Int,sum: Int) {    varmin= scores[0]    varmax= scores[0]    varsum=0forscoreinscores {ifscore >max{max= score        }elseifscore

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

函数也可以有可变数量的参数,将这个参数看成是一个数组。

func sumOf(numbers: Int...) -> Int {    varsum=0fornumberinnumbers {sum+=number}returnsum}sumOf()sumOf(numbers:42,597,12)

1

2

3

4

5

6

7

8

9

1

2

3

4

5

6

7

8

9

函数可以嵌套。嵌套的函数可以访问在外层函数定义的变量。使用嵌套函数可以组织更长更复杂的函数。

func returnFifteen()->Int {vary =10func add() {        y +=5}    add()returny}returnFifteen()

1

2

3

4

5

6

7

8

9

1

2

3

4

5

6

7

8

9

函数是一个引用类型,就是说一个函数可以返回另一个函数作为返回值。类似于C语言中的指针函数。

func makeIncrementer()->((Int) -> Int){funcaddOne(number: Int)->Int {return1+ number    }returnaddOne}varincrement = makeIncrementer()increment(7)

1

2

3

4

5

6

7

8

1

2

3

4

5

6

7

8

注意:上面的例子(Int) -> Int 表示返回函数的参数类型和返回类型。

一个函数可以使用一个函数作为它的的返回值。

func hasAnyMatches(list: [Int], condition: (Int) -> Bool)->Bool {foriteminlist {ifcondition(item) {returntrue}    }returnfalse}func lessThanTen(number: Int)->Bool {returnnumber <10}varnumbers = [20,19,7,12]hasAnyMatches(list: numbers,condition: lessThanTen)

1

2

3

4

5

6

7

8

9

10

11

12

13

1

2

3

4

5

6

7

8

9

10

11

12

13

Swift中的闭包其实就是一个函数代码块,它和ObjC中的Block及C#、Java中的lambda是类似的。闭包的特点就是可以捕获和存储上下文中的常量或者变量的引用,即使这些常量或者变量在原作用域已经被销毁了在代码块中仍然可以使用。事实上前面的全局函数和嵌套函数也是一种闭包,对于全局函数它不会捕获任何常量或者变量,而对于嵌套函数则可以捕获其所在函数的常量或者变量。通常我们说的闭包更多的指的是闭包表达式,也就是没有函数名称的代码块,因此也称为匿名闭包。

函数实际上是一种特殊的闭包。在闭包体内使用in去分离参数和返回值。

numbers.map({    (number: Int) -> Intinletresult=3*numberreturnresult})

1

2

3

4

5

1

2

3

4

5

你已经有几种方式可以更自如的写闭包。当一个闭包类型已经知道,比如是一个代理的回调,你可以去除参数类型、返回类型或者两个都去除。单个语句闭包会把它语句的值当做结果返回。

let mappedNumbers = numbers.map({numberin3*number})print(mappedNumbers)

1

2

1

2

你可以通过参数位置访问-这个方法在非常短的闭包中非常有用。当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在括号后面。当一个闭包是传给函数的唯一参数,你可以完全忽略括号。

letsortedNumbers = numbers.sorted {$0>$1}print(sortedNumbers)

1

2

1

2

对象和类

用class关键字,后面加上一个类名来创建一个类。类中属性的定义和普通常量或者变量的定义相同,但属性是属于这个类的。类中方法和函数的定义和普通方法与函数的定义相同。

classShape{varnumberOfSides =0func simpleDescription()->String {return"A shape with \(numberOfSides) sides."}}

1

2

3

4

5

6

1

2

3

4

5

6

通过在类名后面加括号来创建类的一个实例。一个实例通过一个小圆点来访问这个类的属性和方法。

varshape = Shape()shape.numberOfSides =7varshapeDescription = shape.simpleDescription()

1

2

3

1

2

3

上面版本的shape类少了一些重要的东西:在一个实例被创建时缺少初始化程序来初始化这个类。可以使用init来初始化一个类。

classNamedShape{varnumberOfSides: Int =0varname: String    init(name: String) {        self.name = name    }    func simpleDescription()->String {return"A shape with \(numberOfSides) sides."}}

1

2

3

4

5

6

7

8

9

10

11

12

1

2

3

4

5

6

7

8

9

10

11

12

注意怎样用self来区分属性和初始化程序的参数。当你创建一个类的实例时,初始化程序的参数就像函数调用那样传递。每一个属性都需要给它分配一个值,要么是在属性定义的时候,那么是在初始化程序时。

在一个对象(实例)被销毁或者重新分配之前。使用deinit做一些清理工作。

子类要继承父类是通过在子类名后面加上冒号,然后跟上父类名。没有要求一个类必须继承一个标准的根类,所以你可以根据需要继承一个父类。

子类可以重写父类声明的方法,即在方法前加上 override,如果没有加override就重写的话会报错。编译器也会检查有override的方法在父类中存不存在。

class Square: NamedShape {varsideLength: Double    init(sideLength: Double, name:String) {self.sideLength=sideLength        super.init(name: name)        numberOfSides=4}    func area()->Double {returnsideLength*sideLength    }    override func simpleDescription()->String{return"A square with sides of length \(sideLength)."}}lettest=Square(sideLength:5.2, name:"my test square")test.area()test.simpleDescription()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

除了存储属性,属性可以有getter和setter,称为计算属性。

参考:getter 和 setter

classEquilateralTriangle:NamedShape{varsideLength: Double =0.0init(sideLength: Double, name: String) {        self.sideLength = sideLengthsuper.init(name: name)        numberOfSides =3}varperimeter: Double {get{return3.0* sideLength        }set{            sideLength = newValue /3.0}    }overridefunc simpleDescription() -> String {return"An equilateral triangle with sides of length \(sideLength)."}}vartriangle = EquilateralTriangle(sideLength:3.1, name:"a triangle")print(triangle.perimeter)triangle.perimeter =9.9print(triangle.sideLength)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

在perimeter的setter中,参数被隐含地命名为newValue。你也可以将参数名字在set后面用括号括起来。

注意EquilateralTriangle类的构造器有三个不同的步骤:

给子类的属性赋值。

调用父类的构造器。

改变定义在父类中的属性的值。任何其它的使用方法,getter,setter的步骤可以在这一步完成。

如果你不需要一个计算属性但是仍然想在代码运行前后设定新值的话,可以使用willSet和DidSet。

class TriangleAndSquare {    var triangle: EquilateralTriangle {        willSet {            square.sideLength= newValue.sideLength}    }    var square: Square {        willSet {            triangle.sideLength= newValue.sideLength}    }    init(size: Double, name: String) {        square = Square(sideLength: size, name: name)        triangle = EquilateralTriangle(sideLength: size, name: name)    }}var triangleAndSquare = TriangleAndSquare(size:10, name:"another test shape")print(triangleAndSquare.square.sideLength)print(triangleAndSquare.triangle.sideLength)triangleAndSquare.square= Square(sideLength:50, name:"larger square")print(triangleAndSquare.triangle.sideLength)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

当处理可选类型,你可以在方法属性和subscripting

前面加上问号?。如果?号前面的值是nil,那么问号后面的所有值都是nil.否则,?之后的所有代码都会运行。在这两种情况下,整个表达式的值是可选类型的。

swift3.0-将元组加到数组中

//创建元组  键:值varstudent1 = (number:"2014110323",name:"allen",age:20,sex:"f");//方式一varstudent2 = ("2014110324","iverson",21,"m");//方式二//访问元组print("\(student1.number)  \(student1.name)  \(student1.age)  \(student1.sex)")//方式一print("\(student2.0)  \(student2.1)  \(student2.2)  \(student2.3)");//方式二//将方式二的访问方式转换成方式一var(number,name,age,sex) = student2;print("\(number)")//忽略元组中某些值varstudent3 = student2;let (number1,_,age1,sex1) = student3print("\(student3.0)  \(student3.1)  \(student3.2)  \(student3.3)");//仍可以通过标号访问print("\(number1)  \(age1)  \(sex1)")//不能通过”键“访问//可变元组与不可变元组//用var声明的是可变元组,可改变元组中的值//用let声明的是不可变元组//改变元组中的值//改变student1的学号student1.number ="001"//改变student2的学号//student2.number = "002"  //错误做法/*type '(String, String, Int, String)' has no member 'number'

student2.number = "002" */student2.0="002"//正确做法student3.0="003"//将元组加到数组当中varStudent:[(String,String,Int,String)] = []Student.append(student1);Student.append(student2);Student.append(student3);forvarstudent in Student{print(student)}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

swift-数组,元组,字典优缺点

字典

优点:

通过key值进行索引,查找效率高

通过key值进行数据标注,可读性高,易于区分多种数据

key值唯一,增删改可以保证数据唯一性

缺点:

一个value必须对应一个key,尽管有时不需要key

key值顺序不定,字典对key值表进行了hash,所以不方便存储对顺序敏感的数据

数组

优点:

数据存储顺序固定,增删改也通过index来进行

集成了遍历方法,适合对大量同类数据的处理

不需要定义key,写法相对简单

缺点:

访问特定数据时,查找效率不高

处理特定数据时,需要牢记数据的index,可读性不好,容易产生错位处理

元组

优点:

元组可以同时存储多种类型元素,且元素类型固定,以保证数据安全,除非你定义数据类型为Any。编译器会对赋值参数类型进行检查

元组的元素个数固定,不允许增加、删除,编译器会严格校验赋值参数个数

无需定义key,但是必要时可以为数据命名,方便数据访问

适合同时遍历多元数据,例如官方文档的例子

for (index, value) in shoppingList.enumerate()

缺点:

不适合存储大量数据,因为元组不支持append、remove等方法

考虑到工程实际情况,后端使用的语言可能不支持元组,需要转换为其他格式

转自:有了数组和字典,为何Swift还需要元组(tuples)?

iOS的MVC设计模式

MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller。

Model负责存储、定义、操作数据。

View用来展示给用户,并且和用户进行交互。

Controller是Model和View的协调者,Controller把Model中的数据拿过来给View使用。Controller可以直接与Model和View进行通信,而View不能与Controller直接通信。

记住:views 不拥有它们展示的数据。

当有数据更新时,Model也要与Controller进行通信,这个时候就要用Notification和KVO,这个方式就像发广播一样,Model发信号,Controller设置接收监听信号,当有数据更新是就发信号给Controller,Model和View不能直接通信,这样违背MVC设计原则。View与Controller通信需要利用代理协议的方式,Controller可以直接根据Model决定View的展示。

View如果接受响应事件则通过delegate,target-action,block等方式告诉Controller的状态变化。Controller进行业务的处理,然后再控制View的展示。那这样Model和View就是相互独立的。View只负责页面的展示,Model只是数据的存储,那么也就达到了解耦和重用的目的。

斯坦福iOS开发截图:

Outlet

通过outlet,我们可以从控件中取出信息,或者将新的信息赋予控件。按照词典上的解释outlet可以理解为插座的意思。界面上配置的每个控件,就像是通过“插座”与界面连接。这里,将程序设置在“插座”内,实现控件与界面间的信息交换。

Action

Action就是指程序中具体的行为,处理。应用程序将按照Action内实现的内容来处理。比如,「按下按钮后的处理」或者「输入文字后的处理」等等。

delegate

代理就是把自己做的任务交给别人做。让代理遵循着监听到一些事件,实现一些数据源。做过iOS开发的都应该知道TableView有两个属性,delegate和dataSource。这就是代理最好的体现,tableView在设计之初并不知道这个tableView将来会放在哪里,存储的是什么东西,这里数据源就起到了关键性作用:对于设计者来说,我只要依据数据源来生成对应的视图,对于使用者,只需要填充数据源。而delegate则给予用户一个接口来响应一些事件,比如tableview中的一行被点击等等。

Notification

通知中心实际上是在程序内部提供了消息广播的一种机制,它允许我们在低程度耦合的情况下,满足控制器与一个任意的对象进行通信的目的。每一个 iOS 程序(即每一个进程)都有一个自己的通知中心,即 NSNotificationCenter 对象,该对象采用单例设计模式,可以通过类方法 defaultCenter 获得当前进程唯一的通知中心对象。一个 NSNotificationCenter 可以有许多的通知消息 NSNotification,对于每一个 NSNotification 可以有很多的观察者 Observer 来接收通知。NSNotificationCenter 是 iOS 中通知中心的灵魂,由该类实现了观察者模式,并给开发者提供了诸如注册、删除观察者的接口。

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

推荐阅读更多精彩内容