美文网首页程序员
编写高质量iOS与OSX代码的52个有效方法-第三章:接口与AP

编写高质量iOS与OSX代码的52个有效方法-第三章:接口与AP

作者: 竹与豆 | 来源:发表于2018-07-18 09:38 被阅读15次

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

    1、重命名符号错误

    OC没有其他语言内置的命名空间(namespace),命名时要避免潜在的命名冲冲突(naming clash):


    15-1.png

    比如如下错误,就是重命名符号错误(duplicate symbol error)。

    duplicate symbol _OBJC_CLASS_$_DogObject in:
        xxx/DogObject-ED8631F460AAA56A.o
        xxx/DogObject-917EE703FAC7406E.o
    duplicate symbol _OBJC_METACLASS_$_DogObject in:
        xxx/x86_64/DogObject-ED8631F460AAA56A.o
        xxx/DogObject-917EE703FAC7406E.o
    ld: 2 duplicate symbols for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    • 解决方法:把错误中提到的duplicate symbol _OBJC_CLASS_$_DogObject类名检查一遍,重新配置。

    • 避免方法(尤其是在引入很多三方库,或者项目工程文件较多的时候,应该说在所有的项目中都要如此):变相实现命名空间--为所有名称都加上是当前缀。

    另,苹果宣称保留使用所有两个字母的前缀(two-letter Prefix)的权利。所以自己选用前缀最好是三个字母。

    2、给新增分类和分类方法加上前缀(第25条)

    分类机制通常用于向无源码的既有类中新增功能。

    分类的方法是直接加到类中的,就好比是类中固有的方法,将分类方法加入类中这一操作时在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。

    如果类中本来就有这个方法,分类中又实现了一次,那么分类中方法会覆盖原来的实现代码。有可能会发生多次覆盖。

    如果多个分类名称相同,在运行期,是不会报错的。但是加载的分类有可能不是你所期望的。

    如果有相同的方法,那么运行时调用的不一定是你想要的方法。

    运行期不会报错,但是在实现结果的时候,就会出现未知错误。

    15-2.png

    比如实现了NSString的两个分类,同时都有一个分类方法"- (NSString *)urlEncordedString;"那么在调用过程中,就不知道是调用哪个分类中实现的方法。同样能够正常编译通过。

    NSString *urlString = @"http://www.baidu.com";
    NSLog(@"%@",[urlString urlEncordedString]);
    

    因为不会报错,这种问题比较难发现,所以在写之前就避免这种情况就显得非常重要。

    当然如果分类名相同,但是方法名不同时,有可能出现的问题是:No visible @interface for 'NSString' declares the selector 'seconString',你无法调用自己实现的方法。系统只是提供最后加载到的分类,如此而已。

    • 如何避免:添加分类时,给分类名称加上专用前缀,同时给分类方法名加上专用前缀。

    3、类的实现文件中所用的纯C函数及全局变量

    在类的实现文件中所用的纯C函数及全局变量,在编译好的目标文件中,这些名称要算作“顶级符号”(top-level symbol)。

    如果在不同的类文件中实现同样的C函数,就会报重命名符号错误(duplicate symbol error)

    15-3.png
    duplicate symbol _completion in:
        xxx/ViewController.o
        xxx/NSString+Http.o
    ld: 1 duplicate symbol for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    

    duplicate symbol _completion会指出错误方法名completion,另外会在下面的描述中指明在那些文件中出现冲突。

    • 解决方法:找到错误的类和方法名,修改。

    • 避免方法:C函数名加前缀,同时加上类名信息,在回溯查找问题是就能很快确定位置。

    同样的,即使在实现文件中声明全局静态变量,在不同文件中声明相同名称的变量,也会出现名称冲突错误:

    15-4.png
    duplicate symbol _NameString in:
        xxx/ViewController.o
        xxx/NSString+Http.o
    ld: 1 duplicate symbol for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    • 解决方法:找到错误的类和声明的变量名,修改。

    • 避免方法:声明全局变量,变量名前加前缀。

    4、所开发的程序库中用到第三方库,给第三方库加前缀

    这个问题很简单,如果要把自己封装的程序库给别人用,同时使用了不同的第三方库。那么在别人引用的时候,如果他工程中也使用相同的第三方库,就会出现重命名符号错误。

    另外考虑到所使用第三方库版本不同,那么,在封装自己的程序库时就要将所用到的第三方库中的文件添加前缀,避免此类问题。

    16、提供全能初始化方法

    全能初始化方法(designated initializer):为对象提供必要信息以便其能完成工作的初始化方法。

    可以通过警告或者设置默认值调用全能初始化方法的方式,实现初始化。

    全能初始化方法的调用链一定要维系,也即是,集成关系中,初始化方法的维护调用。

    Mac OS X 的APPKit会iOS的UIKit两个UI框架都广泛运用序列化机制(serialization mechanism),将对象序列化,保存至XML格式的XIB文件中农。这些XIB文件通常用来存放视图控制器机器视图布局。加载NIB文件时,系统会在解压缩的过程中解码视图控制器。

    • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。

    • 若全能初始化方法与超类不同,则需要覆写超类中的对应方法。

    • 若超类初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

    17、实现description方法

    实现description方法返回一个有意义的字符串,用以描述该实例。

    - (NSString *)description {
        return [NSString stringWithFormat:@"%@ %zd",_dogName,_dogAge];
    }
    

    若想在调试时(LLDB)打印出更详尽的对象描述信息,则应实现debugDescription方法。

    - (NSString *)debugDescription {
        return [NSString stringWithFormat:@"<%@: %p \"%@ %zd \">",[self class],self,_dogName,_dogAge];
    }
    

    打印结果

    17-1.png

    18、尽量使用不可变对象 --

    关联第6条-属性
    

    尽量减少对象中的可变内容,应该尽量把对外公布出来的属性设为只读,并且只在必要时才将属性对外公布。

    如果想要修改封装在对象内部的数据,同时不将哲学数据为外人所动,可以在对象内部将readonly属性重新声明为readwrite。

    在定义类的公共API时,对象里表示各种collection的那些属性究竟应该设成可变的,还是不可变的。

    • 尽量创建不可变的对象
    • 若某属性仅可于对象内部修改,则在class-continuation分类中将其由readonly扩展为readwrite。
    • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

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

    OC中一般采用驼峰式大小写命名法。

    1)方法命名

    • 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,如localizedString。属性的存取方法不遵循这种命名方式,一边惹味这种方法不会创建新对象,即使又是返回内部对象的一份拷贝,也认为那相当于原有的对象。这些存取方法应按照其所对应的属性来命名。
    - (NSString *)stringOfDogInfomation;
    
    - (NSDictionary *)dictionaryOfDogInfomation;
    
    • 应该把表示参数类型的名词放在参数前面。

    • 如果方法要在对象上执行操作,就应包含动词,若执行操作还需要参数,应在动词后面加上一个或多个名词。

    • 不要使用str这样的简称,而用string这样的全称。

    [string lowercaseString];
    
    • Boolean属性应加is前缀,如果方法返回非属性的Boolean值,那么应该根据其功能,选用has或is前缀。

    [string hasSuffix:@"this"];
    [string isEqualToString:@"xxxx"];

    • get前缀留给那些借由输出参数来保存返回值的方法,

    清晰明了,统一规范

    2)类与协议的命名

    应该为类和协议加上前缀,避免命名空间冲突。

    命名方式要协调一致,如果要从其他框架中继承子类,务必遵循其命名惯例。

    若要自定义委托协议,则名称中应包含委托发起方的名字,再加上Delegate

    • 起名时,遵从标准的OC命名规范
    • 方法名要言简意赅,从左只有读起来想个日常用语中的句子。
    • 方法名里不要使用缩略后的类型名称
    • 方法起名,确保风格与自己的代码或所要集成的框架相符。

    20、为私有方法名加前缀

    为在内部使用的私有方法加前缀,区分公共方法和私有方法,便于修改方法名和方法签名。

    依据个人习惯,p_method

    • 在私有方法名称前加上前缀,区分私有和公共方法。
    • 不要单用一个下划线做私有方法的前缀,因为这种是苹果公司预留的。

    21、理解OC错误模型

    1)异常 exception --fatal error致命错误

    OC中,在激起罕见的情况下抛出异常,异常抛出之后不再考虑恢复问题,应用程序此时应该退出。不需要再辨析复杂的“异常安全”代码。

    异常一般只用于处理严重错误(fatal error 致命错误)。

    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:string userInfo:nil];
    

    比如编写某个抽象基类,正确用法是先从中集成一个子类,再用这个子类。这种情况下,如果直接使用了这个抽象基类的,那么可以抛出异常。

    OC没有办法将某个类标记为“抽象类”。要想达成效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。

    2)其他错误 --nonfatal error 非致命错误

    OC语言所用编程范式为:令方法返回nil/0,或是使用NSError,表明有错误发生。

    NSError对象封装了三条消息:

    • Error domain(错误范围,类型为字符串)
      错误发生的范围,也就是产生错误的根源,通常用一个特有的全局变量来定义。如NSURLErrorDomain-处理URL的子系统,在从URL中解析或取得数据出错。

    • Error code 错误码,其类型为整数
      独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一些列相关错误,这些错误情况通常采用enum来定义。如,HTTP请求出错时,会把HTTP状态码设为错误码。

    • User info 用户信息,其类型是字典
      有关次错误的额外信息,其中或许包含一段本地化的描述(localized description),或许还包含导致该错误发生的另一个错误,经由此种信息,可将相关错误串成一条错误连(chain of errors)

    NSError的用法:

    • 通过委托协议来传递错误。
      当有错误发生时,当前对象会把错误信息经由协议的某个方法传递给其委托对象(delegate)。如NSURLConnection在其委托协议NSURLConnectionDelegate中定义代理方法- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

    • 经由方法的输出参数返回给调用者。

    - (BOOL)doSomething:(NSString *)thing error:(NSError **)error {
        if ([thing isEqualToString:@"1"]) {
            return YES;
        }
        *error = [NSError errorWithDomain:NSURLErrorDomain code:100 userInfo:@{@"key":@"something wrong"}];
        return NO ;
    }
    
    NSError *error;
    BOOL ret  = [littleDog doSomething:@"0" error:&error];
    if (!ret) {
        NSLog(@"error : %@",[error debugDescription]);
    }
    

    另外,定义自己的指定的专用错误范围字符串,使用这个字符串创建NSError对象,就能确定错误来源。

    extern NSString *const ZYDErrorDomain;
    
    typedef NS_ENUM(NSUInteger,ZYDError) {
        ZYDErrorUnknown                 = -1, //未知错误
        ZYDErrorBadInput                = 500,
    };
    
    

    3)

    • 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
    • 一般错误,可使用指派委托方法来处理错误,也可以把错误信息放在NSError对象中,经由输出参数返回给调用者。

    22、理解NSCopying协议

    1)不可变拷贝 NSCopying

    OC 中如果需要拷贝对象,需要通过copy方法完成。如果希望自己的类支持拷贝操作,就要实现NSCopying协议。该协议只要一个方法:

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

    在以前开发程序时,会据此吧内存分成不同的区(zone),而对象会创建在某个区里面。现在不用,每个程序只有一个区:默认区(default zone)。所以需要实现这个方法,但是不用担心zone参数。

    若要某个类支持拷贝功能,需要改类声明遵从NSCoping协议,并实现其中的方法就可以。

    .h
    @interface DogObject : NSObject <NSCopying>
    
    @end
    
    .m
    
    @interface DogObject()
    {
        NSMutableArray *_familys;//内部成员变量,并非属性
    }
    @end
    
    @implementation DogObject
    
    - (instancetype)initWithDogName:(NSString *)dogName age:(NSInteger)age {
        self = [super init];
        
        if (self) {
            _dogName = [dogName copy];
            _dogAge = age;
        }
        return self;
    }
    
    #pragma mark -- NSCopying
    - (id)copyWithZone:(NSZone *)zone {
        DogObject *copy = [[[self class] allocWithZone:zone] initWithDogName:_dogName age:_dogAge];
        copy -> _familys = [_familys mutableCopy]; //有
        return copy;
    }
    

    2)可变拷贝 NSMutableCopying

    定义一个方法:
    - (id)mutableCopyWithZone:(NSZone *)zone,与copy类似,也用默认的zone参数来调mutableCopyWithZone:。如果类分为可变版本,可不可变版本,需要实现NSMutableCopying。

    3)深拷贝

    深拷贝:在拷贝对象自身是,将其底层数据也一并复制过去。

    浅拷贝:之拷贝容器对象本身,而不复制漆黑中的数据。

    容易内的对象并不都能拷贝,而且调用者也未必要在拷贝容器的同时一并拷贝其中的每个对象。

    一般NSCopying大多数情况下执行的是浅拷贝,如需要在对象上执行深拷贝,那么除非该类的文档说它是用深拷贝来实现NSCopying协议的,否则,要么寻找能够执行深拷贝的方法,要么自己编写方法来实现。

    比如,NSArray中的方法:- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag如果flag为YES,该方法会向数组中每个元素发送copy信息,用拷贝好的创建新的Array,并返回给调用者。

    可以给对象创建自定义深拷贝方法:

    - (id)deepCopy {
        DogObject *copy = [[[self class] alloc] initWithDogName:_dogName age:_dogAge];
        copy -> _familys = [[NSMutableArray alloc] initWithArray:_familys copyItems:YES];
        return copy;
    }
    

    4)

    • 令自己所写的对象具有拷贝功能,需要实现NSCopying协议
    • 如果自定义对象分为可变和不可变版本,需要同时实现NSCopying与NSMutableCopying协议。
    • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下尽量执行浅拷贝
    • 如果对象需要深拷贝,那么新增一个专门执行深拷贝的方法。

    相关文章

      网友评论

        本文标题:编写高质量iOS与OSX代码的52个有效方法-第三章:接口与AP

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