如何在Objective-C分类中简洁初始化只读类属性?
我的代码场景
我给NSURL写了一个分类NSURLMyClass,包含一个只读类属性myString,代码如下:
头文件 NSURL+MyClass.h
@interface NSURL (NSURLMyClass) @property(class, readonly) NSString* myString; @end
实现文件 NSURL+MyClass.mm
@implementation MyObject static NSString* _myString; + (NSString*) myString { if (_myString == nil) { _myString = @"Hello"; } return _myString; } @end
我了解过其他讨论,大多是实例属性的处理——要么在实现里重新声明readwrite属性并合成,要么直接访问带下划线的成员变量,但这些都没涉及分类里的类属性。
我觉得这种只读类属性和现代C里的static constexpr成员变量类似,C里只需要在头文件一行就能初始化:
#include <string> using std::literals; struct MyClass { static constexpr auto myString = "Hello"s; }; // 访问方式:`std::println("{}", MyClass::myString);`
我的问题:
- 能不能像C++一样简洁初始化
NSURL.myString,不用声明带下划线的static变量、getter方法和空值判断? - 我要不要声明
+(instancetype)init方法来初始化myString?这样会不会覆盖NSURL原有的+init方法? - 有没有其他完全不同的实现方式,比如用子类代替分类?我对Objective-C语义不太熟,不确定Foundation返回的
NSURL*能不能直接转成MyClass*(MyClass是NSURL的子类)?
问题解答
1. 能不能像C++一样简洁初始化?
当然可以,有两种更简洁的写法:
- 直接返回常量:如果
myString是固定不变的字符串常量,完全可以省略静态变量和空值判断,直接在getter里返回常量,效果和C++的constexpr一致:
@implementation NSURL (NSURLMyClass) + (NSString*)myString { return @"Hello"; } @end
- 利用类扩展合成属性:如果需要支持后续修改(虽然你声明的是只读,但内部可以放开),可以在实现文件里给NSURL加类扩展,重新声明
myString为readwrite,让编译器自动合成静态存储变量和getter,再在+load里初始化:
// NSURL+MyClass.mm @interface NSURL () @property(class, readwrite) NSString* myString; @end @implementation NSURL (NSURLMyClass) + (void)load { self.myString = @"Hello"; } @end
+load会在类被加载时自动调用,不用手动触发,省去懒加载的判断逻辑。
2. 用+init初始化会不会覆盖原有方法?
首先纠正个误区:+init不是类的初始化方法,Objective-C里类的初始化逻辑应该放在+initialize里。但哪怕是+initialize,你也绝对不能在分类里实现——因为分类的方法会直接覆盖主类的同名方法,NSURL本身的+initialize可能包含Foundation内部的关键逻辑,你一覆盖就会触发各种奇怪的崩溃或异常。
另外,+init本身是用来创建实例的类方法,和类属性初始化完全无关,更不应该去碰它。
3. 用子类代替分类可行吗?
可行,但有致命限制:Foundation提供的绝大多数NSURL创建方法(比如URLWithString:)返回的都是NSURL本身(或它的私有子类)的实例,不是你自定义的MyClass实例。你要是强行把NSURL*转成MyClass*,调用子类属性或方法时会直接崩溃,因为这个实例的类型根本不是你的子类。
如果要用子类,你得自己实现所有NSURL的创建方法来返回MyClass实例,但这样成本极高,而且Foundation内部隐式创建NSURL的场景你根本覆盖不到。相比之下,分类能给所有NSURL实例(包括Foundation内部创建的)添加类属性,是更适合的方案。
内容的提问来源于stack exchange,提问作者SRSR333

