iOS的日常

《招聘一个靠谱的 iOS》-- 答案参考(上)

2017-05-18  本文已影响41人  Ro_bber
  1. 风格就错题
    1.1优化部分
    1.2硬伤部分
  2. 什么情况下使用weak关键字,相比assign有什么不同?
  3. 怎么用copy关键字?
  4. 这个写法会有什么问题:@property(copy) NSMutableArray *array;
  5. 如何让自己的类用copy修饰符?如何重写带copy关键字的setter
  6. @property 的本质是什么?ivargettersetter 是如何生成并添加到这个类中的?
  7. @protocolcategory 中如何使用 @property?
  8. runtime如何实现weak属性?
  9. @property中有哪些属性关键字?@property后面可以有哪些修饰符?
  10. weak属性需要在dealloc中置nil么?
  11. @synthesize@dynamic分别有什么作用?
  12. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?
  13. @property声明的NSString(或NSArrayNSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
  14. 对非集合类对象的copy操作
  15. 集合类对象的copymutableCopy

<h4 id="1">1.语法示例</h4>

修改方法有很多种,现给出一种做示例:

typedef NS_ENUM(NSInteger, JJSex) {
   JJSexMan,
   JJSexWoman
};

@interface JJUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) JJSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;

@end

下面对具体修改的地方,分两部分做下介绍:硬伤部分优化部分 。因为硬伤部分没什么技术含量,为了节省大家时间,放在后面讲,大神请直接看优化部分

<h6 id="1.1">优化部分</h6>

1.enum 建议使用NS_ENUMNS_OPTIONS宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:

//定义一个枚举 
typedef NS_ENUM(NSInteger, JJSex) { 
     JJSexMan,
     JJSexWoman 
};

(仅仅让性别包含男和女可能并不严谨,最严谨的做法可以参考 这里 。)

2.age 属性的类型:应避免使用基本类型,建议使用 Foundation 数据类型,对应关系如下:

int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
动画时间 -> NSTimeInterval

同时考虑到 age 的特点,应使用 NSUInteger ,而非 int 。 这样做的是基于64-bit 适配考虑,详情可参考出题者的博文《64-bit Tips》

3.如果工程项目非常庞大,需要拆分成不同的模块,可以在类、typedef宏命名的时候使用前缀。

