iOS

从swift看OC

从swift看OC

Posted by Jincc on March 5, 2018

今天浏览到PSPDFKit Blog这篇文章,发现里面有很多tips是非常有用的,可以帮助提高Objective-C的工程质量,让我们的代码更安全,健壮和紧凑.

本文示例代码你可以在这里下载OCLikeSwift.

var and let

我们之前讨论过C++的auto关键字,它是非常棒的能够推断出对象的类型信息,特别是在我们处理泛型和block类型的时候.Objective-c不知道从啥时候也引进了__auto_type,但是我们基本上很少去使用证明一个关键字,因为实在太蹩脚了,下面我们包装一下它,让它看起来更加的swift.

###if defined(__cplusplus)
###define let auto const
###else
###define let const __auto_type
###endif
    
###if defined(__cplusplus)
###define var auto
###else
###define var __auto_type
###endif

var  view = [UIView new];
view.frame = CGRectMake(0, 0, 100, 100);
view.backgroundColor = [UIColor redColor];
[self.window addSubview:view];
    
view = nil;

foreach

swift和OC都有一个for..in语法糖.当我们在写swift循环变量一个泛型集合的时候,对象的类型能够很好的被推断出来。这在OC里面表现的不是太好.我们现在要做的事情就是封装一个语法糖能够在集合里面去推断对象的类型,看起来就像foreach

@protocol PSPDFFastEnumeration 
- (id)pspdf_enumeratedType;
@end

@interface NSArray  (PSPDFFastEnumeration) 
- (ElementType)pspdf_enumeratedType;
@end

@interface NSSet  (PSPDFFastEnumeration) 
- (ElementType)pspdf_enumeratedType;
@end

@interface NSDictionary  (PSPDFFastEnumeration) 
- (KeyType)pspdf_enumeratedType;
@end

// Usage: foreach (s, strings) { ... }
###define foreach(element, collection) for (typeof((collection).pspdf_enumeratedType) element in (collection))

Note:这个类别只声明它的返回类型,不要去实现它的实现内容.

下面我们就可以重新我们的循环语句了:

let annotations = [document annotationsForPageAtIndex:pageView.pageIndex type:PSPDFAnnotationTypeLink];
// old
for (PSPDFAnnotation *annotation in annotations) {
    NSLog(@"Color of %@ is %@", annotation, annotation.color);
}
// new
foreach (annotation, annotations) {
    NSLog(@"Color of %@ is %@", annotation, annotation.color);
}

现在,你可能会考虑为什么这个代码更好?如果类型可见的话会导致什么错误?Not much,really.不管怎样,它添加了编译时的安全保障.foreach仅仅会在你定义正确的泛型数组类型的时候正常工作,当你的数组里面不是PSPDFAnnotation 类型的时候,它将会报错. 想象一下,如果我们改变代码想下面这个样子:

for (NSString *annotationName in annotations) {
    NSLog(@"Annotation name is %@", annotationName.uppercaseString);
}

它将会在运行时crash,并且不会有编译器的警告.

Type information for copy / mutableCopy

@interface NSObject 
- (id)copy;
- (id)mutableCopy;
@end

我们知道NSObject协议里面的copymutableCopy返回的类型是id类型。折非常容易产生bugs。想象一下下面的代码:

+ (NSOrderedSet *)propertyKeys {
    NSMutableSet *propertyKeys = super.propertyKeys.mutableCopy;
    let allObjects = propertyKeys.allObjects; // BOOM runtime crash

这段代码没有编译错误但是会在运行时crash,“selector not found for allObjects”.

那么怎么解决呢?这里我们重新声明了这些类的这两个方法,like this:

@interface NSArray  (PSPDFSafeCopy)
/// Same as `copy` but retains the generic type.
- (NSArray  *)copy;
/// Same as `mutableCopy` but retains the generic type.
- (NSMutableArray  *)mutableCopy;
@end

这时候编译器能够知道对象的真正类型,当我们调用一个不属于它的方法的时候,编译器就会正确的处理错误.

+ (NSOrderedSet *)propertyKeys {
    let propertyKeys = super.propertyKeys.mutableCopy;
    let allObjects = propertyKeys.allObjects; // COMPILE TIME ERROR

defer

swift里面有个defer关键字,当离开当前的作用域的时候会执行一段代码块.这是非常方便的当我们在退出或者抛出异常的时候,便捷的语法帮助我们减少了内存泄漏.看下面一段代码:

CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)fileURL, NULL);
if (!imageSource) {
    // set error
    return NO;
}

CGImageRef image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, NULL);
if (!image) {
    // set error
    return NO; //***leak***
}

thumbnail = [UIImage imageWithCGImage:image scale:scale orientation:UIImageOrientationUp];
CFRelease(imageSource);
CFRelease(image);
return YES;

注意到内存泄漏了吗?如果创建image失败,imageSource将不能够正常释放.当然我们可以在条件语句里面释放对象,但是这样写代码将非常容易出错。一个更好的办法就是用defer去描述当我们退出作用域需要干的事情.它的概念和C++RAII有点相似.

CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)fileURL, NULL);
if (!imageSource) {
    // set error
    return NO; }
pspdf_defer { CFRelease(imageSource); };

CGImageRef image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, NULL);

if (!image) {
    // set error
    return NO;
}
pspdf_defer { CFRelease(image); };
thumbnail = [UIImage imageWithCGImage:image scale:scale orientation:UIImageOrientationUp];
return YES;

这和我们写MRC代码有点类似,对象创建以后就已经在考虑它需要释放的事情了.

它是怎么工作的呢? 我们添加了__attribute__((cleanup))属性,告诉编译器需要执行pspdf_defer_cleanup_block 这个方法当退出作用域的时候,然后我们构造了一个block作为了函数的参数,当pspdf_defer_cleanup_block 执行的时候就会调用我们创建的block,这样就做到了退出作用域清理资源的效果。

这个特性描述在GCC extension.

// Similar to defer in Swift
###define pspdf_defer_block_name_with_prefix(prefix, suffix) prefix ### suffix
###define pspdf_defer_block_name(suffix) pspdf_defer_block_name_with_prefix(pspdf_defer_, suffix)
###define pspdf_defer __strong void(^pspdf_defer_block_name(__LINE__))(void) __attribute__((cleanup(pspdf_defer_cleanup_block), unused)) = ^
###pragma clang diagnostic push
###pragma clang diagnostic ignored "-Wunused-function"
static void pspdf_defer_cleanup_block(__strong void(^*block)(void)) {
    (*block)();
}
###pragma clang diagnostic pop

不得不佩服iOS里面宏的魅力啊~.当看到这里的时候一脸懵逼,这是什么鬼?block声明你只给我写一个尖括号

展开以后就一目了然了:

###pragma clang diagnostic push
###pragma clang diagnostic ignored "-Wunused-function"
static void pspdf_defer_cleanup_block(__attribute__((objc_ownership(strong))) void(^*block)(void)) {
    (*block)();
}
###pragma clang diagnostic pop

__attribute__((objc_ownership(strong))) void(^pspdf_defer_27)(void) __attribute__((cleanup(pspdf_defer_cleanup_block), unused)) = ^ {
        _file = ((void *)0);
  };

还有一点值得注意的就是,同一个作用域的执行顺序:

    defer{
        NSLog(@"退出了1");
        defer{
            NSLog(@"退出了4");
            defer{
                NSLog(@"退出了5");
            };
        };
        defer{
            NSLog(@"退出了3");
        };
        NSLog(@"退出了2");
    };
    NSLog(@"退出了0");

输出的结果为0,1,2,3,4,5,同一个作用域它是逆序的.

Checked KeyPaths

当我们在使用Apple’s的APIs,你回发现很多地方需要去传字符串作为函数的参数.最平常的比如KVO、KVC了、AVFoundation或者其他APIs.这类stringly的api并不是安全的,他没有编译器的路径检查.swift3里面提供了###keypath关键字来保证安全性.swift4更是有了更加强大的KeyPaths。相比OC里面就缺乏这类的保护了.幸运的是,我们可以通过宏来让它变得更好.

###if DEBUG
###define KEYPATH(object, property) ((void)(NO && ((void)object.property, NO)), @###property)
###else
###define KEYPATH(object, property) @###property
###endif
NSString *key = KEYPATH(self, view.backgroundColor);
    NSLog(@"key:%@",key);

这个宏构建了一个简单的字符串,因此我们能够很快的使用它.另外DEBUG构建的时候我们会得到一个编译器的检查,如果view没有backgroundColor属性的时候,编译器将会得到一个很好的报错提示.

Boxing CGRect, CGPoint & co

之前,OC引入了字面量的概率,能够很好的去封装integers, enums and for any struct,构建它通过__attribute__((objc_boxable)).

typedef struct __attribute__((objc_boxable)) CGPoint CGPoint;
typedef struct __attribute__((objc_boxable)) CGSize CGSize;
typedef struct __attribute__((objc_boxable)) CGRect CGRect;
typedef struct __attribute__((objc_boxable)) CGVector CGVector;
typedef struct __attribute__((objc_boxable)) CGAffineTransform CGAffineTransform;
typedef struct __attribute__((objc_boxable)) UIEdgeInsets UIEdgeInsets;
typedef struct __attribute__((objc_boxable)) _NSRange NSRange;

使用它通过@()我们将快捷的构建出这些类型.

CGRect rect = CGRectMake(0, 0, 100, 50);
NSValue *boxedRect = @(rect);
NSLog(@"boxed: %@", boxedRect);

总结:程序里面的有些问题虽然可以通过程序员的约定俗成来解决,就像js语言,但是如果能在编译时候就发现一些代码漏洞,那么你的代码将会更加的健壮.

Generic Type

OC里面的泛型大多用在集合Array 、Dictionary 、Set 、HashTable 这些类,通过泛型来指定元素的类型,向集合里面插入元素的时候,编译器就会检查元素的类型,如果不匹配就会给出警告。同时,我们也能够自定义泛型类。

@interface Queue<ObjectType> : NSObject
- (void)enqueue:(ObjectType)value;
- (ObjectType)dequeue;
@end

这里我们创建了一个Queue的泛型类,使用ObjectType作为了集合里面的占位符.然后我们就可以把ObjectType作为类型用在属性,方法,成员变量中了.在创建对象时,如果指定了泛型类型,那么在具体使用过程中,如果违反了规则,编译器会给出警告,如下代码所示。不过仅此而已,在运行时,你依然可以传递其它类型的值。当然,如果创建对象时没有指定泛型类型,编译器也不会给出警告.

Queue<UIView *> *queue = [[Queue alloc] init];
[queue enqueue:view];
[queue enqueue:@2];		// Warning

注: __covariant 表示元素可以接受子类型

__contravariant 表示可以接受父类类型元素。