Swift的结构体、类

84 阅读11分钟

结构体

Swift 的结构体,让我们可以创建d自定义复杂的数据类型,并拥有自己的变量和函数。

关键字struct

struct Album {

    let title: String
    let artist: String
    let year: Int
    var number: Int

    func printSummary() {

        print("\(title) (\(year)) by \(artist)")
    }
    
    // 能改变自身的 变量的函数,需用mutating
    mutating func takeVacation(days: Int) {

        if 4 > days {
            number = days
        }
    } 
}

//创建 结构体实例。使用 初始化器
let redx = Album(title: "Red", artist: "Taylor Swift", year: 2012,number: 0)  
 

此时,Album就像StringInt————我们可以创建它们、赋值、复制它们等

  • mutating关键字

任何 更改属于结构体的可变属性的方法,都必须用特殊的mutating关键字标记

未标记为 mutating 的方法,无法调用 mutating 函数 - 您必须将它们都标记为 mutating

在 Swift 标准库中,比如Bool、Int、Double、 String、Array、Dictionary等常见类型都是结构体。

相关概念:属性 方法 实例 初始化器

属于结构体的变量和常量称为“属性”。

属于结构体的函数称为“方法”。

当我们从结构体中创建常量或变量时,我们将其称为“实例”

当我们创建结构体实例时,我们使用“初始化器”

  • 函数和方法有什么区别

Swift 的函数和方法,两者都可以接受任意数量的参数,包括可变参数,并且都可以返回值。 那么有什么区别呢?

老实说,唯一真正的区别是:方法是属于类型的,例如结构、枚举和类,而函数则不然。这是唯一的区别。方法与特定类型(如结构体)相关联,意味着方法获得了一项重要的能力:它们可以引用该类型内的其他属性和方法。 方法还有一个优点 :方法可以避免名称空间污染。

初始化器

Swift 在名为 的结构体中默默地创建一个特殊函数init(),使用该结构体的所有属性作为其参数。然后它会自动将这两段代码视为相同:

let x = Album(title: "Red", artist: "Taylor Swift", year: 2012number: 0)  

let x = Album.init(title: "Red", artist: "Taylor Swift", year: 2012number: 0)  

如果属性有默认值,初始化器会自动调整

struct Dog {

    let name: String 
    var year = 3
 
}

Dog(name: 'Tom')
Dog(name: 'Tom', year: 12)

如果给属性设置 可选变量,则会自动生成多个初始化器。

 struct Point {
    var x: Int?
    var y: Int?
}
// 创建。
var p1 = Point(x: 10, y: 10)
var p2 = Point(y: 10)
var p3 = Point(x: 10)
var p4 = Point()

黄金法则:所有属性在初始化程序结束时都必须有一个值

  • 自定义 初始化器
struct Player {

    let name: String
    let number: Int

    init(name: String, number: Int) {

        self.name = name
        self.number = number
        
        // 其他
    }
    
    init(name: String) {

        self.name = name
        number = Int.random(in: 1...9)
    } 

}  
  • self表示实例本身

初始化器 没有func关键字 ,也不会显式地具有返回类型

可以在初始化程序中调用结构体的其他方法,但在为所有属性赋值之前不能这样做

如果您创建自己的初始化程序,则删除成员初始化程序。

struct Employee {

    var name: String

    var yearsActive = 0

    init() {

        self.name = "Anonymous" 

    }

}

这时候,将不再被允许:

let roslin = Employee(name: "Laura Roslin")

如果您希望保留它,请将自定义初始值设定项移至扩展

extension Employee {

    init() {

        self.name = "Anonymous" 

    }
}  

属性

结构体可以有两种属性:

  • 存储属性:是在结构实例内保存一段数据的变量或常量。
  • 计算属性:在每次访问时 动态计算属性的值。这意味着,计算属性是存储属性和函数的混合:它们像存储属性一样访问,但像函数一样工作。
struct Employee { 
    // 存储属性
    var vacationAllocated = 14

    var vacationTaken = 0

    // 计算属性
    var vacationRemaining: Int {

        vacationAllocated - vacationTaken

    }
}  

