iOS 强大的泛型

iOS 强大的泛型

  1. 泛型是什么
  2. 为什么要用泛型
  3. 泛型怎么用
  4. 泛型进阶
  5. 泛型的延伸使用

泛型(Generics)是什么?

引用AppleGenerics 的描述:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.
Generics are one of the most powerful features of Swift, and much of the Swift standard library is built with generic code. In fact, you’ve been using generics throughout the Language Guide, even if you didn’t realize it. For example, Swift’s Array and Dictionary types are both generic collections. You can create an array that holds Int values, or an array that holds String values, or indeed an array for any other type that can be created in Swift. Similarly, you can create a dictionary to store values of any specified type, and there are no limitations on what that type can be.

大意是讲:
泛型可以让你使用定义的类型来编写灵活的、可重用的函数和类型,可以避免重复,以清晰,抽象的方式表达其意图。用人话来说(😔),泛型给予我们更抽象的封装函数或类的能力,不严谨的来讲,一门语言越抽象使用越方便。Swift中的ArrayDictionary都是基于泛型编写的集合类型,如果不太理解也没关系,下面讲几个例子理解下。

1. Objective-C中的泛型

在2015年WWDC上苹果推出了Swift 2.0版本,为了让开发者从Objective-C更好得过渡到Swift上,苹果也为Objective-C带来了Generics泛型支持

Generics. Allow you to specify type information for collection classes like NSArray, NSSet, and NSDictionary. The type information improves Swift access when you bridge from Objective-C and simplifies the code you have to write.

所以我们经常看到的OC中的泛型比如:

// 实例化一个元素类型为`NSString`的数组
NSArray <NSString *> *array = [NSArray new];
// 或者字典
NSDictionary <NSString *, NSNumber *> *dict = @{@"manoboo": @1}

或者:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}

我们先看看OC中的泛型大概做了些什么:
打开NSArray.h 我们可以看到:

@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property (readonly) NSUInteger count;
- (ObjectType)objectAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObjects:(const ObjectType _Nonnull [_Nullable])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

声明一个Generics的格式如下:

@interface 类名 <占位类型名称>
@end

占位类型后也可以加入类型限制,比如:

@interface MBCollection <T: NSString *>
@end

若不加入类型限制,则表示接受id即任意类型。我们先看看一个简单使用泛型的例子:

@interface MBCollection<__covariant T>: NSObject

@property (nonatomic, readonly) NSMutableArray <T> *elements;

- (void)addObject:(T)object;

- (BOOL)insertObject:(T)object atIndex: (NSUInteger)index;

@end

其中T为我们提前声明好的占位类型名称,可自定义(如ObjectType等等),需注意的是该T的作用域只限于@interface MBCollection@end之间,至于泛型占位名称之前的修饰符则可分为两种:__covariant协变)和__contravariant逆变

两者的区别如下:
__covariant意为协变,意思是指子类可以强制转转换为(超类)父类,遵从的是 SOLID 中的L里氏替换原则 ,大概可以描述为: 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的[1]
__contravariant意为逆变,意思是指父类可以强制转为子类
用我们上面自定义的泛型来解释:

MBCollection *collection;
MBCollection <NSString *> *string_collection;
MBCollection <NSMutableString *> *mString_collection;
        
collection = string_collection;
string_collection = collection;
collection = mString_collection;

默认不指定泛型类型的情况下,不同类型的泛型可以互相转换。
这个时候就可以在占位泛型名称前加入修饰符__covariant__contravariant来控制转换关系,像NSArray就使用了__covariant修饰符。
引申:
在上面这个例子中,声明属性时,还可以在泛型前添加__kindof关键词,表示其中的类型为该类型或者其子类,如:

@property (nonatomic, readonly) NSMutableArray <__kindof T> *elements;

之后就可以这样调用了

MBCollection <NSString *> *string_collection;
NSMutableString *str = string_collection.elements.lastObject;

也不会有这样的类型警告了
使用__kindof消除类型警告.png

2. Swift中的泛型

The Swift Programming Language 的例子开始讲起

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

这个函数作用是为了交换两个Int整型值,但是想象一下,这段代码只能做到交换整型值吗,那我们想交换两个String类型或者其他更多的类型呢?可能会写swapTwoStringsswapTwoDoubles,如何如何将交换两个Int值进化为交换两个同类型的值呢?怎么去封装可以让函数更抽象支持更多的类型,因此就诞生了泛型,可以看出使用泛型可以更好地、更抽象地扩大该方法的作用域

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

