这节课我们来学习最重要的一个知识点:类 ,之前学习的都是编程语言本身提供的最基础的类型,光使用这些类型很难完成更加抽象的任务,加入我们要抽象出 人 的类型,那么就需要自己动手定义一个新的 Class
我们暂时抛弃 Playground 这种教学开发方式,使用完成的 Project 工程来开发,打开 Xcode ,点击 File->New->Project,勾选 iOS 、Single View App,点击 Next ,输入工程名称 HelloWorld,开发语言选择 Swift ,点击 Next ,选择存放项目文件的目录,点击 Create ,一个新的项目就创建好了
工程目录和配置文件如上图所示,将 Deployment Target 调整为 9.0,这表示我们的应用仅支持iOS9.0及以上,Devices 选择 iPhone ,这个应用就可以在苹果手机上面运行了,如果不方便在真机上运行,可以在虚拟机上面查看运行结果,在顶部偏左的地方选择iPhone X虚拟机,然后点击小三角开始运行,也可以用快捷键 Command + R
稍等片刻,我们的iPhone X虚拟机就启动完成了,并且 HelloWorld 应用已经安装在虚拟机上面运行了,目前还是一个空白的应用,什么都没有,接下来我们把能看见的 Hello World 显示在界面上,打开 ViewController.swift 文件并修改代码如下
import UIKit
class ViewController: UIViewController {
let label = UILabel() // 创建一个用于显示文本的 UILabel
override func viewDidLoad() {
super.viewDidLoad()
//设置文本内容
label.text = "Hello World!"
//计算文本内容大小
label.sizeToFit()
//将文本添加到视图上
view.addSubview(label)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//将文本内容居于屏幕中央
label.center = CGPoint.init(x: view.frame.width / 2, y: view.frame.height / 2)
}
}
运行应用,我们就能看到屏幕中央的 Hello World!
先不用急着去理解代码,我们还没有开始学用户界面开发,这里只是让新手看到用户界面的效果,同时引出我们教学过程中的程序入口,记得之前用 Playground 的时候,写完代码就能看见运行结果,但是真实的工程中不一样,程序需要一个开始运行的入口,我们可以把 ViewController 的 viewDidLoad 函数作为入口,只需要知道,当程序启动的时候会创建一个 ViewController 的实例用于显示用户界面,并调用实例的 viewDidLoad 函数
暂时忽略 ViewController 这个类,只在其中做一个入口
import UIKit
func lessonRun() {
print("Hello World!")
}
class ViewController: UIViewController {
let label = UILabel() // 创建一个用于显示文本的 UILabel
override func viewDidLoad() {
super.viewDidLoad()
//设置文本内容
label.text = "Hello World!"
//计算文本内容大小
label.sizeToFit()
//将文本添加到视图上
view.addSubview(label)
lessonRun()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//将文本内容居于屏幕中央
label.center = CGPoint.init(x: view.frame.width / 2, y: view.frame.height / 2)
}
}
自定义类
现在我们就把 lessonRun 函数当做 Playground 吧,回到正题,我们希望自己来定义一个 人 的类型,为了方便代码管理,在项目文件中新建一个文件加专门用来存放相关联的文件
右键点击 HelloWorld 文件夹,点击 Add Files to ,然后点击左下角的 New Folder ,输入文件夹名字 ClassLesson ,然后点击右下角的 Add 按钮,一个文件夹就创建好了
开发实际项目的时候,我们会创建很多很多的 .swift 代码文件,而每一个文件基本上就是一个类型的定义,因此合理的用文件夹来进行分类是一个很有必要的习惯
右键点击 ClassLesson 文件夹,点击 New File ,勾选 iOS 、Swift File,点击 Next,输入文件名 Person 后点击 Create ,现在我们在 ClassLesson 文件夹下面创建了一个代码文件专门用来定义 Person 类
class Person {
}
定义类的方式就是 class + 类名 + { },类名的命名规则采用首字母大写的方式,并且最好有一个前缀名,因为在编程的过程中可能会用到别人写的代码,就很有可能出现类名冲突,前缀能避免绝大部分冲突,并且前缀也充当一个标记,你可以使用自己的昵称缩写作为前缀,比如 ZQPerson ,教学中我们就暂时不用前缀了,只要类名能大致表名意图就行了
存储属性
Person 这个类型我们首先想到的是,应该有身份证号,姓名,籍贯,年龄,性别这些基本属性
enum Gender {
case unknown
case male
case female
}
class Person {
var id: String = ""
var name: String = ""
var hometown: String = ""
var age: Int = 0
var gender: Gender = .unknown
}
有了这样的定义我们就可以开始创建一个 Person 类的实例了,修改lessonRun 函数如下
func lessonRun() {
let person = Person.init()
person.id = "1234567890"
person.name = "JiangMing"
person.hometown = "ChongQing"
person.age = 25
person.gender = .male
//以上代码创建了一个人的实例,并设置了其基本属性
print("My name is \(person.name) and I am \(person.age) years old")
}
功能函数
我们可以为类定义一些特有的功能,例如可以赋予 Person 类自我介绍的能力
enum Gender {
case unknown
case male
case female
}
class Person {
var id: String = ""
var name: String = ""
var hometown: String = ""
var age: Int = 0
var gender: Gender = .unknown
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
func lessonRun() {
let person = Person.init()
person.id = "1234567890"
person.name = "JiangMing"
person.hometown = "ChongQing"
person.age = 25
person.gender = .male
//以上代码创建了一个人的实例,并设置了其基本属性
person.makeIntroduction()
}
构造函数
类的实例由构造函数创建出来,默认的构造函数是 init ,我们可以自己修改构造函数
设想一下,人的身份证号和家乡这样的属性应该在创建的时候就指定,并且不允许修改,最合理的方式就是把它们定义为常量,而类的常量属性必须在构造函数中初始化
enum Gender {
case unknown
case male
case female
}
class Person {
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
/** 自定义构造函数*/
init(id: String, hometown: String) {
//当参数名与自身的属性名冲突时,使用self表示自己,没有参数名冲突时可以省略self
self.id = id
self.hometown = hometown
}
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
func lessonRun() {
let person = Person.init(id: "1234567890", hometown: "ChongQing")
person.name = "JiangMing"
person.age = 25
person.gender = .male
//以上代码创建了一个人的实例,并设置了其基本属性
person.makeIntroduction()
}
构造函数跟普通函数是区别的,它是用来创建实例的特殊函数,我们习惯用实例来统称创建出来的实体,也可以把 类 的实例叫做 对象 ,但是像字符串,整数这样的实例是不能叫做 对象 的
构造函数还可以有很多个
class Person {
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
/** 自定义构造函数*/
init(id: String, hometown: String) {
//当参数名与自身的属性名冲突时,使用self表示自己,没有参数名冲突时可以省略self
self.id = id
self.hometown = hometown
}
/** 构造函数可以有多个*/
init(id: String, hometown: String, name: String) {
self.id = id
self.hometown = hometown
self.name = name
}
/** convenience 修饰的叫便利构造函数,便利构造函数必须调用一个非便利构造函数*/
convenience init(id: String, hometown: String, name: String, gender: Gender) {
self.init(id: id, hometown: hometown, name: name)
self.gender = gender
}
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
构造函数有许多许多规则讲究,在实际开发中遇到问题的时候可以根据错误提示去一步步完善知识点
析构函数
对象在使用完成后会被自动释放销毁,在对象被释放的时候它可能占用了系统资源需要我们手动去释放资源占用,因此我们需要知道对象何时被销毁,这就需要用到析构函数
class Person {
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
/** 自定义构造函数*/
init(id: String, hometown: String) {
//当参数名与自身的属性名冲突时,使用self表示自己,没有参数名冲突时可以省略self
self.id = id
self.hometown = hometown
}
/** 构造函数可以有多个*/
init(id: String, hometown: String, name: String) {
self.id = id
self.hometown = hometown
self.name = name
}
/** convenience 修饰的叫便利构造函数,便利构造函数必须调用一个非便利构造函数*/
convenience init(id: String, hometown: String, name: String, gender: Gender) {
self.init(id: id, hometown: hometown, name: name)
self.gender = gender
}
/** 析构函数*/
deinit {
print("\(name) 的生命周期结束,被自动销毁")
}
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
属性观察器
假如当我们的年龄长一岁时,需要作出一些反应,我们可以用属性观察器来实现
var age: Int = 0 {
willSet(newAge) {
print("我现在 \(age) 岁")
print("我马上 \(newAge) 岁")
}
didSet(oldAge) {
print("我刚刚 \(oldAge) 岁")
print("我现在 \(age) 岁")
}
}
func lessonRun() {
let person = Person.init(id: "1234567890", hometown: "ChongQing")
person.name = "JiangMing"
person.age = 25
person.gender = .male
//以上代码创建了一个人的实例,并设置了其基本属性
person.age += 1
}
属性观察器有两个, willSet() 观察器观察属性改变之前的状态并捕捉新值,didSet() 观察器观察属性改变之后的状态并捕捉旧值,这两个观察器使用一个就够了,通常我们使用简写方式
var age: Int = 0 {
willSet {
print("我现在 \(age) 岁")
print("我马上 \(newValue) 岁")
}
didSet {
print("我刚刚 \(oldValue) 岁")
print("我现在 \(age) 岁")
}
}
属性观察器中 默认使用 newValue 表示新值,oldValue 表示旧值
计算属性
还有一种属性叫做计算属性,顾名思义,计算属性不是实体存在的属性,而是在需要使用的时候去动态计算的属性,比如说我们需要给 Person 类新增一个 姓 和 名 的属性,如果把这两个新增属性设计成存储属性,显然是不合理了,因为已经有了 姓名 这样的存储属性,姓 和 名 只需要通过 姓名 来动态计算就行了
class Person {
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
/** 获取姓氏*/
var familyName: String? {
if name.isEmpty {
return nil
}
//截取第一个字符
return String(name.first!)
}
/** 获取名*/
var givenName: String? {
if name.isEmpty {
return nil
}
//去掉第一个字符
return String(name.dropFirst())
}
/** 自定义构造函数*/
init(id: String, hometown: String) {
//当参数名与自身的属性名冲突时,使用self表示自己,没有参数名冲突时可以省略self
self.id = id
self.hometown = hometown
}
/** 构造函数可以有多个*/
init(id: String, hometown: String, name: String) {
self.id = id
self.hometown = hometown
self.name = name
}
/** convenience 修饰的叫便利构造函数,便利构造函数必须调用一个非便利构造函数*/
convenience init(id: String, hometown: String, name: String, gender: Gender) {
self.init(id: id, hometown: hometown, name: name)
self.gender = gender
}
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
这种方式只适用于汉语单姓,不够严谨,仅用于说明计算属性的特点
这样的计算属性叫做只读计算属性,因为我们只能通过这它来获取结果,不能通过它来设置新的值,如果我们希望能够设置 姓 或者 名 来设置姓名 ,就得把 只读计算属性 改为 读写计算属性
class Person {
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
/** 姓氏*/
var familyName: String? {
get {
if name.isEmpty {
return nil
}
//截取第一个字符
return String(name.first!)
}
set {
if newValue != nil && !name.isEmpty {
name = newValue! + givenName!
}
}
}
/** 名*/
var givenName: String? {
get {
if name.isEmpty {
return nil
}
//去掉第一个字符
return String(name.dropFirst())
}
set {
if newValue != nil && !name.isEmpty {
name = familyName! + newValue!
}
}
}
/** 自定义构造函数*/
init(id: String, hometown: String) {
//当参数名与自身的属性名冲突时,使用self表示自己,没有参数名冲突时可以省略self
self.id = id
self.hometown = hometown
}
/** 构造函数可以有多个*/
init(id: String, hometown: String, name: String) {
self.id = id
self.hometown = hometown
self.name = name
}
/** convenience 修饰的叫便利构造函数,便利构造函数必须调用一个非便利构造函数*/
convenience init(id: String, hometown: String, name: String, gender: Gender) {
self.init(id: id, hometown: hometown, name: name)
self.gender = gender
}
/** 自我介绍*/
func makeIntroduction() {
print("My name is \(name)")
print("I'm from \(hometown) and I am \(age) years old")
}
}
func lessonRun() {
let person = Person.init(id: "1234567890", hometown: "ChongQing")
person.name = "姜小明"
person.age = 25
person.gender = .male
print(person.name)
person.familyName = "张"
person.givenName = "小琦"
print(person.name)
}
类的继承
如果我们现在要定义一个 教师 的类型,会遇到一个问题,如果从头开始定义,就需要把 身份证,名字,家乡等等一切属性和功能全部重新定义一遍,这样就不符合 不写重复代码 的原则,很明显 教师 也是 人 ,拥有 人 的一切属性和能力,同时也具备普通人不具备的属性和能力,因此我们可以在 人 的定义之上来定义 教师 ,这就是类的继承,继承的类叫做 子类 ,被继承的类叫 父类 ,子类 拥有 父类 的一切特性并可以定义自己特有的特性,在 ClassLesson 文件夹下新建一个 Teacher.swift 文件
class Teacher: Person {
}
类的继承方式如上,冒号 + 父类型,我们定义了一个新的 Teacher 类,下面就来为其定义它独有的特性
class Teacher: Person {
/** 教师证*/
var workID: String = ""
/** 所属学校*/
var school: String = ""
/** 所任学科*/
var subject: String = ""
}
func lessonRun() {
let person = Teacher.init(id: "1234567890", hometown: "YiChang")
person.name = "张琦"
person.age = 25
person.gender = .female
person.workID = "9876543210"
person.school = "Huazhong University of Science and Technology"
person.subject = "Sociology"
}
在 Person 类的定义中有一个自我介绍的函数,Teacher 类同样有此函数,但是老师的自我介绍可能需要新增一些内容,我们可以在 Teacher 类中重写自我介绍的方法
class Teacher: Person {
/** 教师证*/
var workID: String = ""
/** 所属学校*/
var school: String = ""
/** 所任学科*/
var subject: String = ""
override func makeIntroduction() {
//如果需要执行父类中的函数定义,使用 super 表示父类
super.makeIntroduction()
print("我是一名 \(subject) 教师")
print("我在 \(school) 任教")
}
}
func lessonRun() {
let person = Teacher.init(id: "1234567890", hometown: "YiChang")
person.name = "张琦"
person.age = 25
person.gender = .female
person.workID = "9876543210"
person.school = "Huazhong University of Science and Technology"
person.subject = "Sociology"
person.makeIntroduction()
}
存储属性不能重写,计算属性和函数可以重写,重写时如果需要执行父类对应的函数或者计算属性,使用 super 来指向父类
静态属性和静态函数
以上的属性和函数都属于某个真实存在的实例,而有些属性或者函数则需要设计成 类型 共享,假如我们要给 Person 这个类新增一个 生物学定义 的属性,显然 生物学定义 这样的属性是属于 Person 这个类型,而非具体的某个 Person 的实例,就需要用 static 将其声明为静态属性
class Person {
static let biologyDefinition = "人属于真核域,动物界,后生动物亚界,后口动物总门,脊索动物门,脊椎动物亚门,羊膜总纲,哺乳纲,兽亚纲,真兽次亚纲,灵长目,真灵长半目,直鼻猴亚目,人猿次目,狭鼻下目,真狭鼻小目,人猿超科,人科,人亚科,人族,人属,人亚属,智人种"
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
}
func lessonRun() {
print(Person.biologyDefinition)
print(Teacher.biologyDefinition)
}
在使用静态属性的时候并不需要创建一个实例,因为静态属性属于类型本身,不属于某个实例,把 生物学属性 改成函数
class Person {
static func biologyDefinition() -> String {
return "人属于真核域,动物界,后生动物亚界,后口动物总门,脊索动物门,脊椎动物亚门,羊膜总纲,哺乳纲,兽亚纲,真兽次亚纲,灵长目,真灵长半目,直鼻猴亚目,人猿次目,狭鼻下目,真狭鼻小目,人猿超科,人科,人亚科,人族,人属,人亚属,智人种"
}
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
}
func lessonRun() {
print(Person.biologyDefinition())
print(Teacher.biologyDefinition())
}
静态函数还可以用 class 来修饰,用 class 修饰的静态函数唯一的区别就是可以在子类中重写
class Person {
class func biologyDefinition() -> String {
return "人属于真核域,动物界,后生动物亚界,后口动物总门,脊索动物门,脊椎动物亚门,羊膜总纲,哺乳纲,兽亚纲,真兽次亚纲,灵长目,真灵长半目,直鼻猴亚目,人猿次目,狭鼻下目,真狭鼻小目,人猿超科,人科,人亚科,人族,人属,人亚属,智人种"
}
let id: String
var name: String = ""
let hometown: String
var age: Int = 0
var gender: Gender = .unknown
}
class Teacher: Person {
override class func biologyDefinition() -> String {
return "人,教师"
}
/** 教师证*/
var workID: String = ""
/** 所属学校*/
var school: String = ""
/** 所任学科*/
var subject: String = ""
}
func lessonRun() {
print(Person.biologyDefinition())
print(Teacher.biologyDefinition())
}
嵌套类型
我们可以把 Gender 枚举的定义写在 Person 类的定义中
class Person {
enum Gender {
case unknown
case male
case female
}
}
对于 Person 类自身来说使用没有什么区别,但是在其他地方要使用这个枚举,就需要像下面这样
func lessonRun() {
let person = Teacher.init(id: "1234567890", hometown: "YiChang")
person.name = "张琦"
person.age = 25
person.gender = .female
let male = Person.Gender.male
let female = Person.Gender.female
}
类型扩展
我们可以在类型的定义之外为某个类型新增功能,包括函数和计算属性,叫做 类型扩展
extension Person {
/** 伴侣*/
var mate: Person? {
return nil
}
/** 吃*/
func eat() {
}
/** 喝*/
func drink() {
}
/** 睡觉*/
func sleep() {
}
}
将这段代码写在 Person 类型定义之外,Person 类型现在拥有了吃,喝,睡的函数和 伴侣 的计算属性,我们可以通过扩展为任何类型新增功能,包括 Swift 基础类型
结构体
结构体 是相对于 类 更加轻量的封装,多用于封装简单的数据类型,例如 点 这样的类型
struct Point {
var x: Double = 0
var y: Double = 0
init(x: Double, y: Double) {
self.x = x
self.y = y
}
//对称点
var symmetrical: Point {
return Point.init(x: -x, y: -y)
}
//判断与另一个点是否相等
func isEqualTo(_ another: Point) -> Bool {
return x == another.x && y == another.y
}
//对于结构体来说,如果要在函数中更改自身的属性,函数必须声明为 mutating ,表示这个函数只有变量可以使用
mutating func reset() {
x = 0
y = 0
}
}
let point1 = Point.init(x: 1, y: 2)
var point2 = Point.init(x: 3, y: 4)
point2.reset()
结构体使用起来跟类没有多大区别,实际上我们之前用过的所有基础数据类型,包括整数,字符串,浮点数都是结构体,结构体跟类最大的区别在于,一个是值类型,一个是引用类型
值类型和引用类型
func lessonRun() {
let person = Person.init(id: "1234567890", hometown: "ChongQing")
person.name = "JiangMing"
person.age = 25
person.gender = .male
let anotherPerson = person
anotherPerson.name = "JiangXiaoMing"
print(person.name)
let point = Point.init(x: 0, y: 0)
var anotherPoint = point
anotherPoint.x = 1
anotherPoint.y = 1
print(point)
}
差不多的两段代码,结果却大不相同,Person 是一个类,属于引用类型,当我们把 Person 的实例赋值给另外一个常量时,仅仅表示现在两个常量 person 和 anotherPerson 都指向了同一个实例,所以我们无论调用哪个常量没有任何区别
而 Point 是一个结构体,属于值类型,值类型以拷贝的形式进行赋值,也就是说当我们执行 var anotherPoint = point 这句话的时候,实际上是讲 point 当前的值原原本本地复制一份赋值给 anotherPoint ,之后就没有任何关联了,因此改变 anotherPoint 的值并不影响 point 的值
什么时候用 类 什么时候用 结构体 ,例子中已经给出了答案,在模拟现实问题的时候,人 这样的东西是不可能以拷贝的形式存在,人 是一个独立的个体,完成了一生的任务自然被销毁,随便一调用就拷贝出了一个一模一样的人那就全乱套了,但是像 地点 这样的东西只是一个信息载体,不具有独立存在的特点,我告诉你我的地点,只是将我当前的地点的信息拷贝一份给你,但是不能说我把我的地点告诉了你,你就能对我的地点这样的属性进行任何修改,除非你有超能力能让我飞
这节课我们初步学习了类与结构体的知识,已经可以开始对实际问题进行抽象模拟了,下一节课我们学习另一个重要知识:协议