接口与API设计

文章目录

用前缀避免命名空间冲突

OC没有其他语言内置的命名空间机制,而我们可能会写很多的文件,所以我们在起名的时候要设法避免潜在的命名冲突,否则很容易就重名了,这样可能会导致整个程序的崩溃

所以在这里,我们就有一个具体的规定,就是我们使用三个字符的前缀,然后apple使用两个字符的前缀

注意:

  • 选择与公司,应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码里都使用这一前缀
  • 若自己开发的程序中用到了第三方库,则应为其中的名称加上前缀

提供全能初始化方法

我们在进行对象的创立时候都要进行初始化,初始化的时候都有着不同的方法,创建类的实例方法不止一种,不过,我们还是有其中一个座位全能的初始化方法,另其他的初始化方法都来调用他,比如下面的NSData这个例子:

如上图所示的**initWithTimeIntercalSinceReferenceDate:**这个方法是全能初始化方法,其他的初始化方法都要调用他,只有在全能初始化方法中才会存储内部数据,这样的话,当底层数据存储机制改变的时候,只需要改这个方法的代码就行

如果子类的全能初始化方法和其超类方法的名称不一样,那么我们应该去覆写超类的全能初始化方法

子类必须做的两件事:

  1. 实现自己的指定的初始化方法,如

    objc 复制代码
    - (instancetype)initWithName:(NSString *)name {
        self = [super initWithXXX:...]; // 调用父类指定初始化方法
        if (self) {
            _name = name;
        }
        return self;
    }
  2. 必须覆写父类的指定初始化方法

    否则会出现,父类初始化了,子类没有初始化完整

注意:

  • 要在类里提供一个全能初始化方法
  • 若全能初始化方法与超类不同,则应覆写超类里的对应方法
  • 若超类的初始化不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常

实现description方法

在进行调试的时候,我们常用的一个方法就是打印并查看对象的信息,在打印如数组的对象时,我们直接打印这个对象打印出的一般就直接是对象里的元素,但是当我们在我们自定义的类里面进行直接打印的时候,打印的却直接是对象的地址例如下面

objc 复制代码
NSLog(@"%@", person);
//一般输出的是<Person: 0x60000022b180>即<类名: 内存地址>

所以一般我们想要输出更有用的信息的时候,就需要来重写description方法,例如:

  1. 字符串拼接

    objc 复制代码
    - (NSString *)description {
        return [NSString stringWithFormat:@"%@: %p, %@ %@",
                [self class], self, _firstName, _name];
    }
  2. 字典拼接

    我们也可以通过字典或者json风格输出

    objc 复制代码
    - (NSString *)description {
        NSDictionary *info = @{
            @"firstName": self.firstName ?: @"",
            @"name": self.name ?: @"",
            @"age": @(self.age),
            @"gender": self.gender ?: @""
        };
        return [NSString stringWithFormat:@"%@: %@", [self class], info];
    }

    这样的输出就会更简明

NSObject协议里还有一个可以在调试器中以控制台命令打印对象时可以调用的方法

objc 复制代码
- (NSString *)debugDescription;
  • 实现description方法返回一个有意义的字符串,用以描述该实例
  • 若想在调试打印的时候打印出更详尽的对象描述信息,则应该实现debugDescription方法

尽量使用不可变方法

设计类的时候,我们应该尽量使用不可变对象,即尽量把对外公开的属性设为只读类型的,即:

objc 复制代码
@property (nonatomic, copy, readonly) NSString* name;

然后如果我们想可以重新修改这个封装在对象里的数据的话,可以在类拓展(extension)中重新声明这个属性

如:

objc 复制代码
@interface Person ()
@property (nonatomic, copy, readwrite) NSString* name;
@end

这样我们就可以实现,对外readonly只读,对内readwrite可写

在.h里面我们设置只读readonly可以实现对外和对内都是只读的,都不能改,但是当我们想init赋值的时候就会报错,而我们在类拓展里使用readwrite可以在.m文件内部把这个属性解锁为可写的

即:

objc 复制代码
//在外部
person.firstName = @"Tom"; //报错
//在内部
self.firstName = @"Tom"; // 可以

完整示例

objc 复制代码
// Person.h
@interface Person : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *name;
- (instancetype)initWithName:(NSString *)firstName
                    lastName:(NSString *)name;

@end
// Person.m
@interface Person ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *name;
@end

@implementation Person

- (instancetype)initWithName:(NSString *)firstName
                    lastName:(NSString *)name {
    self = [super init];
    if (self) {
        self.firstName = firstName;
        self.name = name;
    }
    return self;
}
@end

不要把可变的 collection(NSMutableArray / NSMutableSet)直接暴露出去,而是提供方法去操作它(add / remove)

若是可变的话,即

objc 复制代码
@property (nonatomic, strong) NSMutableArray *friends;

这样外部可以直接进行addObject或者remove方法,数据就会失控

所以我们在返回一个对象要求是collection时,还是尽量返回一个不可变的对象

对外只暴漏不可变

objc 复制代码
@property (nonatomic, copy, readonly) NSArray *friends;

对内用可变的存

objc 复制代码
@implementation Person {
    NSMutableArray *_internalFriends;
}

getter返回副本

objc 复制代码
- (NSArray *)friends {
    return [_internalFriends copy];
}

提供操作方法

objc 复制代码
- (void)addFriend:(Person *)person {
    if (![_internalFriends containsObject:person]) {
        [_internalFriends addObject:person];
    }
}
- (void)removeFriend:(Person *)person {
    [_internalFriends removeObject:person];
}

