OC-实现自定义类的可变与不可变
设计原则
- 禁止外部修改
- 所有属性设置为只读(readonly)
- 不提供修改内部状体的公共方法
- 防御性拷贝
- 在初始化赋值时,对传入进来的可变对象创建不可变副本
- 使用@private隐藏内部实例变量
注意事项
-
OC中类的可变性(Mutable)与不可变性(Immutable)是通过设计模式与命名约定来区分的,一个类是否可变取决于它的内部是否提供修改其内部状态的方法
-
核心区别
- 不可变类创建后内容不可更改;可变类提供方法动态修改内容
- 不可变类通常是基类(如NSString);可变类通常是不可改变类的子类(如NSMutableString)
- 可变类名前面加上Mutable
- 不可变类修改会创建新对象,可变类修改时直接更新原对象
举例:
NSString* str = @"我是"; str = [str stringByAppendingString:@"劳大"]; NSMutableString* str = [[NSMutableString alloc] initWithString:@"我是"]; [str appendString:@"劳大"];
设计一个不可变类
//Peron.h
@interface Person : NSObject
@property (nonatomic, copy, readonly)NSString* name;
@property (nonatomic, assign, readonly)NSInteger age;
- (insanceType)initWithName:(NSString*)name withAge: (NSInteger)age;
@end
-
解释
-
@property参数:
- Nonatomic:非原子性访问
- copy:对传入的字符串创建不可改变副本
- readonly:外部只读不写
-
initWithName: withAge:初始化方法,必须通过此方法设置属性值
-
//Peron.m
@implementation Person
- (instanceType)initWithName:(NSString*)name withAge:(NSInteger)age {
if (self = [super init]) {
_name = [name copy];
_age = age;
}
}
-
解释
-
_name = [name copy]:创建参数字符串的不可改变副本,防止外部修改传入的字符串引起对象内部的信息改变(防御性拷贝)
-
_age = age:基本类型,直赋值无需拷贝
-
为什么Person是不可改变的
- 属性只读,外部无法通过点语法或者setter方法修改属性
- Peron类内部并未提供修改Person类实例参数的方法,一旦实例创建,其信息不可修改
你也可以直接把类的setter与getter方法的code写在类的实现部分,这样也可以实现不可变类,这样允许类内部修改状态,但是对外界隐藏修改接口,从而保持类的不可变性。
//Person.h
@interface Person : NSObject {
@private NSString* _name;
@private NSinteger _age;
}
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
- (NSString *)name; // 仅暴露 getter
- (NSInteger)age; // 仅暴露 getter
@end
//Person.m
@implementation Person
- (instanceType)initWithName:(NSString*)name withAge:(NSInteger)age {
if (self = [super init]) {
_name = name;
_age = age;
}
return self;
}
- (NSString*)name {
return _name;
}
- (NSInteger)age {
return _age;
}
- (void)setName:(NSString *)name {
_name = [name copy];
}
- (void)setAge:(NSInteger)age {
_age = age;
}
//允许类的内部修改状态
- (void)celebrateBirthday {
[self setAge:self.age + 1];
}
@end
为Person类扩展可变子类
//MutablePeron.h
@interface MutablePerson : Person
@property (nonatomic, copy, readwrite) NSString *name;
@property (nonatomic, assign, readwrite) NSInteger age;
- (void)incrementAge;
@end
- 解释
- readwrite:覆盖父类的readonly,允许外部修改
- incrementAge:提供自定义的修改方法(子类需要自己的存取逻辑)
//MutablePer.m
@implementation MutablePerson
// 重写父类的 setter 方法
- (void)setName:(NSString *)name {
_name = [name copy];
}
- (void)setAge:(NSInteger)age {
_age = age;
}
- (void)incrementAge {
self.age++;
}
@end
- 解释
setName:
和setAge:
:
必须重写父类的属性实现,否则无法修改。incrementAge
:
提供一个不依赖属性的修改方法,直接操作_age
。
可以用下面的方法来检查是否有对应方法
// 检查 Person 类是否有 setter 方法
BOOL hasSetter = [Person instancesRespondToSelector:@selector(setName:)]; // NO
// 检查 MutablePerson 类是否有 setter 方法
BOOL hasSetterInMutable = [MutablePerson instancesRespondToSelector:@selector(setName:)]; // YES
子类覆盖父类属性的原理
-
子类在覆盖父类属性时,并不完全会重新声明一个新属性,而是复用父类的实例变量并调整存取方法的行为
-
当子类覆盖父类的属性时
- 子类不会创建新的实例变量(是储存对象状态的底存储单元,当子类继承父类时会自动继承父类的所有实例变量,属性是访问这些实例变量的接口)
- 继承父类的getter实现
总结一下
@property
会自动生成存取方法(getter/setter),但readonly
修饰的属性只会生成 getter 方法,不会生成 setter 方法。这是实现不可变类的关键机制之一- 适用readonly编译器只会生成getter方法而不会生成setter方法,所以子类在覆盖父类属性的时候需要新增setter方法
- readwrite覆盖readonly时,子类会继承父类的示例变量,同时子类会新增setter方法,getter方法可以继承父类或重写
- 自定义类的可变与不可变的核心区别在于是否允许外界修改其内部状态,而这一区别的实现方式主要体现在是否向外界暴露修改内部信息的接口(方法或属性)
类变量
特性
1. 全类共享,全局唯一
- 类变量的值被该类的所有实例共享,修改一处会影响所有实例。
- 类比:
一个班级的所有学生(实例)共享同一个班级编号(类变量),修改班级编号会影响所有学生。
2. 存储在类对象的内存中
- 类变量不存储在实例对象的内存中,而是存储在类对象(
Class
)或全局数据区。 - 生命周期:从程序启动到程序结束
实现
@interface MyClass : NSObject
+ (NSString *)className; // 类方法,访问类变量
+ (void)setClassName:(NSString *)name;
@end
@implementation MyClass
// 类变量:文件作用域的静态变量
static NSString *_className = @"Default";
+ (NSString *)className {
return _className;
}
+ (void)setClassName:(NSString *)name {
_className = name;
}
@end
// 使用示例
[MyClass setClassName:@"NewClass"];
NSLog(@"Class name: %@", [MyClass className]);
实例变量
- 属于每个实例
- 跟随实例对象存储在堆内存中
- 跟随着实例对象创建到销毁
- 通过实例方法访问
- 在类的花括号内声明
类对象存储实例变量的类型与名称
实例对象存储实例变量的值