// 计算属性(可读可写)
var vacationRemaining: Int {

    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }

}  
  • newValue是由 Swift 自动提供给我们的,并存储用户尝试分配给属性的任何值

什么时候应该使用计算属性或存储属性 ???

属性观察器

是在属性发生更改时 运行的特殊代码片段。它们采用两种形式:didSet观察者在属性刚刚更改时运行;willSet观察者在属性更改之前运行。

struct Game {

    var score = 0 {
        willSet {

            print("当前值= \(score)")
            print("新值 将= \(newValue)")

        }  
        didSet {

            print("分数= \(score)")
            print("旧值= \(oldValue)")  
        }
    }
} 

在实践中,willSet的使用量比 didSet少得多。性能考虑,请尽量避免让观察器 投入太多工作

访问控制

关键字修饰 属性或方法

private“不要让结构体之外的任何东西使用它”。

fileprivate“不要让当前文件之外的任何内容使用它”。

public“让任何人、任何地方都可以使用它”。

静态属性、静态方法

有时,我们希望将属性或方法添加到结构体本身,而不是其实例。使用关键字static修饰,这样,它们就属于结构体本身,而不是结构体的各个实例。 我们不需要创建实例,就可以使用。这意味着它们在结构体的实例上不是唯一存在的。

struct School {

    static var studentCount = 0

    static func add(student: String) {

        print("\(student) joined the school.")

        studentCount += 1

    }

}  

// 调用
School.add(student: "Taylor") 

静态属性和方法,不能引用非静态属性和方法

self 和 Self的含义不同:self指的是结构体的当前值,Self指的是当前类型

  • 静态属性的适用场景

1、使用静态属性来组织 应用程序中的通用数据 (全局数据);通用方法

如:

struct AppData {

    static let version = "1.3 beta 2"

    static let name = "xxxxx"

    static let homeURL = "https://www.xxxxxxx.com"

    static func getRandomInt() -> Int {

        return Int.random(in: 1...1000) + 1
    }  
}  

2、创建结构示例,方便预览

struct Employee {

    let username: String

    let password: String

    static let example = Employee(username: "xxx", password: "cccc")

}  

只有少数情况下静态属性或方法才有用途,但它们仍然是一个有用的工具

结构体 适用场景

结构体的主要目的是用来 封装少量相关简单数据值。

有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。

任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。

结构体不需要去继承另一个已存在类型的属性或者行为。

元组VS结构体

  • 当您想要从函数返回两个或多个任意值时,请使用元组。但当您想要多次发送或接收某些固定数据时,更喜欢使用结构体。

类与结构体有许多共同点,但在关键地方有所不同。

  • 共同点

您可以创建并命名它们。 您可以添加属性和方法,包括属性观察器和访问控制。 您可以创建自定义初始值设定项 来根据需要配置新实例。

  • 不同点
  1. 您可以使一个类继承另一个类中的功能,获取其所有属性和方法。如果您想重写某些方法,您也可以这样做。
  2. 由于第一点,Swift 不会自动为类生成成员初始化程序!!所以您要么需编写自己的初始值设定项,要么为所有属性分配默认值。
  3. (重要)当您复制类的实例时,两个副本共享相同的数据 - 如果您更改一个副本,另一个副本也会更改。即 浅拷贝。结构体的每个副本都是独立的,唯一的,并保存自己的数据。
  4. 当类实例的最终副本被销毁时,Swift 可以选择运行deinitializer函数。结构体没有此函数。
  5. 即使您将类设为常量,您仍然可以更改其var属性。 但常量结构体中的变量属性则不能。

编译器不会为类自动生成成员初始化程序。但希望您能理解,因为一个类可以基于另一个类 - 如果类 C 添加一个额外的属性,它将破坏 C、B 和 A 的所有初始化器。 类的作者必须手动编写自己的初始化程序

由于类的一个实例可以在多个位置引用,因此了解最终副本何时被销毁变得很重要。这就是去初始化器的用武之地:它允许我们清理在最后一个副本消失时分配的任何特殊资源。

如果类的所有成员 都在定义时初始化了,编译器会为类生成无参的初始化器

关键字class

class Point {
    var x: Int = 0
    var y: Int = 0
}
// 创建
var point = Point()

继承

Swift 允许我们基于现有类来创建类,这个过程称为继承。当一个类从另一个类(其“父类”或“超级”类)继承功能时,Swift 将授予新类(“子类”或“子类”)对该父类的属性和方法的访问权限,从而允许我们进行小的添加或更改以自定义新类的行为方式。

请在子类名称后面写一个冒号,然后添加父类名称。以下是例子:

// 父类
class Employee {

    let hours: Int

    // 必须 初始化
    init(hours: Int) {

        self.hours = hours
    }
    
    func printSummary() {

        print("工作了 \(hours) 小时")

    } 
} 

// 子类1
class Developer: Employee {

    func work() {

        print("我写代码有 \(hours) 小时.")

    }
}

// 子类2
class Manager: Employee {

    func work() {

        print("我开会了 \(hours) 小时.")

    }
}  
  • 子类可直接引用父类的属性,直接调用父类的公共方法

  • 子类想要更改父类的方法,则必须用override修饰该方法以重写

class Manager: Employee {

    override func printSummary() {

        print("工作中 开会了 \(hours) 小时")

    } 
} 

使用了override,但你的方法实际上并没有覆盖父类中的某些内容,Swift 将拒绝构建你的代码

父类的A方法不带参,子类想更改为带参,则不需要override。

final修饰的类,不可被继承

类的初始化器

如果子类有任何自定义初始化器,它必须始终在完成自己的初始化器后,调用父类的初始化器,如果有的话。

class Vehicle {

    let isElectric: Bool  // 是否 电动

    init(isElectric: Bool) {

        self.isElectric = isElectric

    }
}  

class Car: Vehicle {

    let isConvertible: Bool

    init(isElectric: Bool, isConvertible: Bool) {

        self.isConvertible = isConvertible

        super.init(isElectric: isElectric)   // 必须
    }
}

super是 Swift 自动为我们提供的另一个值,类似于self:它允许我们调用属于父类的方法

如果子类没有任何自己的初始值设定项,它将自动继承其父类的初始值设定项。

引用类型 和 值类型

类实例的所有副本共享相同的数据,这意味着您对一个副本所做的任何更改,都会自动更改其他副本。因为类在 Swift 中是引用类型 。结构体是值类型。

因此,在类中,不需要对更改其属性的方法使用关键字 mutating

反初始化器

Swift 的类可以选择赋予一个deinitializer,这有点像初始化器的反面,因为它在对象被销毁时而不是在创建时被调用.

如果你看一下大局,你会发现每个都使用大括号来创建它们的作用域:条件、循环和函数都创建局部作用域。

当一个值退出作用域时,我们意味着创建它的上下文将消失。对于结构来说,这意味着数据正在被破坏,但对于类来说,这意味着只有底层数据的一个副本会消失——其他地方可能仍然有其他副本。但是,当最终副本消失时(当指向类实例的最后一个常量或变量被销毁时),底层数据也会被销毁,并且它所使用的内存将返回给系统。

deinit {

    print("User \(id): I'm dead!")

}  
  • 自动引用计数

Swift 在幕后执行称为自动引用计数(ARC)的操作。ARC 跟踪每个类实例存在多少个副本:每次获取类实例的副本时,Swift 都会为其引用计数添加 1,每次销毁副本时,Swift 都会从其引用计数中减去 1。当计数达到 0 时,意味着没有人再引用该类,Swift 将调用其析构函数并销毁该对象。

类的使用

类不像结构体那样常用,但在共享数据方面,具有非常宝贵的用途

结构体与类对比

结构体是值类型,类是引用类型(指针类型)

  • 值类型 Swift 中所有的结构体、枚举类型都是值类型。当它被赋值给一个变量、常量或者被传递给一个函数的时候,其内容会被拷贝。相当于“深拷贝”,两者相互独立。

  • 引用类型 在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。相当于“浅拷贝”。

因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。判定两个常量或者变量,是否引用同一个类实例,Swift 提供了两个恒等运算符:相同===、不相同!==

由于结构体属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。属于引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性。