4.doLogIn方法不应写在该类中:

  • 虽然LogIn的命名不太清晰,但笔者猜测是login的意思, (勘误:Login是名词,LogIn 是动词,都表示登陆的意思。见: Log in vs. login
  • 登录操作属于业务逻辑,观察类名 UserModel ,以及属性的命名方式,该类应该是一个Model而不是一个“ MVVM模式下的 ViewModel”:

无论是MVC模式还是MVVM模式,业务逻辑都不应当写在Model里:MVC 应在CMVVM应在VM

(如果抛开命名规范,假设该类真的是MVVM模式里的ViewModel,那么 UserModel这个类可能对应的是用户注册页面,如果有特殊的业务需求,比如: -logIn对应的应当是注册并登录的一个Button,出现-logIn
方法也可能是合理的。)

5.doLogIn方法命名不规范:添加了多余的动词前缀。 请牢记:

如果方法表示让对象执行一个动作,使用动词打头来命名,注意不要使用dodoes这种多余的关键字,动词本身的暗示就足够了。

应为-logIn(注意:Login是名词, LogIn 是动词,都表示登陆。 见 Log in vs. login

6.-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用 with 来连接两个参数: withAge: 应当换为age:,age: 已经足以清晰说明参数的作用,也不建议用 andAge :通常情况下,即使有类似 withA:withB: 的命名需求,也通常是使用withA:andB: 这种命名,用来表示方法执行了两个相对独立的操作(从设计上来说,这时候也可以拆分成两个独立的方法),它不应该用作阐明有多个参数,比如下面的:
//错误,不要使用"and"来连接参数
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//错误,不要使用"and"来阐明有多个参数
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正确,使用"and"来表示两个相对独立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;

7.由于字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为 copy 。(原因在下文有详细论述:@property声明的NSString(或NSArrayNSDictionary)经常使用copy关键字,为什么?)

8.“性别”(sex)属性的:该类中只给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那如何对“性别”(Sex)初始化?

Objective-C 有 designatedsecondary 初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。举例说明:

@implementation JJUser 
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex 
{ 
        if(self = [super init]) {
            _name = [name copy]; 
            _age = age; 
            _sex = sex; 
        } 
        return self; 
}
 - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age { 
        return [self initWithName:name age:age sex:nil]; 
}
@end

上面的代码中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因为仅仅是调用类实现的 designated 初始化方法。
因为出题者没有给出 .m文件,所以有两种猜测:
1:本来打算只设计一个 designated 初始化方法,但漏掉了“性别”(sex)属性。那么最终的修改代码就是上文给出的第一种修改方法。
2:不打算初始时初始化“性别”(sex)属性,打算后期再修改,如果是这种情况,那么应该把“性别”(sex)属性设为readwrite 属性,最终给出的修改代码应该是:

typedef NS_ENUM(NSInteger, JJSex) {
        JJSexMan,
        JJSexWoman 
}; 
@interface JJUser : NSObject<NSCopying> 
@property (nonatomic, readonly, copy) NSString *name; 
@property (nonatomic, readonly, assign) NSUInteger age; 
@property (nonatomic, readwrite, assign) JJSex sex; 
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex; 
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age; 
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex; 
@end

.h中暴露 designated 初始化方法,是为了方便子类化 (想了解更多,请戳--》 《禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)》。)

由于是只读属性,所以编译器不会为其创建对应的“设置方法”,即便如此,我们还是要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。要是不写明语义的话,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法之前自行拷贝属性值。这种操作多余而且低效。

9.initUserModelWithUserName 如果改为 initWithName 会更加简洁,而且足够清晰。

10.UserModel 如果改为 User 会更加简洁,而且足够清晰。

11.UserSex如果改为Sex 会更加简洁,而且足够清晰。

12.第二个 @propertyassignnonatomic 调换位置。 推荐按照下面的格式来定义属性:

@property (nonatomic, readwrite, copy) NSString *name;

属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样做你的属性更容易修改正确,并且更好阅读。这在《禅与Objective-C编程艺术 >》里有介绍。而且习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作。

<h6 id="1.2">硬伤部分</h6>
1.在-(void)之间应该有一个空格
2.enum 中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿使用下划线命名法。
3.enum 左括号前加一个空格,或者将左括号换到下一行
4.enum 右括号后加一个空格
5.UserModel :NSObject应为UserModel : NSObject,也就是:右侧少了一个空格。
6.@interface@property 属性声明中间应当间隔一行。
7.两个方法定义之间不需要换行,有时为了区分方法的功能也可间隔一行,但示例代码中间隔了两行。
8.-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名与参数之间多了空格。而且 -(id) 之间少了空格。
9.-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名与参数之间多了空格:(NSString*)name 前多了空格。
10.-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,应为 (NSString *)name,少了空格。
11.doLogIn方法中的 LogIn 命名不清晰:笔者猜测是login的意思,应该是粗心手误造成的。
(勘误: Login 是名词,LogIn 是动词,都表示登陆的意思。见: Log in vs. login


<h4 id="2">2.什么情况使用 weak 关键字,相比 assign 有什么不同?</h4>

什么情况使用 weak 关键字?

在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性

自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。在下文也有论述:《IBOutlet连出来的视图属性为什么可以被设置成weak?》

不同点:

  1. weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloatNSlnteger 等)的简单赋值操作。

  2. assign 可以用非 OC 对象,而 weak 必须用于 OC 对象


<h4 id="3">3. 怎么用 copy 关键字?</h4>

用途:

  1. NSStringNSArrayNSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableStringNSMutableArrayNSMutableDictionary
  2. block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks

block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你“日用而不知”,你平时开发中是经常在用我说的这种做法的,比如下面的属性不写copy也行,但是你会选择写还是不写呢?

@property (nonatomic, copy) NSString *userId;
- (instancetype)initWithUserId:(NSString *)userId {
   self = [super init];
   if (!self) {
       return nil;
   }
   _userId = [userId copy];
   return self;
}

下面做下解释: copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

@property 声明 NSStringNSArrayNSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableStringNSMutableArrayNSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

该问题在下文中也有论述:用@property声明的NSString(或NSArrayNSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?


<h4 id="4">4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;</h4>

两个问题:

  1. 添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃.因为 copy 就是复制一个不可变 NSArray 的对象;
  2. 使用了 atomic 属性会严重影响性能 ;

第1条的相关原因在下文中有论述《用@property声明的NSString(或NSArrayNSDictionary)经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题?》 以及上文《怎么用 copy 关键字?》也有论述。

比如下面的代码就会发生崩溃:
.h文件

@property (nonatomic, copy) NSMutableArray *mutableArray;

.m文件

NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];

接下来就会奔溃:

-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460

第2条原因,如下:

该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明 nonatomic 可以节省这些虽然很小但是不必要额外开销。

在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的”(atomic))。