完整示例:

objc 复制代码
// Person.h
@interface Person : NSObject

@property (nonatomic, copy, readonly) NSArray *friends;

- (void)addFriend:(Person *)person;
- (void)removeFriend:(Person *)person;

@end
// Person.m
@interface Person ()
@property (nonatomic, strong) NSMutableArray *internalFriends;
@end

@implementation Person

- (instancetype)init {
    if (self = [super init]) {
        _internalFriends = [NSMutableArray array];
    }
    return self;
}

- (NSArray *)friends {
    return [_internalFriends copy];
}

- (void)addFriend:(Person *)person {
    if (person && ![_internalFriends containsObject:person]) {
        [_internalFriends addObject:person];
    }
}
- (void)removeFriend:(Person *)person {
    [_internalFriends removeObject:person];
}

@end

这样来避免外部直接修改里面的内容

使用清晰而协调的命名方法

这里主要是告诉我们怎么正确命名方法之类的

  1. 方法名要想一句话,即动作 + 对象,如addFriend

  2. 参数带语意让整句可以读通,如

    objc 复制代码
    - (void)sendMessage:(NSString *)message toPerson:(Person *)person;
  3. 返回一个新对象用方法名带类型,如arrayWith...、dictionaryWith...、stringWith...等

  4. BOOL一定要加is/ha s

  5. 不乱用get

  6. 统一风格不用缩写

为私有方法加前缀

编写在类内部使用的私有方法时,应该给这些方法前面加前缀,如前面加p_,p表示私有的(private),后面正常命名即可,如

objc 复制代码
- (void)p_privateMethod;

理解Objective-c错误模型

OC 中:异常(NSException)很少用,NSError 才是主流错误处理方式

OC不常用异常(NSException),只有极罕见的情况下才会用

  1. 自动引用计数不是异常安全的,如
objc 复制代码
// 作用域结束本该释放
id obj = [[NSObject alloc] init];
@throw ... //  如果这里抛异常

​ 可能会导致ARC不能正常释放或者内存泄漏

  1. 解决的代价会很大,若要保证异常安全,所有代码都会有额外开销

  2. 异常的使用一般只在如不可恢复的严重错误里使用,如数组越界、访问 nil 关键资源、程序逻辑彻底错误、抽象方法被错误调用且不可恢复的时候才会使用

常用错误模型NSError

先来认识一下,主要有三个信息:

objc 复制代码
NSError *error = [NSError errorWithDomain:... code:... userInfo:...];
  1. Error Domain(错误范围,类型是字符串)
    • 代表错误的发生范围,产生错误的根源
    • 通常使用特定的全局变量来表示,如NSURLErrorDomain:专门表示URL加载、网络请求、URL解析等相关错误,NSCocoaErrorDomain:表示Cocoa框架内部的错误
    • 例如当URL解析失败时,会使用NSURLErrorDomain来表示该错误来源于URL处理子系统
  2. Error Code(错误码,整数)
    • 用于指明在某个错误范围内发生的具体错误,同一个范围内可能有不同的错误情况,通常使用enum定义。
    • 在HTTP请求错误中,会返回对应的状态码,如404(Not Found)、500(Internal Server Error)等错误信息
  3. User info(用户类型,字典)
    • 有关此错误的额外信息,其中包含一段本地化的描述

NSError的两种常见传递方式:

  1. 委托,例如

    objc 复制代码
    - (void)connection:(NSURLConnection *)connection
      didFailWithError:(NSError *)error {
    
        NSLog(@"错误: %@", error.localizedDescription);
    }

    常见于网络请求或者异步任务

  2. 输出参数

    objc 复制代码
    - (BOOL)doSomething:(NSError **)error {
        if (发生错误) {
            if (error) {
                *error = [NSError errorWithDomain:@"com.example" code:1001 userInfo:@{
                  NSLocalizedDescriptionKey: @"出错了"
                }];
            }
            return NO;
        }
        return YES;
    }

NSError **error 是指向NSError指针的指针,目的是把错误的对象传回去

在ARC下其真实类型是NSError * __autoreleasing *,这是一个自动释放对象的二级指针,他会自动释放,且不控制生命周期,会安全传递

理解NSCopying协议

在使用对象时,我们经常需要拷贝它,之前我们可以直接使用copy方法,而我们想令自己的类支持拷贝操作,就要实现NSCopying协议,该协议只有一个方法:

objc 复制代码
- (id)copyWithZone:(NSZone* )zone;

以前在开发程序时,会将内存分为不同的区,而对象的创建会在某个区内部,现在只有一个默认区,所以我们不需要担心里面的zone参数

而我们想覆写copy方法,实际上真正需要实现的是copyWithZone:方法,如果想要某个类实现拷贝功能,只需要该类遵从NSCopying协议,并实现对应方法

如:

objc 复制代码
-(id)copyWithZone:(NSZone *)zone {
    return [[[self class] allocWithZone:zone] initWithName:_firstName lastName:_name];
}

这里主要是创建了一个新的对象,然后进行一个全能初始化的操作,让其执行所有的初始化

在有的时候我们会遇到一些容器类的复制,我们这时候就要分情况,哪些时候要使用深拷贝,哪些时候使用浅拷贝

objc 复制代码
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;

如果copyItems参数为yes,这个方法会给数组中的每个对象发生copy,这个方法才是容器的一个深拷贝,而用普通的copy方式只是浅拷贝,不会拷贝每一个set中的元素

相关推荐
茶底世界之下5 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程7 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink15 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架18 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer