1. 程式人生 > >iOS 強大的泛型,同樣也可以對UIButton進行擴展

iOS 強大的泛型,同樣也可以對UIButton進行擴展

pub 來講 end ast welcom sign sig 應用 技術

文章圍繞這五點:

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;

也不會有這樣的類型警告了

技術分享

使用__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類型或者其他更多的類型呢?可能會寫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將方法歸類給CIImage類中,同樣也可以對UIButton進行擴展

extension UIButton: CIImageDownloaderProtocol {}

extension CIImageKit where Base: UIButton {

func setImage(url: URL, placeHolder: UIImage?) {

// 實現 下載圖片並緩存、展示的邏輯

}

}

iOS 強大的泛型,同樣也可以對UIButton進行擴展