在iOS开发中,你会发现,几乎所有属性都声明为 nonatomic

一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。

因此,开发iOS程序时一般都会使用 nonatomic 属性。但是在开发 Mac OS X 程序时,使用 atomic 属性通常都不会有性能瓶颈。


<h4 id="5">5. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?</h4>

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopyingNSMutableCopying 协议。

具体步骤:

  1. 需声明该类遵从 NSCopying 协议

  2. 实现 NSCopying 协议。该协议只有一个方法:

    - (id)copyWithZone:(NSZone *)zone;
    

注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。
以第一题的代码为例:
.h文件

typedef NS_ENUM(NSInteger, JJSex) {
       JJSexMan,
       JJSexWoman
};

@interface JJUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) JJSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;
 @end

然后实现协议中规定的方法:

- (id)copyWithZone:(NSZone *)zone {
    JJUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex];
    return copy;
}

但在实际的项目中,不可能这么简单,遇到更复杂一点,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。举个例子,假如 CYLUser 中含有一个数组,与其他 CYLUser 对象建立或解除朋友关系的那些方法都需要操作这个数组。那么在这种情况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的全部代码:
.h文件

typedef NS_ENUM(NSInteger, JJSex) {
    JJSexMan,
    JJSexWoman
};

@interface JJUser : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) JJSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JJSex)sex;
- (void)addFriend:(JJUser *)user;
- (void)removeFriend:(JJUser *)user;

@end

.m文件

@implementation JJUser {
   NSMutableSet *_friends;
}

- (void)setName:(NSString *)name {
   _name = [name copy];
}

- (instancetype)initWithName:(NSString *)name
                    age:(NSUInteger)age
                    sex:(JJSex)sex {
   if(self = [super init]) {
       _name = [name copy];
       _age = age;
       _sex = sex;
       _friends = [[NSMutableSet alloc] init];
   }
   return self;
}

- (void)addFriend:(JJUser *)user {
   [_friends addObject:user];
}

- (void)removeFriend:(JJUser *)user {
   [_friends removeObject:user];
}

- (id)copyWithZone:(NSZone *)zone {
   JJUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex];
   copy->_friends = [_friends mutableCopy];
   return copy;
}

- (id)deepCopy {
   CYLUser *copy = [[[self class] alloc] initWithName:_name age:_age sex:_sex];
   copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
   return copy;
}

@end

以上做法能满足基本的需求,但是也有缺陷:

如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

【注:深浅拷贝的概念,在下文中有介绍,详见下文的:@property声明的 NSString(或NSArrayNSDictionary)经常使用 copy 关键字,为什么?如果改用 strong 关键字,可能造成什么问题?

在例子中,存放朋友对象的 set 是用 “copyWithZone:” 方法来拷贝的,这种浅拷贝方式不会逐个复制 set 中的元素。若需要深拷贝的话,则可像下面这样,编写一个专供深拷贝所用的方法:

