Swift基础8

类和结构体

1.Swift中类和结构体有很多共同点:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义附属脚本用于访问值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 实现协议以提供某种标准功能

2.与结构体相比,类还有如下附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 解构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

语法定义

类和结构体有着类似的定义方式。我们通过关键字class和struct来分别表示类和结构体,并在一对大括号中定义它们的具体内容:

class SomeClass {

}

struct SomeStruct {

}

eg:

struct Engine {
    var power = 0
    var age = 0
}

class Car {
    
    var engine = Engine()
    var name:String?
    var speed = 0
}

结构体和类都适用构造器语法来生成新的实例。构造器语法的最简单形式时在结构体或者类的类型名称后跟随一对空括号。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。

let engine = Engine()
let car = Car()

属性访问

1.通过使用点语法,我们可以访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者通过点(.)连接。
2.我们也可以使用点语法为变量属性赋值。

print(car.engine.power)
car.engine.power = 86
print(car.engine.power)

结构体类型和成员逐一构造器

所有的结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:

let engine2 = Engine(power: 100,age: 20)

与结构体不同,类实例没有默认的成员逐一构造器。

结构体和枚举是值类型

值类型 被赋予给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。

在之前的章节中,我们已经大量的使用了值类型。实际上,在Swift中,所有的基本类型:整数,浮点数,布尔值,字符串,数组和字典都是值类型,并且在底层都是以结构体的形式所实现。

在Swift中,所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型属性,在代码中传递的时候都会被复制。

struct Engine {
    var power = 0
    var age = 0
}

class Car {
    
    var engine = Engine()
    var name:String?
    var speed = 0
}

var engine = Engine(power: 100,age: 20)
var engine2 = engine

engine2.power = 101

print(engine)
print(engine2)


enum Direction {
    case Wast,East,South,North
}

var direction1 = Direction.East
var direction2 = direction1
direction2 = .South

print(direction1)
print(direction2)

类是引用类型

与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,引用的是已存在的实例本身而不是其拷贝。

let car = Car()
car.name = "BZ"
let car2 = car
car2.name = "DZ"
print(car.name!)
print(car2.name!)

恒等运算符

因为类是引用类型,有可能有多个常量和变量在幕后同时引用同一个类的实例。如果能够判定两个常量或者变量是否引用同一个类实例将会很有帮助。为了达到这个目的,Swift内建了两个恒等运算符:

  • 等价于 (===)
  • 不等价于 (!==)

运用这两个运算符检测两个常量或者变量是否引用同一个实例。

note: “等价于” (用三个等号表示, ===) 与“等于” (用两个等号表示,==)的不同:
1.“等价于”表示两个类类型的常量或者变量引用同一个类实例。
2.“等于”表示两个实例的值“相等”或“相同”,判定时要遵照设计者定义的评判标准,因此相对于“相等”来说,这是一种更加合适的叫法。

类和结构体的选择

结构体实例是通过值传递的,类实例是通过引用传递的。这意味着两者适用不同的任务。
按照通用的标准,当符合一条或者多条一下条件时,请考虑使用结构体:

1.该数据结构的主要目的是用来封装少量相关简单数据值。
2.有理由预计该数据结构的实例在被赋值或传递时,封装的数据将会被拷贝而不是被引用。
3.该数据结构中存储的值类型属性,也应该被拷贝,而不是被引用。
4.该数据结构不需要去继承另一个既有类型的属性或者行为。

字符串,数组和字典类型的赋值与复制行为

Swift中,许多基本类型,如String,Array和Dictionary类型均以结构体的形式实现。这意味着被赋值给新的常量或变量,或者被传入函数或方法时,它们的值会被拷贝。

在OC中,NSString,NSArray和NSDictionary类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或方法时,不会发生值拷贝,而是传递现有实例的引用。

属性

属性将值跟特定的类、结构体或枚举关联。存储属性存储常量或变量作为实例的一部分,而计算属性计算一个值。计算属性可用于类,结构体和枚举,存储属性只能用于类和结构体。

存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接作用于类型本身,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到父类继承的属性上。

存储属性

