在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 时,多花三秒钟想想:这个属性该让谁知道?该让谁修改?这个三秒钟的思考,就是你给项目未来架构留下的最宝贵的遗产。