iOS 專案中常將 JSON 直接映射為 Model 物件。若後端回傳欄位為 null 或缺失,對應的 NSString 屬性會保持 nil。後續直接使用這些屬性(如 [label setText:model.name])雖不會崩潰,但可能導致 UI 顯示異常或邏輯判斷錯誤。需求:遍歷物件所有 NSString 型別屬性,若為 nil 則自動設為 @"",且不需預知屬性名稱與數量。
// GMCommonHelper.h #import#import @interface GMCommonHelper : NSObject - (void)fixNilStringProperties:(id)object; @end // GMCommonHelper.m #import "GMCommonHelper.h" @implementation GMCommonHelper - (void)fixNilStringProperties:(id)object { if (!object) return; unsigned int count = 0; objc_property_t* properties = class_copyPropertyList([object class], &count); for (unsigned int i = 0; i < count; ++i) { objc_property_t prop = properties[i]; const char* name = property_getName(prop); const char* attrs = property_getAttributes(prop); // 屬性型別編碼:@"NSString" 開頭為物件類型 if (attrs && attrs[0] == 'T' && attrs[1] == '@' && strstr(attrs, "NSString")) { NSString* key = [NSString stringWithUTF8String:name]; id value = [object valueForKey:key]; if (value == nil) { [object setValue:@"" forKey:key]; } } } free(properties); } @end
property_getAttributes 回傳字串格式如 T@"NSString",C,N,V_name:
T:型別編碼前綴@"NSString":Objective-C 物件類型(Class 名稱在雙引號內)C:複製語意(copy)N:非原子性V_name:實例變數名稱判斷是否為 NSString:strstr(attrs, "NSString") 即可,簡單可靠。
若屬性為 readonly,setValue:forKey: 會拋出異常。屬性編碼中包含 ,R, 表示唯讀,可加入檢查:
if (strstr(attrs, ",R,")) continue; // 跳過唯讀屬性
若屬性為自訂 Model 類別,需遞歸修正:
if (value && ![value isKindOfClass:[NSString class]] && ![value isKindOfClass:[NSNumber class]] && ![value isKindOfClass:[NSArray class]] && ![value isKindOfClass:[NSDictionary class]]) {
[self fixNilStringProperties:value]; // 遞歸處理子物件
}
// NSObject+FixNil.h #import@interface NSObject (FixNil) - (void)fixNilStringsRecursively; @end // NSObject+FixNil.m #import "NSObject+FixNil.h" #import @implementation NSObject (FixNil) - (void)fixNilStringsRecursively { static NSMutableSet* visited = nil; if (!visited) visited = [NSMutableSet new]; if ([visited containsObject:self]) return; // 防循環引用 [visited addObject:self]; unsigned int count = 0; objc_property_t* props = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i < count; ++i) { objc_property_t prop = props[i]; const char* attrs = property_getAttributes(prop); if (!attrs) continue; if (strstr(attrs, ",R,")) continue; // 唯讀跳過 const char* name = property_getName(prop); NSString* key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; // NSString 且為 nil -> 設為空字串 if (value == nil && strstr(attrs, "NSString")) { [self setValue:@"" forKey:key]; } // 自訂物件遞歸 else if (value && ![value isKindOfClass:[NSString class]] && ![value isKindOfClass:[NSNumber class]] && ![value isKindOfClass:[NSArray class]] && ![value isKindOfClass:[NSDictionary class]] && ![value isKindOfClass:[NSNull class]]) { [value fixNilStringsRecursively]; } } free(props); [visited removeObject:self]; } @end
UserModel* user = [UserModel modelWithJSON:jsonDict]; [user fixNilStringsRecursively]; // 現在 user.name、user.nickname 等 NSString 屬性保證不為 nil self.nameLabel.text = user.name; // 安全
NSMutableSet 記錄已訪問物件防止無限遞歸。NSObject 且屬性標記 @objc 才能被 Runtime 發現。class_copyPropertyList 僅返回公開屬性,私有 ivar 需用 class_copyIvarList。利用 Objective-C Runtime 的反射能力,我們實現了通用的 nil 字串自動修正工具。核心在於 property_getAttributes 型別判斷與 setValue:forKey: 動態賦值。配合分類擴充,可零侵入式接入現有專案,大幅減少防衛性程式碼,提升程式碼健壯性。