泛型 与 some(Opaque Type)
泛型:实质上就是不使用具体数据类型(例如 int、double、float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业务逻辑的实现。
在OC中泛型通常用于以下情况:
class MyContainer<T> { private var list: [T] init(){ list = [T]() } func append(_ el: T) { list.append(el) } func last() -> T? { list.last } } func runTest() -> Void{ let container = MyContainer<String>() for c in "abcd" { container.append(String(c)) } if let el = container.last(){ print("last: \(el)") }else{ print("last: nil") } }
而在Swift中泛型同样可以用于函数,如下:
func equal<T: Equatable>(left lv: T, right rv: T) -> Bool{ return lv == rv } func runTest() -> Void{ _ = equal(left: 1, right: 1) // Int _ = equal(left: 1.0, right: 1.0) // Double _ = equal(left: "abc", right: "abc")// String }
some(Opaque Type):不透明类型,它只能应用于“Any”、“AnyObject”、协议和/class类型,我们先不说它的作用,先看下面一个场景:
protocol Animal { func eat(food: String) func call() } struct Dog: Animal{ var name: String func eat(food: String) { print("\(name) 吃了 \(food)") } func call() { print("我是一只狗,我的名字叫:\(name)") } } struct Cat: Animal{ var name: String func eat(food: String) { print("\(name) 吃了 \(food)") } func call() { print("我是一只猫,我的名字叫:\(name)") } } struct Zoo{ func dog() -> Animal { Dog(name: "旺财") } func cat() -> Animal { Cat(name: "小花") } } func runTest() -> Void{ let zoo = Zoo() let dog = zoo.dog() dog.call() dog.eat(food: "骨头") let cat = zoo.cat() cat.call() cat.eat(food: "鱼") }
动物园里有小动物,猫和狗,从动物园中可以获取小动物,但是往往我们不希望返回具体的类,所以我们需要定义一个协议,从而返回这个协议即可,上面Zoo返回的小动物类型为Animal协议
这里都没问题,接下来的做法将会让你惊呆!
swift中可以在协议中添加关联类型,associatedtype:作为协议实现泛型的一种方式,可以在协议中预先定义一个占位符,实现协议的时候再确定这个占位符具体的类型。那么接下来我们在Animal协议中添加一个关联类型,用于定义动物的名字的类型。
protocol Animal { associatedtype NameType // 动物名字类型占位符,即:协议实现泛型的方式(我们可能不清楚这个动物的名字的类型是啥,所以这里需要定义一个泛型,让具体实现的动物类中自己决定) var name: NameType { get set } // 动物的名字属性,其类型为NameType泛型 func eat(food: String) func call() } struct Dog: Animal{ var name: String // 这里的name类型为String func eat(food: String) { print("\(name) 吃了 \(food)") } func call() { print("我是一只狗,我的名字叫:\(name)") } } struct Cat: Animal{ var name: [String] // 这里的name类型为[String],它可能有多个名字 func eat(food: String) { print("\(name.joined(separator: ",")) 吃了 \(food)") } func call() { print("我是一只猫,我的名字叫:\(name.joined(separator: ","))") } }
好了,经过上面的改造,我们会发现,Zoo类中的dog和cat两个方法报错了,错误为:Protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements,从错误原因我们感到很尴尬,就因为我们的协议Animal添加了associated type ,就不能用于返回值类型了。
why!why!why!
我们都知道swift是类型严格的开发语言,并且支持类型推断,让我们来分析一下上面的原因。首先Animal增加了关联类型,相当于定义了一个泛型占位符,这就导致Animal的类型是不确定的,既然它是类型不确定的,那就不能推断出它的类型是什么,所以swift不允许这样的事情发生。
那这种情况我们该怎么办呢?
写过SwiftUI的都看到过这样的代码:
struct TestContentView: View { var body: some View { // 这里的some Text("Hello") } }
计算型属性body的类型为 some标示的 View协议的类型,我们知道View协议中同样也定义了associated type ,于是我们修改Zoo的两个方法如下:
struct Zoo{ func dog() -> some Animal { Dog(name: "旺财") } func cat() -> some Animal { Cat(name: ["小花", "多啦A梦"]) } }
我们在两个方法的返回值类型前添加了some关键字修饰,结果编译通过,程序完美运行。
到此我们大概应该知道了这个some得作用,应该就是用来确定Animal的具体类型的,通过some修饰的协议类型,是一种固定的、确定的类型,从而支持类型推断,那么接下来我们验证一下这个想法。
struct Zoo{ enum AnimalType { case dog case cat } func animal(type: AnimalType) -> some Animal { if type == .dog { return Dog(name: "旺财") } return Cat(name: ["小花", "多啦A梦"]) } }
我们定义了一个animal的方法,通过传入动物的类型,返回对应的动物,此时你会看到编译错误:Function declares an opaque return type, but the return statements in its body do not have matching underlying types
从错误信息中可以看出,编译器无法推断出Animal的具体类型,因为在这个函数体内,增加了条件判断,返回了不同类型的动物,它无法推断出Animal的name属性到底是什么类型的。
到这里,我们可以知道,我们上面的推断是正确的。也就是说,some修饰的类型,必须在方法体内就是确认的、固定的,不能存在多种类型的关联类型。
针对这种情况我们在编写SwiftUI的时候,有些同学应该也遇到过,如下:
struct ContentView: View { var body: some View { if #available(iOS 14.0, *) { return LazyHStack(alignment: .center, spacing: 10) { Text("Lazy H Stack") } } else { return HStack(alignment: .center, spacing: 10) { Text("H Stack") } } } }
这里同样也报了错误:Function declares an opaque return type, but the return statements in its body do not have matching underlying types
解决办法就是要确保我们返回的是同一个类型,像ContentView,这里我们可以这样做:
struct ContentView: View { var body: some View { if #available(iOS 14.0, *) { return AnyView(LazyHStack(alignment: .center, spacing: 10) { Text("Lazy H Stack") }) } else { return AnyView(HStack(alignment: .center, spacing: 10) { Text("H Stack") }) } } }
我们在LazyHStack和HStack上包了一层AnyView,AnyView是A type-erased (类型擦除)view,将被包裹的视图的类型擦除掉,从而实现类型的统一。我们通过AnyView的原理,就可以解决我们的例子中的问题。
首选我们需要新建一个AnyAnimal的类,该类必须同样实现Animal协议
struct AnyAnimal: Animal{ private typealias Func1 = (String) -> Void private typealias Func2 = () -> Void private let eat: Func1 private let _call: Func2 var name: String = "AnyAnimal" init<T>(_ _animal: T) where T: Animal{ eat = { food in _animal.eat(food: food) } _call = { _animal.call() } } func eat(food: String) { eat(food) } func call() { _call() } }
然后调整我们Zoo的方法
struct Zoo{ enum AnimalType { case dog case cat } func animal(type: AnimalType) -> some Animal { if type == .dog { return AnyAnimal(Dog(name: "旺财")) } return AnyAnimal(Cat(name: ["小花", "多啦A梦"])) } }
返回的类型统一由AnyAnimal包装,这样编译通过,运行完美。好了,我通过举了一个动物园中的动物这个例子,介绍了泛型与some的区别,虽然有点牵强,但大体意思是表达出来了,大家可以慢慢体会。