iOS 强大的泛型

ManoBoo撸了一个月小程序,感觉身体已经被掏空,各种兼容问题,2333,不闲扯了,其实这篇文章早已写完,一直没有时间校正,终于等小程序上线完了(以后尽量保持每月一篇的频率),这次说一说泛型,如果错误请指出,欢迎拍砖交流~


文章围绕这五点:

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将方法归类给CIImageKit类中,同样也可以对UIButton进行扩展

extension UIButton: CIImageDownloaderProtocol {}
extension CIImageKit where Base: UIButton {
    func setImage(url: URL, placeHolder: UIImage?) {
        // 实现 下载图片并缓存、展示的逻辑
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容