什么时候需要使用泛型
在讲到泛型之前,先写一段代码(文中的代码都是Swift书写)。
func addTwoInt(_ a:Int,_ b:Int)->Int{
return a+b
}
这是一个很常见的也很简单的Int
类型的加法函数。函数 addTwoInt
将传入的两个参数 a
和b
的和返回。
再看一个函数:
func addTwoFloat(_ a:CGFloat,_ b:CGFloat)->CGFloat{
return a+b
}
这个函数是将两个 CGFloat
类型的和输出。
如果还需要写Double
类型的加法,按照上面的方式再写一个函数就可以了。面对越来越多的类型,需要写的函数也越来越多,幸好,我们能够使用+
操作符的类型并不多。尽管如此,这样的方式写代码我们依旧不能忍受。有什么办法让他们统一一下呢?
毕竟,这样一大堆类似的函数除了类型不一样,其余的操作都是完全同质的。
有两种方式可以帮助我们使用一个函数搞定上面说的所有的类型的加法。
-
使用Swift中的
Any
类型替代
我们可以定义以下函数来代替上面的几个类型的加法函数
func addTwoNum(_ a:Any,_ b:Any)->Any{
if let intA = a as? Int,let intB = b as? Int
{
return intA + intB
}
if let floatA = a as? CGFloat,let floatB = b as? CGFloat
{
return floatA + floatB
}
fatalError()
}
-
使用泛型
下面的代码使用泛型替代
func addTwo<T>(_ a:T,_ b:T)->T{
if let intA = a as? Int,let intB = b as? Int
{
return intA + intB as! T
}
if let floatA = a as? CGFloat,let floatB = b as? CGFloat
{
return floatA + floatB as! T
}
fatalError()
}
这两种方式都可以解决上面说的将多个除类型不同的同质化函数转化成一个。看起来似乎没什么不一样的地方。
其实不是的!看下面的解释
在Swift中,
Any
类型会避开编译器的类型检测,即使是我们输入了非数字类型调用addTwoNum
函数,或者我们返回的不是一个与输入参数同类型的返回值,编译器在编译的时候也不会报错。只有在运行时发现类型不对导致Crash。
而泛型自带了类型推断,也即是在编译过程中,会进行类型推断。
addTwo<T>
可以理解成一个函数族,编译器会识别其中的类型T
,后续的参数和返回值必须也是类型T
,编译才能通过。这样保证了函数使用的确定性。
这样的情况,当然是选择使用泛型嘛!
如果泛型只有这些简单的用处,那确实不怎么地,因为我们依然还时需要在addTwo<T>
中判断它的实际类型,不然我们并不能进行相应的操作,这里的操作是 +
。如何进行修改呢?
给泛型添加约束
在addTwo<T>
中,泛型T
目前来说只是一个位置的类型。任意类型都可以被做为参数带进来,我们在函数内部仅仅实现了Int
和CGFloat
的 +
操作。如果换成其他的类型,比如 String
,那将返回一个致命错误:fatalError()
。这种设计显然不合理。使用约束可以规定用户能够使用的类型。
所谓约束,并不是真正的约束,而是对泛型的可选范围进行调整。通常情况下,我们会使用需要泛型遵循必要的协议的方式实现。
一个例子:
func comparTwo<T:Comparable>( _ a:T,_ b:T) -> Bool {
return a>b
}
这是一个比较函数。规定了T
必须遵循Comparable
,Comparable
是Swift中自带的协议之一。只要遵循这个协议的类别,都可以使用>
操作符号进行比较, 返回一个布尔值。Int
,String
等都遵循了这个协议。
调用:
如果传入不遵循
Comparable
协议的类型的时候,比如NSArray
,在编译的时候就会报错。上图中。
除此之外,遵循了相应的协议,在comparTwo<T:Comparable>
的函数中,不必像addTwo<T>
一样需要进行类型判断,代码更加简洁。
Swift中一共有55个协议,并将让它们分成了三大类,有兴趣的可以看看这篇关于关于Swift的55个协议简介的文章。Swift中鼓励我们使用协议,所以很多人说这是面向协议编程,这里不讨论。
返回到addTwo<T>
中,我们需要一个可以进行加法的协议。目前我在Swift的协议中并没有找到。倒是在Int和CGFloat中看到它们分别重载了 +
,-
等等各种操作符。所以,使用加法协议看来是没有现成的了。但是我们可以自己写一套协议NewProtocol
,这套协议将定义如何将两个类型进行+
操作,然后在每个可能使用的类型中,重载相应的操作符实现。之后再让addTwo<T>
中的T
遵循这个协议。接下来就跟使用comparTwo<T:Comparable>
一样使用addTwo<T:NewProtocol>
。这已经 属于如何使用协议的范畴了,在这里也不再仔细讨论。
其实我们已经看出来了,在有已知的可用的协议的情况下,我们可以很方便的使用泛型做到将多个不同但类型同质化函数合并成一个函数。可如果现存的55个协议中,并没有我们想要的协议,那我们就需要自己定义协议,这跟写很多个函数一样,需要写不少东西。有没有什么办法,可以不使用协议而达到同样的目的呢,答案是有的!
泛型和高阶函数结合
上面提到了通过协议可以对一部分指定的类实现泛型编程。但是面对没有现成协议的时候,我们还可以配合高阶函数的使用来解决。
下面我通过另一种方式来实现:
func addTwo<T>( _ a:T,_ b:T,_ sideFun:(T,T)->(T)) -> T {
return sideFun(a,b)
}
调用:
let intA:Int = 5
let intB:Int = 15
let floatC:CGFloat = 0.6
let floatD:CGFloat = 1.62
print(self.addTwo(intA, intB) { (a, b) -> Int in
return a+b
}) // 20
print(self.addTwo(floatC, floatD) { (a, b) -> CGFloat in
return a+b
}) // 2.22
是不是简洁了很多倍? 并且这个函数不仅适用于Int
和CGFloat
,还是用于一个String
,Double
等各种类型。同时,仔细的你肯定发现了,这个函数不仅可以做加法,还可以做减法和其他更多的操作。我们看实际的调用:
// 针对String类型
let newString = self.addTwo("第一段字符", "第二段字符") { (a, b) -> String in
return a+b
}
print(newString) // 第一段字符第二段字符
// 减法
print(self.addTwo(intA, intB) { (a, b) -> Int in
return a-b
}) // -10
print(self.addTwo(floatC, floatD) { (a, b) -> CGFloat in
return a-b
}) // -1.02
看起来比使用协议的功能还要强大!但是这有个弊端就是:sideFun
需要使用者自己去实现,相应的多增加了使用者需要书写的代码,但是相对于这种方式带来的便利,这种弊端可以忽略不计吧。
到此,我们联想到了Swift中数组的一个函数:
public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]
这是数组的排序函数。跟addTwo
类似。但是sorted
函数将的实际参数是slef
本身(这是数组的扩展)。sorted
是Swift的一种泛型类型,跟addTwo
的T
一样。
总结一下,之前说的泛型可以代替一系列操作类似的类型。但是如果泛型和高阶函数一起使用,它则可以代替一系列类似的函数形式。
单纯的泛型,可以替代多种类型,进行同类操作。结合高阶函数,泛型可以将具有相同的函数形式多种操作合为为一,比如addTwo
,就是一种使用两个参数,结合一个函数参数,输出不同结果的函数形式。