1. Class / Struct / Enum + 泛型

依旧用 The Swift Programming Language 中的一个例子,如何用泛型实现一个栈,这这时用泛型就可以轻松地实现poppush,免去Swift中的类型转换。

struct Stack<T> {
    
    private var collection = Array<T>.init()
    // private var colection = [T]()  等同于上方
    var maxLength: Int = 10
    
    var topElement: T? {
        return collection.isEmpty ? nil: collection.last
    }
    
    // 声明可以忽略返回结果
    @discardableResult
    mutating func push(_ object: T) -> Bool {
        if collection.count < maxLength {
            collection.append(object)
            return true
        }else {
            return false
        }
    }
    
    @discardableResult
    mutating func pop() -> T {
        return collection.removeLast()
    }
}


// 调用
var stack = Stack<String>.init()
stack.push("mano")
stack.push("boo")
stack.push("welcome")
stack.pop()
if let topElement = stack.topElement {
    print("the top element is \(topElement)")
}

我们使用泛型定义了一个Stack栈,栈中的元素类型为T,的容量为10个元素,实现了最简单的入栈出栈的功能,在T泛型后也可以限定泛型的class或者遵从的protocol,如struct Stack<T: NSString>struct Stack<T: Hashable>struct Stack<T: Equatable>
Swift中泛型应用地很广泛,常见的ArrayDictionary等都支持泛型,使用如下:

    var dict: Dictionary<String, Any>
    var dict: [String: Any] // 与上面的功能一样,只是语法糖的简写
    
    var arr: [String]
    var arr: Array<String>

再举一个例子[2]

// 模仿 Dictionary 自定义一个泛型字典
struct GenericsDictionary<Key: Hashable, Value> {
    private var data: [Key: Value]
    
    init(data:[Key: Value]) {
        self.data = data
    }
    
    subscript(key: Key) -> Value? {
        return data[key]
    }
}

使用:

let genericsDict = GenericsDictionary.init(data:["name": "manoboo", "age": 24])
let name = genericsDict["name"]
let age = genericsDict["age"]
// 此时 age 的类型为 Any?

Swift 4.0中给subscript方法带来了泛型支持,所以我们可以这样写:

subscript<T>(key: Key) -> T? {
    return data[key] as? T
}

使用:

let name: String? = genericsDict["name"]
let age: Int? = genericsDict["age"]

2. protocol + 泛型

前面介绍了Swift中常见的泛型使用情景,下面看看如何使用protocol配合泛型封装一个小型的图片请求扩展库模板
在OC中我们常见的第三方库基本都是使用category扩展原先的类,比如 SDWebImage的一个例子:

UIImageView *imageView = [[UIImageView alloc] init];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
             placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

但是扩展很多的情况下,我们可能会重复命名,可能不知道该方法属于哪个库,得益于Swiftextension优秀的扩展能力,我们可以很好的避免这个问题,代码如下:

public final class CIImageKit<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

// protocol中 需要用 associatedtype 来预设一个类型
public protocol CIImageDownloaderProtocol {
    associatedtype type
    var ci: type { get }
}

public extension CIImageDownloaderProtocol {
    public var ci: CIImageKit<Self> {
        get {
            return CIImageKit(self)
        }
    }
}

我们声明了一个CIImageDownloaderProtocol协议,对于遵从了该协议的类,都有一个CIImageKit类型的对象,下面我们扩展UIImageView

extension UIImageView: CIImageDownloaderProtocol {}
extension CIImageKit where Base: UIImageView {
    func setImage(url: URL, placeHolder: UIImage?) {
        // 实现 下载图片并缓存、展示的逻辑
    }
}

这就是一个基本的第三方库封装的模版,如何调用呢?

let image = UIImageView()
image.ci.setImage(url: URL.init(string: "https://www.manoboo.com")!, placeHolder: nil)

我们通过中间一层protocol将方法归类给CIImage类中,同样也可以对UIButton进行扩展

extension UIButton: CIImageDownloaderProtocol {}
extension CIImageKit where Base: UIButton {
    func setImage(url: URL, placeHolder: UIImage?) {
        // 实现 下载图片并缓存、展示的逻辑
    }
}
Comments
Write a Comment