美文网首页ios基础
iOS 编写高质量Objective-C代码(三)—— 接口和A

iOS 编写高质量Objective-C代码(三)—— 接口和A

作者: 齐舞647 | 来源:发表于2018-08-17 15:46 被阅读437次

    《编写高质量OC代码》已经顺利完成一二三四五六七篇!
    附上链接:
    iOS 编写高质量Objective-C代码(一)—— 简介
    iOS 编写高质量Objective-C代码(二)—— 面向对象
    iOS 编写高质量Objective-C代码(三)—— 接口和API设计
    iOS 编写高质量Objective-C代码(四)—— 协议与分类
    iOS 编写高质量Objective-C代码(五)—— 内存管理机制
    iOS 编写高质量Objective-C代码(六)—— block专栏
    iOS 编写高质量Objective-C代码(七)—— GCD专栏


    这一篇,将通过介绍OC的接口和API设计来提高Objective-C的代码质量

    一、用前缀避免命名空间冲突

    OC里没有命名空间的概念(namespace)。于是,我们需要给类加前缀,避免重名,避免发生命名冲突。当然,不仅是类名,一些全局变量和方法也需要加上适当的前缀加以区分。

    所以,我们要:

    • 选择与公司、工程相关的前缀作为类名的前缀。

    • 为了避免重复引用第三方库带来的冲突,必要时也要为他们加上前缀区分。

    二、提供“全能初始化方法”

    • 在类中提供一个全能初始化方法,并在文档中写明注释。其他的初始化方法全调用此全能初始化方法。
    • 好处:当类的结构发生改变或初始化逻辑发生改变时,只需要改动全能初始化方法即可。

    举个例子来说:可以看一下NSDate类中定义了一个全能初始化方法:

    - (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
    

    其余的初始化方法 定义在NSDate (NSDateCreation) 分类中

    - (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
    - (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
    - (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
    

    在官方文档中,关于NSDate有如下说明
    If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must:

    • Override initWithTimeIntervalSinceReferenceDate:, one of the designated initializer methods

    解释:选定一个方法作为全能初始化方法,剩下的其余的初始化方法都调用这这个方法初始化,这样做的好处是以后如果初始化的逻辑更改了只需更改全能初始化方法,或者即使子类覆写的时候也只覆写全能初始化方法~

    三、实现 description 方法

    本条写的是通过覆写description(或者debugDescription)方法来在NSLog打印(或者LLDB打印时)输出更多的自定义信息。

    下面举个例子:

    - (NSString *)description {
    
        return [NSString stringWithFormat:@"<%@: %p, %@>",
                [self class],
                self,
                @{
                  @"qi": _qi,
                  @"share" : _share}
                ];
    }
    

    四、尽量使用不可变对象

    1. 声明对外属性时,尽量使用不可变对象,同时,对外属性声明里尽量加上readonly修饰(默认是readwrite修饰)。这样外部只能读取数据而不能修改数据,保证了这个类的实例所持有的数据更加安全。尤其是不要把可变的collection作为属性公开,而是应该提供相应的方法修改可变的collection。
    2. 若外部想修改修改对象的值有两种途径:
      • 提供接口方法修改
      • 使用KVC(Key-Value Coding)技术
        这种技术允许对象的数据或属性可以在运行时通过其键名进行查找,其中,属性的名称即为其值的键名。在静态语言中,这样的做法是不可能的。KVC大大的增加了设计的自由度:通过KVC,无需知道对象的类型即可访问其属性或数据。

    例如:
    不推荐写法:

    //Animals.h
    @property (nonatomic, strong) NSSet *animals;
    

    应改为:

    //Animals.h
    @interface Animals : NSObject
    
    @property (nonatomic, strong, readonly) NSSet *animals;
    
    - (void)addAnimal:(NSString *)animal;
    - (void)removeAnimal:(NSString *)animal;
    
    @end
    
    
    //Animals.m
    @implementation Animals {
        NSMutableSet *_mutableAnimals;
    }
    
    - (NSSet *)animals {
        return [_mutableAnimals copy];
    }
    
    - (void)addAnimal:(NSString *)animal {
        [_mutableAnimals addObject:animal];
    }
    
    - (void)removeAnimal:(NSString *)animal {
        [_mutableAnimals removeObject:animal];
    }
    

    但是,小编认为这样写固然有好处:保证了数据的安全性,但代码量也会提升不少。所以推荐大家可以有选择的使用,对一些重要的类才有使用必要。

    另外,如果某属性仅可以在对象内部修改,则可以在.h文件中声明为readonly。然后 在.m的类扩展中将属性扩展为readwrite属性。

    当然,师父说了:也可以在.h文件中把属性声明为readonly,在.m文件中通过实例变量修改值,当block内部修改值时,可以用self->实例变量的方法访问修改。(小编测试过,确实有效。欢迎路过的大神继续讨论)

    五、使用清晰而协调的命名方式

    师父语录:“写OC代码像是在讲故事,而读OC代码更像是在听故事。”

    这句话要归功于OC清晰而协调的命名方式。

    • 首先,是驼峰式命名方法:这个和大部分编程语言都一样。
    • 其次,也是最关键的方法命名。从左至右读起来就像日常用语中的句子。

    例如:我们想给初始化一个矩形,并给他的宽和高赋值。

    // C++:
    Rectangle *aRectangle = new Rectangle(5.0, 10.0);
    
    // Objective-C:
    Rectangle *aRectangle = [[Rectangle alloc] initWithWidth:5.0 andHeight:10.0];
    

    很显然,OC的方法可以很直接的看出所要传递的参数的具体含义,而C++的传参就并没有这么直观。

    六、为私有方法名加前缀

    这一条:给大家参考一下我们QiShare团队制定的 iOS 代码规范
    QiShare更喜欢通过#pragma mark -来区分 公私有等方法
    例如:

    #pragma mark - Private Functions
    
    // code...
    
    
    #pragma mark - Action functions
    
    // code...
    
    
    #pragma mark - Request functions
    
    // code...
    
    
    #pragma mark - xxxDataSource
    
    // code...
    
    
    #pragma mark - xxxDelegate
    
    // code...
    
    

    当然,大家也可以根据团队自己定制规范。

    七、理解 Objective-C 错误模型

    很多语言都有异常处理机制,Objective-C也不例外。@throw
    但是,
    注意:OC里抛异常可能会导致内存泄漏
    注意:OC里抛异常可能会导致内存泄漏
    注意:OC里抛异常可能会导致内存泄漏
    解释:OC里的ARC机制(Automatic Reference Counting)在默认情况下是“无异常安全”。简单来说,一旦抛出异常,对象就无法正常自动释放了。
    所以,

    1. 异常只用于处理严重的错误(fatal error,致命错误)
    2. 对于一些不那么严重的错误(nonfatal error,非致命错误),有两种解决方案:
      • 让对象返回nil或者0(例如:初始化的参数不合法,方法返回nil或0)
      • 使用NSError

    八、理解 NSCopying协议

    在iOS开发中,使用对象时经常需要拷贝它,这时我们会通过copy/mutableCopy方法完成。
    如果我们想让自己的类支持拷贝,那就必须要实现NSCopying协议,该协议只有一个方法:

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

    当然,如果要求返回对象是可变的那就要实现NSMutableCopying协议,对应方法:

    - (id)mutableCopyWithZone:(nullable NSZone *)zone;
    

    同时,在拷贝对象时,要注意是执行浅拷贝还是深拷贝

    那么引出了一个概念:什么是深拷贝?什么是浅拷贝?

    • 深拷贝:内容拷贝(既拷贝新的指针又拷贝出新的Object
    • 浅拷贝:指针拷贝(仅拷贝新的指针指向原来的Object

    这里有张很经典的图解:

    左边浅拷贝,右边深拷贝
    深拷贝在拷贝对象时,会将指针所指的底层数据也拷贝一份。而浅拷贝只是创建了一个新的指针指向要拷贝的内容。一般情况下,尽量使用浅拷贝。
    此外,还有一个注意点:
    [NSMutableArray copy] 拷贝出 => NSArray (不可变)
    [NSArray mutableCopy] 拷贝出 => NSMutableArray(可变)
    这种操作可以在可变版本和不可变版本间切换。

    说太多,不如给一个Demo~

    • 下面请看小编准备的NSCopying协议相关的小Demo:

    QiShareMember.h:

    @interface QiShareMember : NSObject <NSCopying>
    
    @property (nonatomic, copy, readonly) NSString *name; //!< 姓名
    @property (nonatomic, copy, readonly) NSString *sex; //!< 性别
    @property (nonatomic, assign, readonly) NSUInteger age; //!< 年龄
    
    //! 初始化方法
    - (instancetype)initWithName:(NSString *)name andSex:(NSString *)sex andAge:(NSUInteger)age;
    
    - (void)addFriend:(QiShareMember *)friend;
    - (void)removeFriend:(QiShareMember *)friend;
    
    @end
    

    QiShareMember.m:

    @implementation QiShareMember {
        
        NSMutableSet *_friends;
    }
    
    - (instancetype)initWithName:(NSString *)name andSex:(NSString *)sex andAge:(NSUInteger)age {
        
        if (self = [super init]) {
            
            _name = [name copy];
            _sex = [sex copy];
            _age = age;
            _friends = [NSMutableSet new];
        }
        
        return self;
    }
    
    - (void)addFriend:(QiShareMember *)friend {
        
        [_friends addObject:friend];
    }
    
    - (void)removeFriend:(QiShareMember *)friend {
        
        [_friends removeObject:friend];
    }
    
    - (id)copyWithZone:(NSZone *)zone {
        
        QiShareMember *copy = [[[self class] allocWithZone:zone] initWithName:_name andSex:_sex andAge:_age];
        copy->_friends = [_friends mutableCopy]; //!< 注意friends只是一个实例变量不是一个属性,所以不能用点语法
        
        return copy;
    }
    
    @end
    

    最后,特别致谢《Effective Objective-C 2.0》第三章

    相关文章

      网友评论

      • 梁森的简书:1.为什么要尽量使用不可变对象呢? 是因为内存空间的原因吗?
        2.对外属性尽量加上readOnly? 我项目中如果要设计对象属性必然是要让它可写的。如cell的一个模型属性 感觉用readOnly很少(能举个开发中的例子吗?)
        梁森的简书:@充满活力的早晨 你们都是怎么进行单元测试的?
        充满活力的早晨:其实从单元测试的角度来看,我们单元测试的都是方法,不会单独对属性进行测试,要是外界可以随便修改属性的值,那么就保证不了这个类的单元测试准确
        齐舞647:@梁森森 对外的对象,一般为了数据的可靠性和准确性不希望被改动。所以设置为readonly。
        而你举的例子,是要把模型传递给cell。属于对内传递数据的对象。
        对一些比较重要的不可变数据,可以在.h中声明readonly,.m中再声明为readwhite。这样最安全。

      本文标题:iOS 编写高质量Objective-C代码(三)—— 接口和A

      本文链接:https://www.haomeiwen.com/subject/osdkbftx.html