分类(Category):
概念
分类(Category)是OC中的特有语法,它是表示一个指向分类的结构体的指针。原则上它只能增加方法,不能增加成员(实例)变量。具体原因看源码组成:
注意:
1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
2.分类中的可以写@property
, 但不会生成setter/getter
方法, 也不会生成实现以及私有的成员变量(编译时会报警告);
3.可以在分类中访问原有类中.h中的属性;
4.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为分类 > 本类 > 父类
。因此在开发中尽量不要覆盖原有类;
5.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。
分类格式:
@interface 待扩展的类(分类的名称)
@end
@implementation 待扩展的名称(分类的名称)
@end
实际代码如下:
// Programmer+Category.h文件中
@interface Programmer (Category)
@property(nonatomic,copy) NSString *nameWithSetterGetter; //设置setter/getter方法的属性
@property(nonatomic,copy) NSString *nameWithoutSetterGetter; //不设置setter/getter方法的属性(注意是可以写在这,而且编译只会报警告,运行不报错)
- (void) programCategoryMethod; //分类方法
@end
// Programmer+Category.m文件中
那么问题来了:
为什么在分类中声明属性时,运行不会出错呢?
既然分类不让添加属性,那为什么我写了@property仍然还以编译通过呢?
接下来我们探究下分类不能添加属性的实质原因:
我们知道在一个类中用
@property
声明属性,编译器会自动帮我们生成成员变量和setter/getter
,但分类的指针结构体中,根本没有属性列表。所以在分类中用@property
声明属性,既无法生成成员变量也无法生成setter/getter
。
因此结论是:我们可以用@property
声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了_成员变量和setter/getter
方法,报错就在所难免了。
报错原因如下
//普通声明,无setter/getter
// programmer.nameWithoutSetterGetter = @"无setter/getter"; //调用setter,编译成功,运行报错为:(-[Programmer setNameWithSetterGetter:]: unrecognized selector sent to instance 0x7f9de358fd70')
// NSLog(@"%@",programmer.nameWithoutSetterGetter); //调用getter,编译成功,运行报错为-[Programmer setNameWithSetterGetter:]: unrecognized selector sent to instance 0x7fe22be11ea0'
// NSLog(@"%@",_nameWithoutSetterGetter); //这是调用_成员变量,错误提示为:(Use of undeclared identifier '_nameWithoutSetterGetter')
那接下来我们继续思考:
既然报错的根本原因是使用了系统没有生成的setter/getter
方法,可不可以在手动添加setter/getter
来避免崩溃,完成调用呢?
其实是可以的。由于OC是动态语言,方法真正的实现是通过runtime
完成的,虽然系统不给我们生成setter/getter
,但我们可以通过runtime
手动添加setter/getter
方法。那具体怎么实现呢?
代码实现如下:
按照这个思路,我们通过运行时手动添加这个方法。
#import <objc/runtime.h>
static NSString *nameWithSetterGetterKey = @"nameWithSetterGetterKey"; //定义一个key值
@implementation Programmer (Category)
//运行时实现setter方法
- (void)setNameWithSetterGetter:(NSString *)nameWithSetterGetter {
objc_setAssociatedObject(self, &nameWithSetterGetterKey, nameWithSetterGetter, OBJC_ASSOCIATION_COPY);
}
//运行时实现getter方法
- (NSString *)nameWithSetterGetter {
return objc_getAssociatedObject(self, &nameWithSetterGetterKey);
}
@end
实际使用效果
//通过runtime实现了setter/getter
programmer.nameWithSetterGetter = @"有setter/getter"; //调用setter,成功
NSLog(@"%@",programmer.nameWithSetterGetter); //调用getter,成功
// NSLog(@"%@",_nameWithSetterGetter); //这是调用_成员变量,错误提示为:(Use of undeclared identifier '_nameWithSetterGetter')
问题解决。
但是注意,以上代码仅仅是手动实现了
setter/getter
方法,但调用_成员变量
依然报错。
类扩展(Class Extension)
Extension是Category的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。
其实开发当中,我们几乎天天在使用。对于有些人来说像是最熟悉的陌生人。
类扩展格式:
@interface XXX ()
//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end
作用:
为一个类添加额外的原来没有变量,方法和属性
一般的类扩展写到.m
文件中
一般的私有属性写到.m
文件中的类扩展中
类别与类扩展的区别:
①类别中原则上只能增加方法(能添加属性的的原因只是通过
runtime
解决无setter/getter
的问题而已);
②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private
类型的(用范围只能在自身类,而不是子类或其他地方);
③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
swift的扩展
扩展就是向一个已有的类、结构体或枚举类型添加新功能(functionality
)。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即逆向建模)。扩展和 Objective-C
中的分类(categories
)类似。(不过与Objective-C
不同的是,Swift
的扩展没有名字。)
Swift 中的扩展可以:
- 添加计算型属性和计算静态属性
- 定义实例方法和类型方法
- 提供新的构造器
- 定义下标
- 定义和使用新的嵌套类型
- 使一个已有类型符合某个协议
注意:
如果你定义了一个扩展向一个已有类型添加新功能,那么这个新功能对该类型的所有已有实例中都是可用的,即使它们是在你的这个扩展的前面定义的。
扩展语法
声明一个扩展使用关键字extension:
extension SomeType {
// 加到SomeType的新功能写到这里
}
一个扩展可以扩展一个已有类型,使其能够适配一个或多个协议(protocol)。当这种情况发生时,协议的名字应该完全按照类或结构体的名字的方式进行书写:
extension SomeType: SomeProtocol, AnotherProctocol {
// 协议实现写到这里
}
一个扩展可以扩展一个已有类型,使其能够适配一个或多个协议(protocol
)。当这种情况发生时,协议的名字应该完全按照类或结构体的名字的方式进行书写:
extension SomeType: SomeProtocol, AnotherProctocol {
// 协议实现写到这里
}
按照这种方式添加的协议遵循者(protocol conformance)被称之为
计算型属性(Computed Properties)
除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。
扩展可以向已有类型添加计算型实例属性和计算型类型属性。下面的例子向 Swift
的内建Double
类型添加了5个计算型实例属性,从而提供与距离单位协作的基本支持。
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 initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
println("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 输出 "square.origin is now at (10.0, 10.0)”
这个例子定义了 3 个几何形状的结构体:
- Point封装了一个(x, y)的坐标
- Size封装了一个width和height
- Rect表示一个有原点和尺寸的矩形
Rect也提供了一个名为center的计算属性。一个矩形的中心点可以从原点和尺寸来算出,所以不需要将它以显式声明的Point来保存。Rect的计算属性center提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。
例子中接下来创建了一个名为square的Rect实例,初始值原点是(0, 0),宽度高度都是10。如图所示蓝色正方形。
square的center属性可以通过点运算符(square.center)来访问,这会调用 getter 来获取属性的值。跟直接返回已经存在的值不同,getter 实际上通过计算然后返回一个新的Point来表示square的中心点。如代码所示,它正确返回了中心点(5, 5)。
center属性之后被设置了一个新的值(15, 15),表示向右上方移动正方形到如图所示橙色正方形的位置。设置属性center的值会调用 setter 来修改属性origin的x和y的值,从而实现移动正方形到新的位置。
extension Double {
var km: Double { return self * 1_000.0 }
var m : Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
println("One inch is \(oneInch) meters")
// 打印输出:"One inch is 0.0254 meters"
let threeFeet = 3.ft
println("Three feet is \(threeFeet) meters")
// 打印输出:"Three feet is 0.914399970739201 meters"
存储型属性
简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量,存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。
可以在定义存储属性的时候指定默认值,也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值
下面的例子定义了一个名为FixedLengthRange
的结构体,它描述了一个在创建后无法修改值域宽度的区间:
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间表示整数0,1,2
rangeOfThreeItems.firstValue = 6
// 该区间现在表示整数6,7,8
FixedLengthRange
的实例包含一个名为firstValue
的变量存储属性和一个名为length
的常量存储属性。在上面的例子中,length
在创建实例的时候被赋值,因为它是一个常量存储属性,所以之后无法修改它的值。
常量和存储属性
如果创建了一个结构体的实例并赋值给一个常量,则无法修改实例的任何属性,即使定义了变量存储属性:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 该区间表示整数0,1,2,3
rangeOfFourItems.firstValue = 6
// 尽管 firstValue 是个变量属性,这里还是会报错
因为rangeOfFourItems
声明成了常量(用let
关键字),即使firstValue
是一个变量属性,也无法再修改它了。
这种行为是由于结构体(struct
)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。
属于引用类型的类(class)则不一样,把一个引用类型的实例赋给一个常量后,仍然可以修改实例的变量属性。
构造器(Initializers)
扩展可以向已有类型添加新的构造器。这可以让你扩展其它类型,将你自己的定制类型作为构造器参数,或者提供该类型的原始实现中没有包含的额外初始化选项。
扩展能向类中添加新的便利构造器,但是它们不能向类中添加新的指定构造器或析构函数。指定构造器和析构函数必须总是由原始的类实现来提供。
注意:
如果你使用扩展向一个值类型添加一个构造器,在该值类型已经向所有的存储属性提供默认值,而且没有定义任何定制构造器(custom initializers)时,你可以在值类型的扩展构造器中调用默认构造器(default initializers)和逐一成员构造器(memberwise initializers)。
正如在值类型的构造器代理中描述的,如果你已经把构造器写成值类型原始实现的一部分,上述规则不再适用。
下面的例子定义了一个用于描述几何矩形的定制结构体Rect。这个例子同时定义了两个辅助结构体Size和Point,它们都把0.0作为所有属性的默认值:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}
因为结构体Rect
提供了其所有属性的默认值,所以正如默认构造器中描述的,它可以自动接受一个默认的构造器和一个成员级构造器。这些构造器可以用于构造新的Rect
实例:
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
你可以提供一个额外的使用特殊中心点和大小的构造器来扩展Rect
结构体:
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
这个新的构造器首先根据提供的center
和size
值计算一个合适的原点。然后调用该结构体自动的成员构造器init(origin:size:)
,该构造器将新的原点和大小存到了合适的属性中:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect的原点是 (2.5, 2.5),大小是 (3.0, 3.0)
注意:
如果你使用扩展提供了一个新的构造器,你依旧有责任保证构造过程能够让所有实例完全初始化。
在 Swift 扩展里添加"存储属性"
那么是如何在 Swift 的 Extension 扩展 里添加所谓的"存储属性"呢?
我们都知道,在 Swift 的
Extension
里是不能添加存储属性的,这里可以类比Objective-C
的Category
分类,分类是不能添加实例变量和属性的。
疑问
这里就有个问题了,为什么不能添加呢?
因为不管是
Swift
的Extension
还是Objective-C
的Category
都不能改变原有的类或者结构体的内存结构,在实例化这些类的时候,内存结构是确定的,而添加属性或者实例变量需要内存空间,会改变原有的内存结构。
利用关联对象
在
Objective-C
中我们常常用运行时Associated Object
关联对象 来给Category
添加属性,而在Swift
里,我们同样可以利用关联对象在Extension
中添加计算属性,以达到所谓的存储属性的效果。
struct AssociatedKeys {
static var testNameKey: String = "testNameKey"
}
extension UIView {
public var testName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.testNameKey) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.testNameKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
&AssociatedKeys.testNameKey: &
操作符是取出地址作为 UnsafeRawPointer
参数传入。
.OBJC_ASSOCIATION_RETAIN_NONATOMIC:
是一个 objc_AssociationPolicy
枚举,它有以下几种选择(从字面意思可以猜测是与Objective-C
中的属性修饰符相关):
public enum objc_AssociationPolicy : UInt {
case OBJC_ASSOCIATION_ASSIGN
case OBJC_ASSOCIATION_RETAIN_NONATOMIC
case OBJC_ASSOCIATION_COPY_NONATOMIC
case OBJC_ASSOCIATION_RETAIN
case OBJC_ASSOCIATION_COPY
}
我们可以测试一下:
var testString = "test"
let view = UIView()
view.testName = testString
print(view.testName) // 输出 Optional("test")
testString.append("change")
print(view.testName) // 输出 Optional("test")
view.testName = "testChange"
print(view.testName) // 输出 Optional("testChange")