ManoBoo撸了一个月小程序,感觉身体已经被掏空,各种兼容问题,2333,不闲扯了,其实这篇文章早已写完,一直没有时间校正,终于等小程序上线完了(以后尽量保持每月一篇的频率),这次说一说泛型,如果错误请指出,欢迎拍砖交流~
文章围绕这五点:
1. 泛型是什么
2. 为什么要用泛型
3. 泛型怎么用
4. 泛型进阶
5. 泛型的延伸使用
泛型(Generics)是什么?
引用Apple
中Generics
的描述:
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
中的Array
和Dictionary
都是基于泛型编写的集合类型,如果不太理解也没关系,下面讲几个例子理解下。
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;
也不会有这样的类型警告了
2. Swift中的泛型
从The Swift Programming Language
的例子开始讲起
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数作用是为了交换两个Int
整型值,但是想象一下,这段代码只能做到交换整型值吗,那我们想交换两个String
类型或者其他更多的类型呢?可能会写swapTwoStrings
、swapTwoDoubles
,如何如何将交换两个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
中的一个例子,如何用泛型实现一个栈,这时用泛型就可以轻松地实现pop
和push
,免去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
中泛型应用地很广泛,常见的Array
、Dictionary
等都支持泛型,使用如下:
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"]];
但是扩展很多的情况下,我们可能会重复命名,可能不知道该方法属于哪个库,得益于Swift
中extension
优秀的扩展能力,我们可以很好的避免这个问题,代码如下:
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?) {
// 实现 下载图片并缓存、展示的逻辑
}
}
- SOLID - 面向对象编程及面向对象设计的五大基本原则 中对于
L
(里氏替换原则)的解释