- (id)deepCopy {
   CYLUser *copy = [[[self class] alloc] initWithName:_name age:_age sex:_sex];
   copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
   return copy;
}

至于如何重写带 copy 关键字的 setter这个问题,
如果抛开本例来回答的话,如下:

- (void)setName:(NSString *)name {
    //[_name release];
    _name = [name copy];
}

不过也有争议,有人说“苹果如果像下面这样干,是不是效率会高一些?”

- (void)setName:(NSString *)name {
   if (_name != name) {
       //[_name release];//MRC
       _name = [name copy];
   }
}

这样真得高效吗?不见得!这种写法“看上去很美、很合理”,但在实际开发中,它更像下图里的做法:

如果使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫做“自动合成”( auto synthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法” (synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。

@interface JJPerson : NSObject 
@property NSString *firstName; 
@property NSString *lastName; 
@end

在上例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

@implementation JJPerson 
@synthesize firstName = _myFirstName; 
@synthesize lastName = _myLastName; 
@end 

上述语法会将生成的实例变量命名为 _myFirstName_myLastName ,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

总结下 @synthesize 合成实例变量的规则,有以下几点:

  1. 如果指定了成员变量的名称,会生成一个指定的名称的成员变量,

  2. 如果这个成员已经存在了就不再生成了.

  3. 如果是 @synthesize foo; 还会生成一个名称为foo的成员变量,也就是说:
    如果没有指定成员变量的名称会自动生成一个属性同名的成员变量,

  4. 如果是 @synthesize foo = _foo; 就不会生成成员变量了.

假如 property 名为 foo,存在一个名为 _foo 的实例变量,那么还会自动合成新变量么? 不会。如下图:


<h4 id="15">15. 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?</h4>

回答这个问题前,我们要搞清楚一个问题,什么情况下不会autosynthesis(自动合成)?

  1. 同时重写了 settergetter
  2. 重写了只读属性的 getter
  3. 使用了 @dynamic
  4. @protocol 中定义的所有属性
  5. category 中定义的所有属性
  6. 重载的属性

当你在子类中重载了父类中的属性,你必须 使用 @synthesize 来手动合成ivar

除了后三条,对其他几个我们可以总结出一个规律:当你想手动管理 @property 的所有内容时,你就会尝试通过实现 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic 来达到这个目的,这时编译器就会认为你打算手动管理 @property,于是编译器就禁用了 autosynthesis(自动合成)。

因为有了 autosynthesis(自动合成),大部分开发者已经习惯不去手动定义ivar,而是依赖于 autosynthesis(自动合成),但是一旦你需要使用ivar,而 autosynthesis(自动合成)又失效了,如果不去手动定义ivar,那么你就得借助 @synthesize 来手动合成 ivar

其实,@synthesize 语法还有一个应用场景,但是不太建议大家使用:

可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

@implementation CYLPerson 
@synthesize firstName = _myFirstName; 
@synthesize lastName = _myLastName; 
@end 

上述语法会将生成的实例变量命名为 _myFirstName_myLastName,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

举例说明:应用场景:

@import Foundation;

@interface CYLObject : NSObject
@property (nonatomic, copy) NSString *title;
@end

@implementation CYLObject {
   //    NSString *_title;
}

//@synthesize title = _title;

- (instancetype)init
{
   self = [super init];
   if (self) {
       _title = @"微博@iOS程序犭袁";
   }
   return self;
}

- (NSString *)title {
   return _title;
}

- (void)setTitle:(NSString *)title {
   _title = [title copy];
}

@end

结果编译器报错:

当你同时重写了 settergetter 时,系统就不会生成 ivar(实例变量/成员变量)。这时候有两种选择:

  1. 要么如第14行:手动创建 ivar
  2. 要么如第17行:使用@synthesize foo = _foo; 关联 @propertyivar

更多信息,请戳- 》 When should I use @synthesize explicitly?

上一篇下一篇

猜你喜欢

热点阅读