1.简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或者变量。存储属性可以是变量存储属性,也可以是常量存储属性。

2.可以在定义存储属性的时候指定默认值,也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值。

struct FixedLengthRange {
    var firstValue:Int
    let length: Int
    
}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
rangeOfThreeItems.firstValue = 6

在上面的例子中,length在创建实例的时候被初始化,因为它是一个常量存储属性,所以之后无法修改它的值。

3.常量结构体的存储属性
如果创建了一个结构体的实例并将其赋值给一个常量,则无法修改改实例的任何属性,即使定义了变量存储属性:

let  rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
//因为结构体是值类型,赋值给let常量以后就无法修改
rangeOfThreeItems.firstValue = 6

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

4.延迟存储属性
延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用lazy来表示一个延迟存储属性。

note:必须将延迟存储属性声明成变量(使用var关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

/* DataImporter是一个将外部文件中的数据导入类,这个类的初始化会消耗不少时间*/
class DataImporter {
    var filename = "data.txt"
}

class DataManager {
    lazy var importer = DataImporter()
    
    var data = [String]()
}

let manager = DataManager()
manager.data.append("some data")
manager.data.append("some more data")
//第一次访问时,才进行初始化
print(manager.importer.filename)

note:如果一个被标记为lazy的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

5.存储属性和实例变量
Swift中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。这个类型中属性的全部信息---包括命名、类型和内存管理特征,都在唯一一个地方(类型定义中)定义。

计算属性

除了存储属性外,类、构造体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个getter和一个可选的setter,来间接获取和设置其他属性或变量的值。


struct Point {
    var x = 0.0, y = 0.0
}

struct Size {
    var width = 0.0, height = 0.0
}


struct Rect {
    
    var origin = Point()
    var size = Size()
    
    var center:Point {
        
        get{
            let centerX = origin.x + size.width / 2
            let centerY = origin.y + size.height / 2
            
            return Point(x: centerX, y: centerY)
        }
        
        set(newCenter){
            origin.x = newCenter.x - size.width / 2
            origin.y = newCenter.y - size.height / 2
            
        }
    }
}

var square = Rect(origin: Point(x:0.0,y: 0.0), size: Size(width: 10.0, height: 10.0))

let center = square.center

square.center = Point(x: 100, y: 100)

print(square)

1.便捷setter声明
如果计算属性的setter没有定义表示新值的参数名,则可以使用默认newValue。下面是使用了便捷setter声明的Rect结构体代码:

struct Point {
    var x = 0.0, y = 0.0
}

struct Size {
    var width = 0.0, height = 0.0
}


struct Rect {
    
    var origin = Point()
    var size = Size()
    
    var center:Point {
        
        get{
            let centerX = origin.x + size.width / 2
            let centerY = origin.y + size.height / 2
            
            return Point(x: centerX, y: centerY)
        }
        
        set{
            origin.x = newValue.x - size.width / 2
            origin.y = newValue.y - size.height / 2
            
        }
    }
}

var square = Rect(origin: Point(x:0.0,y: 0.0), size: Size(width: 10.0, height: 10.0))

let center = square.center

square.center = Point(x: 100, y: 100)

print(square)

2.只读计算属性
只有getter没有setter的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

note:必须使用var关键字定义属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后在也无法修改的值。

只读计算属性的声明可以去掉get关键字和花括号

struct rect {
    
    var  width = 0
    var height = 0
    
    var boundLength : Int  {
        
        return  (width + height) * 2;
    }
    
}

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现状的值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。
note:不需要为非重写的计算属性添加属性观察器,因为通过它的setter直接监控和响应的变化。

可以为属性添加如下的一个或全部观察器:
1.willSet在新的值被设置之前调用。
2.didSet在新的值被设置之后立即调用。

willSet观察器会将新的属性值作为常量参数传入,在willSet的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称为newValue表示。

类似地,didSet观察器会将旧的属性作为参数传入,可用为该参数命名或者使用默认参数名oldValue。

note:父类的属性在子类的构造器中被赋值时,它在父类中的willSet和didSet观察器会被调用。

class StepCounter {
    
    
    var totalSteps: Int = 0 {
        
        willSet{
            print("the new value  \(newValue)")
        }
        
        didSet {
            print("the old Value \(oldValue)")
        }
        
        
    }
    
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 200


stepCounter.totalSteps = 100

note:如果在一个属性的didSet观察器里为它赋值,这个值会替换该观察器之前设置的值。

全局变量和局部变量

计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。全局变量时在函数、方法、闭包或任何类型之外定义的变量。局部变量时在函数、方法或闭包内部定义的变量。

全局或局部变量都属于存储型变量,跟存储属性类似,它提供特定类的存储空间,并允许读取和写入。

另外,在全局或局部范围都可以定义计算型变量和为存储类型变量定义观察器。计算型变量跟计算属性一样,返回一个计算的值而不是存储值,声明格式也完全一样。

note:全局的常量或变量都是延迟计算的,不同的地方在于,全局的常量或变量不需要标记lazy特性。
局部范围的常量或变量不会延迟计算

类型属性

实例的属性属于一个特定类型实例。每次类型实例化后都拥有自己的一套属性值,实例之间的属性相互独立。也可以为类型本身定义属性,不管类型有多少个实例,这些属性都只有唯一一份。这种属性就是类型属性。

类型属性用于定义特定类型所有实例共享的数据,比如所有的实例都能有一个常量(就像c语言中的镜头常量),或者所有实例都能访问的一个变量(就像c语言中的静态变量)

值类型的存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算属性一样只能定义成变量属性。

note:跟实例的存储属性不同,必须给存储型类型属性指定默认值,因为类型本身无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行初始化一次,并且不需要对其使用lazy修饰符。

1.类型属性语法
在c或Objective-c中,与某个类型关联的静态常量和静态变量,是作为全局(global)静态变量定义的。但是在Swift编程语言中,类型属性是作为类型定义的一部分写在类型最外的花括号内,因此它的作用范围也就是在类型支持的范围内。

使用关键字static来定义类型属性。在为类(class)定义计算型类型属性时,可以使用关键字class来支持子类对父类的实现进行重写。

struct SomeStructure {
    
    static var storedTypeProperty = "Some value"
    static var computedTypeProperty : Int {
        return 6
    }
    
}


enum SomeEnum {
    static var storedTypeProperty = "Some value"
    static var cpmputedTypeProperty : Int {
        return 6
    }
}

class SomeClass {
    
    static var storedTypeProperty = "Some Value"
    static var computedTypeProperty : Int {
        return 6
    }
    
    class var overrideableComputedTypeProperty: Int {
        return 100
    }
    
}

2.获取和设置类型属性的值
跟实例的属性一样,类型属性的访问也是通过点运算符来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例

SomeClass.storedTypeProperty = "hello"
print(SomeClass.storedTypeProperty)

方法

方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法:实例方法为给类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与Objective-c中的类方法(class methods)相似。

结构体和枚举能够定义方法是Swfit与c/Objective-c的主要区别之一。在Objective-c中,类是唯一能定义方法的类型。但在Swift中,你不仅能够选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。

实例方法

实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供了实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致。

实例方法要写在它所属的类型的前后大括号之间。实例方法能够隐式访问它所属类型的所有其他实例方法和属性。实例方法职能被它所属的类的某个特定的实例调用。实例方法不能脱离于现实的实例而被调用。

class Counter {
    
    var count = 0
    
    func increment(){
        count += 1;
    }
    
    func incrementBy(amount: Int){
        count += amount
    }
    
    func reset(){
        count = 0
    }
}

方法局部参数名和外部参数名称

函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用)。方法参数也一样(因为方法就是函数,只是这个函数与某个类型相关联了)。

Swift中的方法和Objective-c中的方法极其相似。像在Objective-c中一样,Swift中方法名称通常用一个介词指向方法的第一个参数,比如:with,for,by等等。前面的Counter类的例子中incrementBy(_:)方法就是这样的。介词的使用让方法在被调用时候能像一个句子一样被解读。

具体来说,Swift默认仅给方法的第一个参数名称一个局部参数名称;默认同时给第二个和后续的参数名称局部参数和外部参数名称。这个约定与典型的命名和调用约定相适应,与你在写objective-c的方法时很相似。这个约定还让表达式方法在调用时不需要在限定参数名称。

class Counter {
    
    var count = 0
    
    
    func incrementBy(amount: Int ,numberOfTimes:Int ){
        count += amount * numberOfTimes
    }
    
}

let counter = Counter()
counter.incrementBy(5, numberOfTimes: 10)
print(counter.count)

修改方法的外部参数名称

有时为方法的第一个参数提供一个外部参数名称是非常有用的,尽管这不是默认的行为。我们可以自己添加一个显式的外部名称作为第一个参数的前缀来把这个局部名称当作外部名称使用。

相反,如果我们不想为方法的第二个以及后续的参数提供一个外部名称,可以通过使用下划线(_)作为该参数的显式外部名称,这样做将覆盖默认行为。

self属性

类型的每一个实例都有一个隐含属性叫做self,self完全等同于该实例本身。我们可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

func increment(){
    self.count++
}

实际上,我们不必在我们的代码里面经常写self。不论何时,只要在一个方法中使用一个已知的属性或者方法名称,如果没有明确的写self,Swift假定你是指当前实例的属性或者方法。

使用这条规则的主要场景是实例方法的某个参数名称与实例方法的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,而且在引用属性时必须使用一种更严格的方式。这时我们可以使用self属性来区分参数名称和属性名称。

struct Point {
    
    var x = 0.0, y = 0.0
    func isToTheRightOfX(x:Double)->Bool {
        return self.x > x
    }
    
}

在实例方法中修改值类型

结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确定需要在某个具体的方法中修改结构体或者枚举的属性,我们可以选择 变异(mutating)这个方法(可变方法),然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时会保留在原始结构中。方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

struct Point {
    
    var x = 0.0 , y = 0.0
    
    mutating func moveByX(x:Double , y: Double){
        
        self.x += x
        self.y += y
        
    }
    
}

var somePoint =  Point(x: 1, y: 1)
print("(\(somePoint.x),\(somePoint.y))")

somePoint.moveByX(100, y: 20)
print("(\(somePoint.x),\(somePoint.y))")

在可变方法中给self赋值

可变方法能够赋给隐含属性self一个全新的实例。

struct Point {
    
    var x = 0.0 , y = 0.0
    
    mutating func moveByX(x:Double , y: Double){
        
       self = Point(x: self.x + x, y: self.y + y)
        
    }
    
}

枚举的可变方法可以把self设置为相同的枚举类型中不同的成员:


enum SwitchState {
    
    case Off,Low,High
    
    mutating func nex() {
        switch self {
        case Off:
            self = Low
        case Low:
            self = High
        case High:
            self = Off
        }
    }
    
}

var lowState = SwitchState.Low
print(lowState)

lowState.nex()
print(lowState)

lowState.nex()
print(lowState)

lowState.nex()
print(lowState)

类方法

实例方法是被类型的某个实例调用的方法。你也可以定义类型本身调用的方法,这种方法就叫做类型方法。声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可以使用关键在class来允许子类重写父类的实现方法。

note:在objective-c里面,我们只能为类定义类型方法。在Swift中,我们可以为所有的类、结构体和枚举定义类型方法;每一个类型方法都被它所支持的类型显式包含。

类型方法和实例方法一样用点语法调用。但是,我们在类型层面上调用这个方法,而不是在实例层面上调用。

在类型方法的方法体中,self指向这个类型本身,而不是类型的某个实例。对于结构体和枚举来说,这意味着我们可以用self来消除静态属性和静态方法参数之间的歧义。

一般来说,任何未限定的方法和属性名称,将会来自于本类中另外的类型级别的方法和属性。一个类型方法可以调用本类中另一个类型方法的名称,而无需在方法名称前面加上类型名称的前缀。同样,结构体和枚举的类型方法也能过直接通过静态属性的名称访问静态属性,而不需要类型名称前缀。

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

推荐阅读更多精彩内容