泛型 与 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的区别,虽然有点牵强,但大体意思是表达出来了,大家可以慢慢体会。

 

posted @ 2021-06-24 20:10  zbblogs  阅读(535)  评论(0编辑  收藏  举报