在Objective-C的日常开发中,属性修饰符是一个看似简单却极易被忽视的设计要素。很多开发者习惯了随手写上 @property (nonatomic, strong) Type *name; 然后继续往下写逻辑,很少停下来想一想:这个属性到底应该让谁看见、让谁修改?而这种随手而为的习惯,往往在团队扩大、代码膨胀、模块耦合加深之后,变成架构演进路上最难清理的技术债务。
这篇文章会从实际开发视角出发,把属性修饰符讲透——不是罗列语法条目,而是说清楚它为什么重要、怎么用、用了之后整个项目的骨架会有什么变化。
一、为什么属性修饰符不只是"语法糖"
1.1 一个容易踩坑的真实场景
假设你的项目里有这样一个场景:UserManager 负责管理用户会话状态,其中有一个 token 属性存放鉴权令牌。早期开发时你随手写成了默认修饰符:
// 技术栈:Objective-C
// UserSessionManager.h - 早期写法(存在隐患)
@interface UserSessionManager : NSObject
// 默认是public,任何地方都可以直接赋值和读取
@property (nonatomic, copy) NSString *token;
@property (nonatomic, assign) BOOL isLoggedIn;
@end
// 技术栈:Objective-C
// UserSessionManager.m - 实现
@implementation UserSessionManager
+ (instancetype)sharedInstance {
static UserSessionManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[UserSessionManager alloc] init];
instance.isLoggedIn = NO;
instance.token = nil;
});
return instance;
}
- (void)refreshTokenWithCompletion:(void(^)(NSString *newToken, NSError *error))completion {
// 发起网络请求刷新token
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
// 模拟成功获取新token
[self setToken:@"eyJhbGciOiJIUzI1NiJ9.new_token_value"];
self.isLoggedIn = YES;
if (completion) {
completion(@"eyJhbGciOiJIUzI1NiJ9.new_token_value", nil);
}
});
}
@end
问题出在哪里?因为 token 和 isLoggedIn 都是默认的 public,项目里任何一个文件都能直接修改它们:
// 技术栈:Objective-C
// 某个随意的网络请求类中 - 糟糕的用法
@implementation OrderService
- (void)submitOrder:(NSDictionary *)orderData {
// 直接修改全局会话状态,完全绕过了UserSessionManager的逻辑
// 如果token过期了,这里不会触发刷新,只会用一个过期的token去发请求
UserSessionManager *session = [UserSessionManager sharedInstance];
session.token = orderData[@"token"]; // 直接赋值,没有任何校验
session.isLoggedIn = YES; // 强行标记为已登录
NSLog(@"订单提交中...");
}
@end
这种"到处直接改属性"的写法,让 UserSessionManager 原本应该承担的状态管理职责完全被架空了。每个模块都在按自己的理解操作 token 和 isLoggedIn,最终出现了"明明还没登录,系统却认为已登录"、"token过期了但没人刷新"这类难以排查的Bug。
1.2 修饰符的真正价值
回过头来看,问题并不出在"用了public"这个语法上,而是该private的被设成了public,该readonly的被设成了readwrite。属性修饰符的真正价值,不在于限制编译器,而在于把架构意图编码到代码里——让每一个使用这个属性的人,在写代码的那一刻就能知道"我能不能碰这个东西"。
下面我们把Objective-C中所有与属性访问控制相关的修饰符系统梳理一遍。
二、四种访问控制修饰符详解
2.1 public(默认修饰符)
public 是属性的默认访问级别,如果不写任何修饰符,属性就是 public。这意味着项目中任何类、任何文件,都可以直接读取和修改这个属性。
// 技术栈:Objective-C
// public修饰符的典型使用场景——面向外部的数据模型
@interface ProductInfo : NSObject
// 以下属性默认就是public(不写public关键字也一样)
@property (nonatomic, copy) NSString *productName; // 任何地方可读可写
@property (nonatomic, copy) NSString *productImageURL; // 任何地方可读可写
@property (nonatomic, assign) CGFloat price; // 任何地方可读可写
@property (nonatomic, assign) NSInteger stock; // 任何地方可读可写
// 显式写public是为了意图清晰
@property (nonatomic, copy, public) NSString *skuCode; // 显式标注,方便阅读
@end
适用场景:数据模型的字段、对外暴露的配置项、不需要额外逻辑控制的简单状态。
风险:一旦设为public,就等于把修改权交给了所有人。在多人协作的大项目中,public属性的修改频率往往远超预期,而且很难追溯"是谁在什么时候改的"。
2.2 private(私有修饰符)
private 将属性的访问范围严格限制在当前类内部。其他类、其他文件都无法访问这个属性,编译器会直接报错。
// 技术栈:Objective-C
// private修饰符——封装内部实现细节
@interface NetworkClient : NSObject
// 对外暴露的接口
- (void)requestWithURL:(NSURL *)url
method:(NSString *)method
body:(NSDictionary *)body
completion:(void (^)(NSData *data, NSError *error))completion;
// 对外暴露的只读配置
@property (nonatomic, assign, readonly) NSInteger requestCount;
// 内部实现细节 - 全部private
@property (nonatomic, strong, private) NSURLSession *session;
@property (nonatomic, strong, private) NSMutableDictionary *headerDefaults;
@property (nonatomic, strong, private) NSCache *responseCache;
@property (nonatomic, assign, private) NSTimeInterval timeout;
@property (nonatomic, assign, private) NSInteger retryCount;
@property (nonatomic, strong, private) dispatch_queue_t operationQueue;
@end
@implementation NetworkClient
- (instancetype)init {
self = [super init];
if (self) {
// 初始化内部依赖
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
self.headerDefaults = [@{@"Content-Type": @"application/json"} mutableCopy];
self.responseCache = [NSCache new];
self.timeout = 30.0;
self.retryCount = 3;
self.operationQueue = dispatch_queue_create("com.myapp.network.operation", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)requestWithURL:(NSURL *)url
method:(NSString *)method
body:(NSDictionary *)body
completion:(void (^)(NSData *data, NSError *error))completion {
self.requestCount += 1; // 内部递增计数
// 构建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = method;
// 合并默认头信息
[request setAllHTTPHeaderFields:self.headerDefaults];
// 如果有请求体
if (body) {
NSError *jsonError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:body
options:0
error:&jsonError];
if (!jsonError) {
request.HTTPBody = jsonData;
}
}
// 检查缓存
NSString *cacheKey = [url absoluteString];
NSData *cachedData = [self.responseCache objectForKey:cacheKey];
if (cachedData) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(cachedData, nil);
});
return;
}
// 发起请求
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && data) {
// 存入缓存
[weakSelf.responseCache setObject:data forKey:cacheKey];
}
dispatch_async(dispatch_get_main_queue(), ^{
completion(data, error);
});
}];
[task resume];
}
@end
适用场景:内部状态管理、缓存数据、内部队列、辅助数据结构——任何不属于类对外API的部分。
优势:强迫使用者通过公开方法来操作数据,你可以在方法中添加校验、日志、状态转换逻辑。
2.3 protected(受保护修饰符)
protected 允许当前类及其子类访问属性,但其他类不能直接访问。这是**基类向子类"开放能力"**的关键修饰符。
// 技术栈:Objective-C
// protected修饰符——为继承体系设计扩展点
// 基类:提供通用的视图容器能力
@interface BaseViewController : UIViewController
// 子类需要访问的内部状态
@property (nonatomic, strong, protected) NSMutableArray *childViewControllers;
@property (nonatomic, assign, protected) BOOL isTransitioning;
@property (nonatomic, assign, protected) CGFloat currentPageIndex;
// 子类可以覆写的配置点
@property (nonatomic, assign, protected) NSInteger maxVisiblePages;
@property (nonatomic, assign, protected) NSTimeInterval transitionDuration;
// 对外公开的能力
@property (nonatomic, strong, readonly) id currentChildVC;
- (void)showPage:(NSInteger)index animated:(BOOL)animated;
- (void)transitionTo:(UIViewController *)viewController;
@end
@implementation BaseViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.childViewControllers = [NSMutableArray array];
self.maxVisiblePages = 3;
self.transitionDuration = 0.3;
}
- (void)showPage:(NSInteger)index animated:(BOOL)animated {
if (index < 0 || index >= self.childViewControllers.count) return;
self.isTransitioning = YES;
self.currentPageIndex = index;
// 执行页面切换逻辑
[UIView animateWithDuration:self.transitionDuration
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
// 动画逻辑
}
completion:^(BOOL finished) {
self.isTransitioning = NO;
}];
}
@end
// 技术栈:Objective-C
// 子类:利用protected属性进行扩展
@interface HomeViewController : BaseViewController
// 子类新增的public属性
@property (nonatomic, strong) id bannerController;
// 子类新增的private属性
@property (nonatomic, strong, private) NSTimer *refreshTimer;
@end
@implementation HomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 子类可以自由使用protected属性
[self.childViewControllers addObjectsFromArray:@[
[[UIViewController alloc] init], // 首页
[[UIViewController alloc] init], // 消息
[[UIViewController alloc] init], // 我的
[[UIViewController alloc] init], // 发现
]];
// 子类可以调整基类的protected配置
self.maxVisiblePages = 4; // 首页有4个tab,比默认多一个
self.transitionDuration = 0.25; // 缩短切换动画时间
self.currentPageIndex = 0; // 初始显示第一个页面
// 子类的private属性,基类和其他子类都看不到
self.refreshTimer = [NSTimer scheduledTimerWithTimeInterval:60
target:self
selector:@selector(checkRefresh)
userInfo:nil
repeats:YES];
}
- (void)checkRefresh {
// 只有HomeViewController自己能看到refreshTimer
NSLog(@"检查首页刷新...");
}
@end
适用场景:框架基类、模板方法模式、需要子类扩展的组件。
核心作用:在"完全公开"和"完全私有"之间找到了平衡——既保护了类的外部可见性,又为继承体系留出了合理的扩展空间。
2.4 package(包级修饰符)
package 修饰符意味着属性在同一个"包"(编译单元/模块)内可见,跨包不可见。这个修饰符在实际项目中相对少用,但在Pod/Carthage/SPM等模块化项目中非常有价值。
// 技术栈:Objective-C
// package修饰符——模块内部共享
// 在同一个Pod或静态库模块内
// 文件A:DatabaseConfig.h
@interface DatabaseConfig : NSObject
// 模块内部共享——同一个Pod中的其他类可以访问
@property (nonatomic, copy, package) NSString *databasePath;
@property (nonatomic, assign, package) NSInteger maxConnections;
@property (nonatomic, assign, package) NSTimeInterval connectionTimeout;
// 对外暴露的唯一接口
+ (instancetype)sharedConfig;
- (void)configureWithPath:(NSString *)path;
@end
// 技术栈:Objective-C
// 同一模块内的另一个文件:DatabaseConnection.h
@interface DatabaseConnection : NSObject
// 同模块内可以访问DatabaseConfig的package属性
@property (nonatomic, assign, package) NSInteger connectionId;
@end
@implementation DatabaseConnection
- (instancetype)init {
self = [super init];
if (self) {
// 访问同一模块的package属性
DatabaseConfig *config = [DatabaseConfig sharedConfig];
NSLog(@"数据库路径: %@", config.databasePath);
NSLog(@"最大连接数: %ld", (long)config.maxConnections);
// 本类的package属性
self.connectionId = [[NSUUID UUID] hash];
}
return self;
}
@end
注意:package 修饰符的实际支持取决于编译环境和构建工具。在CocoaPods中,同一个Pod内的文件默认共享编译上下文,可以用 #import 内部的头文件实现类似效果。
三、readonly与setter/getter的配合使用
3.1 只读属性 + 受控修改模式
只读属性(readonly)是修饰符体系中最实用的"组合技"。通过将属性声明为 readonly,你阻止了外部直接赋值,然后提供一个或多个方法来控制如何修改属性。
// 技术栈:Objective-C
// readonly + 受控setter 的经典模式
@interface UserAccount : NSObject
// 对外只读——外部只能看,不能直接改
@property (nonatomic, readonly, public) NSString *displayName;
@property (nonatomic, readonly, public) NSInteger accountBalance;
@property (nonatomic, readonly, public) NSDate *createdAt;
@property (nonatomic, readonly, public) BOOL isActive;
// 修改必须通过受控方法
- (BOOL)updateDisplayName:(NSString *)newName error:(NSError **)error;
- (BOOL)deposit:(CGFloat)amount error:(NSError **)error;
- (BOOL)withdraw:(CGFloat)amount error:(NSError **)error;
- (void)deactivate;
- (void)reactivate;
@end
@implementation UserAccount {
// 用实例变量存储实际数据,因为readonly没有自动合成的ivar
NSString *_displayName;
NSInteger _accountBalance;
NSDate *_createdAt;
BOOL _isActive;
}
- (instancetype)initWithId:(NSString *)userId {
self = [super init];
if (self) {
_displayName = @"默认用户";
_accountBalance = 0;
_createdAt = [NSDate date];
_isActive = YES;
}
return self;
}
// 自定义getter - 可以加入缓存、格式化等逻辑
- (NSString *)displayName {
return [_displayName copy]; // 防止外部修改内部字符串(如果是mutableString)
}
// 受控修改方法 - 包含完整的校验逻辑
- (BOOL)updateDisplayName:(NSString *)newName error:(NSError **)error {
// 校验1:空值检查
if (newName.length == 0) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:1001
userInfo:@{NSLocalizedDescriptionKey: @"显示名称不能为空"}];
}
return NO;
}
// 校验2:长度限制
if (newName.length > 20) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:1002
userInfo:@{NSLocalizedDescriptionKey: @"显示名称不能超过20个字符"}];
}
return NO;
}
// 校验3:敏感词过滤
NSArray *sensitiveWords = @[@"违规词1", @"违规词2"];
for (NSString *word in sensitiveWords) {
if ([newName containsString:word]) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:1003
userInfo:@{NSLocalizedDescriptionKey: @"名称包含不合规内容"}];
}
return NO;
}
}
// 校验通过,执行修改
_displayName = [newName copy];
// 记录修改日志
NSLog(@"[UserAccount] 显示名称已更新: %@ -> %@", [_displayName description], newName);
return YES;
}
- (BOOL)deposit:(CGFloat)amount error:(NSError **)error {
if (amount <= 0) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:2001
userInfo:@{NSLocalizedDescriptionKey: @"存款金额必须大于0"}];
}
return NO;
}
if (!_isActive) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:2002
userInfo:@{NSLocalizedDescriptionKey: @"账户已停用,无法存款"}];
}
return NO;
}
_accountBalance += (NSInteger)amount;
return YES;
}
- (BOOL)withdraw:(CGFloat)amount error:(NSError **)error {
if (amount <= 0) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:3001
userInfo:@{NSLocalizedDescriptionKey: @"取款金额必须大于0"}];
}
return NO;
}
if (amount > _accountBalance) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:3002
userInfo:@{NSLocalizedDescriptionKey: @"余额不足"}];
}
return NO;
}
if (!_isActive) {
if (error) {
*error = [NSError errorWithDomain:@"UserAccountError"
code:3003
userInfo:@{NSLocalizedDescriptionKey: @"账户已停用,无法取款"}];
}
return NO;
}
_accountBalance -= (NSInteger)amount;
return YES;
}
- (void)deactivate {
_isActive = NO;
NSLog(@"[UserAccount] 账户已停用");
}
- (void)reactivate {
_isActive = YES;
NSLog(@"[UserAccount] 账户已激活");
}
@end
3.2 readonly声明 + readwrite重声明的技巧
在Objective-C中有一个非常有用的模式:在 .h 文件中声明为 readonly,在 .m 文件的类扩展中重新声明为 readwrite。这样外部类只能读,类内部可以自由读写。
// 技术栈:Objective-C
// readonly(对外) + readwrite(对内) 的经典模式
// Counter.h - 对外头文件
@interface Counter : NSObject
// 对外只读
@property (nonatomic, readonly) NSInteger count;
@property (nonatomic, readonly) NSDate *lastModified;
// 对外方法
- (void)increment;
- (void)decrement;
- (void)reset;
@end
// 技术栈:Objective-C
// Counter.m - 实现文件
#import "Counter.h"
@implementation Counter
// 类扩展中重新声明为readwrite——仅.m文件内部可见
@interface Counter ()
@property (nonatomic, readwrite) NSInteger count;
@property (nonatomic, readwrite) NSDate *lastModified;
@property (nonatomic, assign) NSInteger minCount; // 完全private的约束
@end
- (instancetype)init {
self = [super init];
if (self) {
_count = 0;
_lastModified = [NSDate date];
_minCount = 0;
}
return self;
}
- (void)increment {
_count += 1;
_lastModified = [NSDate date];
NSLog(@"计数器增加: %ld", (long)self.count);
}
- (void)decrement {
if (_count > _minCount) {
_count -= 1;
_lastModified = [NSDate date];
NSLog(@"计数器减少: %ld", (long)self.count);
} else {
NSLog(@"计数器已达最小值(%ld),无法减少", (long)self.minCount);
}
}
- (void)reset {
_count = 0;
_lastModified = [NSDate date];
NSLog(@"计数器已重置");
}
@end
这种模式的精妙之处在于:对外隐藏了修改能力,对内保留了完整控制权,且不需要手写getter和setter。当外部代码试图执行 counter.count = 5; 时,编译器会直接报错——这是比任何文档说明都更可靠的"使用指南"。
四、修饰符对架构的深层影响
4.1 分层架构中的修饰符分配策略
在MVC或MVVM架构中,每一层的职责不同,修饰符的选择也应当体现这种职责划分。
// 技术栈:Objective-C
// MVVM架构中各层的修饰符分配策略
// ============ Model层 ============
// Model的职责是承载数据,字段通常是public或readonly
@interface OrderModel : NSObject
@property (nonatomic, copy) NSString *orderId; // public - 各处需要读写
@property (nonatomic, copy) NSString *productName; // public - 各处需要读写
@property (nonatomic, assign) NSInteger quantity; // public - 各处需要读写
@property (nonatomic, assign) CGFloat amount; // public - 各处需要读写
@property (nonatomic, readonly) NSDate *createdAt; // 只读 - 创建时间不应被修改
@property (nonatomic, readonly) NSString *statusText; // 只读 - 由status计算而来
@end
// ============ ViewModel层 ============
// ViewModel负责状态管理和数据转换
@interface OrderViewModel : NSObject
@property (nonatomic, strong, readonly) OrderModel *currentOrder; // 对外只读
@property (nonatomic, assign, readonly) BOOL isLoading; // 对外只读
@property (nonatomic, copy, readonly) NSString *errorDisplayText; // 对外只读
@property (nonatomic, assign, readonly) NSInteger totalAmount; // 对外只读
// 内部状态 - private
@property (nonatomic, strong, private) NSMutableArray *orderList;
@property (nonatomic, assign, private) NSInteger currentPage;
@property (nonatomic, strong, private) id networkTask;
// 对外方法入口
- (void)loadOrders;
- (void)refreshOrder:(NSString *)orderId;
- (void)deleteOrder:(NSString *)orderId;
@end
// ============ View层 ============
// View负责展示,通常暴露配置属性和代理
@interface OrderView : UIView
@property (nonatomic, strong) UIColor *themeColor; // 外部配置样式
@property (nonatomic, assign) CGFloat cardCornerRadius; // 外部配置样式
@property (nonatomic, copy) void(^onItemTap)(OrderModel *model); // 外部设置回调
// 内部组件 - private
@property (nonatomic, strong, private) UIScrollView *scrollView;
@property (nonatomic, strong, private) NSMutableArray *cellViews;
@property (nonatomic, strong, private) UIActivityIndicatorView *loadingIndicator;
@property (nonatomic, assign, private) BOOL isScrolling;
@end
4.2 修饰符对依赖注入模式的影响
依赖注入(DI)是现代iOS架构中的重要模式。修饰符的使用直接影响了依赖注入的可行性和安全性。
// 技术栈:Objective-C
// 依赖注入 + 修饰符
// 协议定义 - 清晰定义依赖接口
@protocol UserDataProvider <NSObject>
- (void)fetchUserWithId:(NSString *)userId
completion:(void(^)(NSDictionary *userData, NSError *error))completion;
@end
@protocol AnalyticsProvider <NSObject>
- (void)trackEvent:(NSString *)eventName properties:(NSDictionary *)properties;
@end
// 核心服务类
@interface UserService : NSObject
// 依赖项 - private但可通过init注入
@property (nonatomic, strong, private) id<UserDataProvider> dataProvider;
@property (nonatomic, strong, private) id<AnalyticsProvider> analyticsProvider;
// 对外只读的状态
@property (nonatomic, readonly) NSString *currentUserId;
@property (nonatomic, readonly) BOOL hasDataLoaded;
// 依赖注入的初始化方法
- (instancetype)initWithDataProvider:(id<UserDataProvider>)dataProvider
analyticsProvider:(id<AnalyticsProvider>)analyticsProvider;
// 业务方法
- (void)loadCurrentUserInfo;
- (void)updateProfile:(NSDictionary *)profileData;
@end
@implementation UserService
- (instancetype)initWithDataProvider:(id<UserDataProvider>)dataProvider
analyticsProvider:(id<AnalyticsProvider>)analyticsProvider {
self = [super init];
if (self) {
// 依赖注入 - 构造函数模式
_dataProvider = dataProvider;
_analyticsProvider = analyticsProvider;
}
return self;
}
- (void)loadCurrentUserInfo {
[self.analyticsProvider trackEvent:@"user_info_loaded"
properties:@{@"source": @"manual"}];
[self.dataProvider fetchUserWithId:self.currentUserId
completion:^(NSDictionary *userData, NSError *error) {
if (!error && userData) {
self.hasDataLoaded = YES;
[self.analyticsProvider trackEvent:@"user_data_received"
properties:@{@"fields": @(userData.count)}];
}
}];
}
- (void)updateProfile:(NSDictionary *)profileData {
[self.analyticsProvider trackEvent:@"profile_update_attempt"
properties:profileData];
}
@end
// 技术栈:Objective-C
// 依赖注入的使用方 - 组装模块
@interface ModuleContainer : NSObject
// 集中管理所有依赖实例
@property (nonatomic, strong) id<UserDataProvider> userDataProvider;
@property (nonatomic, strong) id<AnalyticsProvider> analyticsProvider;
@property (nonatomic, strong) UserService *userService;
+ (instancetype)sharedContainer;
- (void)setupDependencies;
@end
@implementation ModuleContainer
+ (instancetype)sharedContainer {
static ModuleContainer *container = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
container = [[ModuleContainer alloc] init];
[container setupDependencies];
});
return container;
}
- (void)setupDependencies {
// 所有依赖都在一个地方组装,清晰的初始化流程
self.userDataProvider = [[NetworkUserDataProvider alloc] init];
self.analyticsProvider = [[AnalyticsImpl alloc] init];
self.userService = [[UserService alloc] initWithDataProvider:self.userDataProvider
analyticsProvider:self.analyticsProvider];
}
@end
4.3 单例模式下修饰符的最佳实践
单例是iOS开发中使用频率最高的模式之一,也是最容易滥用修饰符的地方。如果单例的所有属性都是public的,那单例就变成了一个"全局变量容器",完全失去了封装意义。
// 技术栈:Objective-C
// 单例模式 - 修饰符的正确应用
@interface AppEnvironment : NSObject
// ============ 对外只读的公开配置 ============
@property (nonatomic, readonly) BOOL isProduction;
@property (nonatomic, readonly) NSString *appVersion;
@property (nonatomic, readonly) NSInteger buildNumber;
@property (nonatomic, readonly) NSString *currentEnvironmentName;
// ============ 需要修改的通过方法控制 ============
- (void)switchToEnvironment:(NSString *)envName;
- (void)resetAllCaches;
// ============ 完全内部的private属性 ============
@property (nonatomic, strong, private) NSMutableDictionary *cachedConfigs;
@property (nonatomic, strong, private) NSDate *lastConfigRefreshTime;
@property (nonatomic, assign, private) dispatch_once_t onceToken;
@property (nonatomic, strong, private) NSCache *persistentCache;
@end
@implementation AppEnvironment
+ (instancetype)sharedInstance {
static AppEnvironment *instance = nil;
dispatch_once(&instance->onceToken, ^{
instance = [[AppEnvironment alloc] initPrivate];
});
return instance;
}
// 使用private init防止外部直接创建实例
- (instancetype)initPrivate {
self = [super init];
if (self) {
_cachedConfigs = [NSMutableDictionary dictionary];
_persistentCache = [NSCache new];
_lastConfigRefreshTime = [NSDate distantPast];
_currentEnvironmentName = @"production";
_isProduction = YES;
_appVersion = @"1.0.0";
_buildNumber = 1;
}
return self;
}
// 防止通过copy或NSCoding创建新实例
- (instancetype)copyWithZone:(NSZone *)zone {
return self;
}
- (instancetype)mutableCopyWithZone:(NSZone *)zone {
return self;
}
// 受控的修改方法
- (void)switchToEnvironment:(NSString *)envName {
if ([envName isEqualToString:@"production"]) {
_currentEnvironmentName = @"production";
_isProduction = YES;
} else if ([envName isEqualToString:@"staging"]) {
_currentEnvironmentName = @"staging";
_isProduction = NO;
} else {
NSLog(@"[AppEnvironment] 无效的环境名称: %@", envName);
return;
}
_lastConfigRefreshTime = [NSDate date];
NSLog(@"[AppEnvironment] 环境已切换至: %@", _currentEnvironmentName);
}
- (void)resetAllCaches {
[_cachedConfigs removeAllObjects];
[_persistentCache removeAllObjects];
_lastConfigRefreshTime = [NSDate distantPast];
NSLog(@"[AppEnvironment] 所有缓存已重置");
}
@end
五、技术优缺点分析
5.1 修饰符体系的核心优势
第一,编译期反馈。 修饰符把架构约束转化成了编译错误。当开发者试图访问一个private属性时,Xcode会立刻给出红色报错,而不是等到运行时才发现数据被污染了。这比任何Code Review都更高效——编译器不会遗漏,不会疲劳。
第二,自文档化。 看到属性声明就一目了然:readonly 告诉你这个值不能直接改,private 告诉你这属于内部实现。新加入团队的开发者不需要翻阅架构文档,只需要看头文件就能理解模块的对外接口。
第三,安全隔离。 在多模块协作中,private属性天然地阻止了跨模块的随意耦合。模块A的内部状态不会意外暴露给模块B,每个模块都需要通过明确的接口来交互,这正是高内聚低耦合目标的代码级实现。
第四,重构友好。 当属性是private或readonly时,你可以放心地修改内部实现——改存储方式、加校验逻辑、改数据结构——只要公开接口不变,外部代码完全不受影响。反之,如果一个核心属性是public的,每次想改它的内部逻辑都要排查项目中所有引用它的地方。
5.2 需要正视的局限性
第一,运行时可绕过。 Objective-C的动态特性意味着修饰符并不是绝对的屏障。通过 setValue:forKey: 或 Runtime API,你可以在运行时访问任何属性,无论它是什么修饰符。这意味着修饰符更像是一份"君子协定",而非"法律强制"。在安全敏感的场景中,不能仅依赖修饰符来保护数据。
第二,package修饰符的实际支持有限。 标准的CocoaPods、Carthage等构建工具对 package 关键字的支持不如public和private那样完善。在大型项目中,你更多需要依赖目录结构和头文件组织来实现"包级"可见性,而不是直接依赖编译器关键字。
第三,继承链中的复杂性。 当类继承层次较深时,修饰符的交互会变得复杂。子类的protected属性与父类的private属性的可见性规则需要开发者非常清楚。一个常见的困惑是:private 在Objective-C中并不是C++意义上的"严格私有",它在运行时仍可通过实例变量直接访问。
第四,与Swift混编的摩擦。 当Objective-C和Swift代码混编时,修饰符的映射行为需要额外注意。Objective-C的默认public属性映射到Swift后可能是internal而不是public,这可能导致跨语言的可见性预期不一致。
六、开发中的注意事项
6.1 不要把所有属性都设为private
一个常见的过度设计倾向是把所有属性都设为private。虽然这看起来最大化了封装性,但在实际项目中会带来几个问题:
// 技术栈:Objective-C
// 过度private vs 合理设计
// ❌ 不推荐:所有属性都private,导致外部完全无法使用
@interface OverEncapsulatedService : NSObject
@property (nonatomic, strong, private) NSString *serviceName;
@property (nonatomic, assign, private) BOOL isEnabled;
@property (nonatomic, strong, private) id delegate;
// 没有对外暴露任何有用的东西...
@end
// ✅ 推荐:根据职责合理分配
@interface WellDesignedService : NSObject
// 外部需要知道的配置
@property (nonatomic, copy) NSString *serviceName;
@property (nonatomic, assign) BOOL isEnabled;
@property (nonatomic, weak) id<ServiceDelegate> delegate;
// 对外只读的状态(通过方法修改)
@property (nonatomic, readonly) NSInteger currentRequestCount;
@property (nonatomic, readonly) NSDate *lastRequestTime;
// 内部实现细节
@property (nonatomic, strong, private) NSURLSession *session;
@property (nonatomic, strong, private) NSMutableDictionary *pendingRequests;
// 业务方法
- (void)start;
- (void)stop;
- (void)sendRequest:(NSDictionary *)request;
@end
核心原则:对外暴露的信息要足够(不能什么都不给),但不要过度暴露(不要把内部细节全部公开)。readonly是最常用的平衡手段——暴露信息但不暴露修改权。
6.2 继承场景下的修饰符决策树
// 技术栈:Objective-C
// 继承体系中修饰符的选择指南
// 决策:一个属性应该用什么修饰符?
//
// 步骤1:外部(非子类、非基类)需要访问吗?
// 是 → public
// 否 → 步骤2
//
// 步骤2:子类需要访问或覆写吗?
// 是 → protected
// 否 → 步骤3
//
// 步骤3:只需要当前类内部访问吗?
// 是 → private
// 不确定 → 先设为private,需要时再提升
// 实际应用示例
@interface BaseNetworkRequest : NSObject
// 子类需要覆写的属性 → protected
@property (nonatomic, copy, protected) NSString *baseUrl;
@property (nonatomic, assign, protected) NSInteger retryCount;
@property (nonatomic, strong, protected) NSDictionary *defaultHeaders;
// 子类需要访问的状态 → protected
@property (nonatomic, assign, protected) BOOL isCancelled;
// 完全内部的属性 → private
@property (nonatomic, strong, private) NSURLSessionDataTask *currentTask;
@property (nonatomic, assign, private) NSTimeInterval startTime;
// 对外只读 → readonly
@property (nonatomic, readonly) NSTimeInterval duration;
@property (nonatomic, readonly) NSInteger statusCode;
// 对外方法 → public(默认)
- (void)cancel;
- (void)execute;
@end
@implementation BaseNetworkRequest
- (instancetype)init {
self = [super init];
if (self) {
_retryCount = 3;
_defaultHeaders = @{@"Content-Type": @"application/json"};
}
return self;
}
- (void)execute {
self.startTime = [[NSDate date] timeIntervalSince1970];
// ... 执行请求
}
- (void)cancel {
self.isCancelled = YES; // 子类可以通过protected属性协作取消
[self.currentTask cancel]; // 访问private属性
}
- (NSTimeInterval)duration {
return [[NSDate date] timeIntervalSince1970] - self.startTime;
}
@end
6.3 与Swift互操作的注意事项
在现代iOS项目中,Objective-C和Swift的混编是常态。理解修饰符在两种语言之间的映射行为,可以避免很多跨语言调用的陷阱。
// 技术栈:Objective-C / Swift 混编
// ObjC修饰符在Swift中的映射
// ObjC头文件
// MyFramework.h
@interface ObjCConfig : NSObject
@property (nonatomic, copy) NSString *apiKey; // Swift中: var (internal)
@property (nonatomic, assign) NSInteger maxRetries; // Swift中: var (internal)
@property (nonatomic, readonly) NSString *version; // Swift中: let (internal)
@end
// 技术栈:Swift
// Swift文件中如何使用ObjC的Config
// Swift中,ObjC的默认public属性变成internal
// 如果这个属性在public框架中使用,需要额外标记
// 方式1:直接使用(同模块内)
let config = ObjCConfig()
config.apiKey = "my-api-key" // 可以修改
config.maxRetries = 5 // 可以修改
let version = config.version // 只能读取
// 方式2:如果ObjC类标记为public @interface
// Swift中也可以看到public
// 但这需要ObjC头文件做特殊标注
// 方式3:使用Swift的访问控制包装ObjC类
public class SafeConfig {
private let internalConfig: ObjCConfig
// 只暴露只读的Swift属性
public let apiKey: String
public let maxRetries: Int
public init() {
self.internalConfig = ObjCConfig()
self.apiKey = internalConfig.apiKey ?? ""
self.maxRetries = Int(internalConfig.maxRetries)
}
// 提供受控的修改方法
public func updateApiKey(_ newKey: String) {
internalConfig.apiKey = newKey
}
}
关键注意事项:
- Objective-C中没有显式的
public修饰符对应Swift的public。如果你需要将ObjC类暴露给Swift框架使用,需要在头文件中用NS_SWIFT_NAME或确保头文件在模块的公开搜索路径中。 readonly在Swift中映射为let(值类型)或只读的var(引用类型),语义上有微妙差异。private在Swift中仍然是private,但Swift的private比ObjC的更严格——ObjC的private在运行时仍可访问。
七、文章总结
属性修饰符在Objective-C中不是可有可无的语法装饰,而是架构设计的基石之一。回顾全文,几个核心要点值得反复体会:
第一,修饰符的本质是"可见性契约"。 当你把一个属性设为private时,你实际上是在告诉整个代码库:"这个数据属于我,你不需要知道,也不应该知道。"这种契约在编译期执行,比口头约定和文档说明可靠得多。
第二,readonly是最被低估的修饰符组合。 它解决了一个日常开发中最常见的问题:我需要暴露数据但不能暴露修改权。通过readonly配合类扩展中的readwrite重声明,你既保留了对外查询的能力,又在内部保留了完整的控制权。这个模式应该成为每个iOS开发者的肌肉记忆。
第三,修饰符的选择应该跟职责匹配。 Model的数据字段偏向public或readonly;ViewModel的状态偏向readonly,通过方法暴露修改能力;Controller和View的内部组件是private;基类给子类留的扩展点是protected。这些不是死规则,而是一种经过实践检验的默认倾向。
第四,修饰符是防御性编程的第一道防线。 它防止了你忘记某个地方不应该改数据、防止了新同事无意间改乱了内部状态、防止了重构时遗漏了某个引用。把编译器变成你的代码审查员,远比事后修Bug成本低。
最终,好的架构不是靠宏大的设计文档构建的,而是靠每一行代码中的细微选择累积而成的。当你下次写下 @property 时,多花三秒钟想想:这个属性该让谁知道?该让谁修改?这个三秒钟的思考,就是你给项目未来架构留下的最宝贵的遗产。
评论
围绕“Objective-C中属性的访问控制修饰符使用及对项目架构的影响”参与讨论