Swift协议与关联类型

发表于 2年以前  | 总阅读数:438 次

Swift协议与关联类型

目录

  • 前言

  • 问题

  • 关联协议的限制

  • 使用关联协议需要做泛型改造

  • 使用关联协议失去了动态类型派发的能力

  • 关联协议与泛型的关系

  • 解决问题的方案

  • 组合方案

  • 添加泛型函数

  • 为关联类型添加约束

  • 结语

  • 参考

前言

在Swift语言当中,泛型(Generic)和协议(Protocol)都是非常重要的语言特性。使用泛型让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可以避免编写重复的代码,而是用一种清晰抽象的方式来表达代码的意图;使用协议能够让你设计一个蓝图,遵循协议的具体类型,帮助你实现某一特定的任务或者功能的方法、属性,特别是协议可以作为类型使用,使其具有了动态派发的能力;本文将讨论Swift协议(Protocol)中特殊的关联类型(Associated Types),它与泛型(Generic)有相似性和又有区别。为了简化文字描述,后续将带有关联类型的协议(Protocol With Associated Types),简称为关联协议;而把普通的不包含任何关联类型的协议(Plain Protocol)简称为普通协议

问题

我们将首先讨论一个业务开发中的具体问题,定制UITabbar和UITabBarController。

图1 定制UITabbar元素示意图

有两种TabBarItem类型,一种是SNSTabBarItem,其中ImageView是图片类型;另一种是SNSTabBarLotItem,其中ImageView是LottieView,即动画类型;为了通用化设计,统一属性名称和调用流程,我们考虑通过设计协议来解决这个问题。

协议代码如下所示:

public protocol SNSTabBarItemProtocol {
    var itemLabel:UILabel! { get }
    associatedtype itemImageViewType:UIView
    var itemImageView:itemImageViewType! { get }

    //创建TabBarItem内部UI元素
    func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage?)

    //.......
}

其中,使用了关键字associatedtype,定义了一个关联类型;满足实际使用中,不同类型的TabBarItem中ImageView类型的变化,同时,又对其增加的限制,要求ImageView必须是继承UIView的子类。另外,定义了createElement函数,它会在自定义的CustomTabBarController中被调用,不同类型item其内部实现不同,满足不同UI布局的定制需求。

实现协议的两种TabBarItem类型:

//第一种,SNSTabBarItem
class SNSTabBarItem: UITabBarItem, SNSTabBarItemProtocol {
    var itemLabel:UILabel!
    var itemImageView:UIImageView! //静态图片类型

    func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage? = UIImage()){
        //......内部实现不同
    }
}
//第二种,SNSTabBarLotItem
class SNSTabBarLotItem: UITabBarItem, SNSTabBarItemProtocol {
    var itemLabel:UILabel!
    var itemImageView:HYLotSwitchView! //Lottie动画类型

    func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage? = UIImage()){
        //......内部实现不同
    }
}

接下来,在自定义CustomTabBarController中创建所有item,并调用协议中定义的方法createElements

// 创建自定义图标    
func createCustomIcons(_ containers: [String:UIView]) {
    guard let items = self.tabBar.items, !items.isEmpty,!containers.isEmpty else{
        return
    }
    let barItemWidth: CGFloat = self.tabBar.bounds.size.width / CGFloat(items.count)

    for item in items {   //遍历元素
        //..........
        if let item = item as? SNSTabBarItemProtocol {  //错误,Protocol 'SNSTabBarItemProtocol' can only be used as a generic constraint because it has Self or associated type requirements
            item.createElements(superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: self.tabBar.bounds.size.height), backgroundImage: nil)
            //..............
        }
        //..........
    }
}

代码中if let item = item as? SNSTabBarItemProtocol这里会遇到一个致命问题Protocol 'SNSTabBarItemProtocol' can only be used as a generic constraint because it has Self or associated type requirements,这是什么原因呢?我们通过简单分析这个错误提示,可以得出一些线索:带有associatedtype的关联协议只能修饰泛型,这与普通协议相比带来了明显的差异和使用限制

关联协议的限制

使用关联协议需要做泛型改造

我们来回顾一下Swift官方文档关于协议作为类型(Protocols as Types)的描述:

Protocols as TypesProtocols don’t actually implement any functionality themselves. Nonetheless, you can use protocols as a fully fledged types in your code. Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”.

❝You can use a protocol in many places where other types are allowed, including:

  • As a parameter type or return type in a function, method, or initializer
  • As the type of a constant, variable, or property
  • As the type of items in an array, dictionary, or other container

挑重点进行说明,协议本身实际上并不实现任何功能,但是你可以在代码中使用协议作为完善的类型

这说明我们前面问题中的使用方法针对普通协议是正确的,而针对关联协议就不再正确;从错误提示可以得到答案,Protocol 'xxx' can only be used as a generic constraint because it has Self or associated type requirements关联协议只能修饰泛型

通过一个具体的例子来验证:

protocol Proto{
}

var delegate:Proto

这段代码运行正常,接下来改造一下,增加associatedtype关联类型

protocol Proto{
    associatedtype T
}

var delegate:Proto //Protocol 'Proto' can only be used as a generic constraint because it has Self or associated type requirements

改为关联协议后就会出现与前面例子相似的错误,那么我们来引入泛型进行修改。

protocol Proto{
    associatedtype T
}

class C<T:Proto> {
    var delegate:T

    init() {
    }
}

运行正确,从这里我们可以得到结论,每一个从前使用普通协议的的地方,现在为了使用associatedtype,需要进行改造,引入泛型,使用关联协议修饰泛型参数,就能够避免产生错误。

使用关联协议失去了动态类型派发的能力

但是,改造后class C变成一个泛型类,带有泛型TT遵循Proto协议,然后在C内部,delegate的类型是T,也就是说原本一个普通的class类型,需要被改造成泛型class,很多时候这不是我们设计的本意,而存粹是为了支持使用associatedtype,不得不进行的改造。这样失去了dynamic dispatch动态类型派发的能力!

比如有一个数组,其内部存储的类型是不同的,但是遵循相同的协议,这在使用普通协议时,是可行的,而使用带有associatedtype的关联协议就不可行了,失去了动态派发的能力,多态的能力,只能变成一个统一的类型,而不能支持不同类型 ,因此我们失去了一个重要的语言特性。

关联协议与泛型的关系

关联协议,从外部看,使用associatedtype更像是提供了一个语法糖,提供有意义的名字做占位;从内部看,建立类型的语意要求,使用typealias显示或者隐式指明具体类型;利用associatedtype相当于定义了一个未知类型的占位符,并且这个占位符可以在协议定义的整个生命周期内使用。

我们来对比两段代码:

protocol Animal{
    associatedtype Food
    func eat(food:Food) 
}

协议Animal定义了每种动物要eat某种类型的Food,到现在为止,我们还不知道哪种动物吃哪种Food

struct Animal<Food>{
    func eat(food:Food) 
}

Animal结构体,支持泛型参数Food,定义每种动物eat某种类型的Food

这种场景下,使用关联协议和使用泛型参数作用非常相似, 但是他们之间仍然不完全相同。

由于目前的语言限制,协议中无法使用泛型,我们假设可以使用泛型协议,写出类似下面的代码,然后与使用关联协议的代码进行对比分析:

//假设泛型协议成立
protocol Animal<Food> {   
    func eat(food:Food)
}

struct Grass:Food{
}

struct Cow:Animal<Grass>{  //泛型参数指定具体遵循协议的类型
    func eat(f: Grass) {
    }
}
//使用关联协议
protocol Animal{
    associatedtype Food
    func eat(food:Food) 
}
struct Cow:Animal{ 
    func eat(f: Food) {
        Self.Food  //通过类似属性的方式,直接获取到关联类型的名字
    }
}

从外部看,我们使用泛型协议方式,只能看到遵循协议的具体类型,即Grass是一个遵循Food协议的类型;对比使用associatedtype的关联协议,我们可以通过类似属性的方式,可以直接获取到关联类型的名字,这使得某些情况下,添加参数类型的约束限制成为可能。还不止于此,如果有多个关联类型,或者关联类型需要被其他关联协议限制,或者同时使用多个协议,这些复杂的情况组合,就使得假设的泛型协议很难代替关联协议,并且泛型协议不得不把这些(原本可以通过associatedtype隐藏在内部的)信息全部暴露给外部使用者。

另外,关联协议利用associatedtype解决的问题是面向对象的类型关系继承,来看下面例子:

protocol Food{   
}

struct Grass:Food{  
}

protocol Animal {
    func eat(f:Food)
}

struct Cow:Animal {
    func eat(f: Grass) {  //Type 'Cow' does not conform to protocol 'Animal'
    }
}

首先定义了Food协议,Grass作为一种具体的食物遵循Food协议;另外,我们通过Animal协议,规范动物需要eat食物Food,具体是哪种Food没有确定,最后Cow作为一种具体的动物,遵循Animal协议,实现了eat方法,参数指定Grass类型,Grass遵循Food协议,但是编译器提示错误,Cow没有遵循Animal协议,只能改为func eat(f: Food)

我们可以发现:遵循普通协议的具体类型,其内部遵循的协议类型不能捕获复杂的类型关系

接下来改造Animal协议为关联协议

protocol Food{
}

struct Grass:Food{
}

protocol Animal {
    associatedtype FoodType //关联类型
    func eat(f:FoodType)
}

struct Cow:Animal {
    func eat(f: Grass) { //Grass遵循Food协议,OK
    }
}

Cow().eat(f: Grass())

有了associatedtype的帮助,可以完成面向对象的类型继承关系使用。

解决问题的方案

现在我们讨论文章开头提出的的问题如何解决,有两种方案可供参考:

组合方案

typealias Codable = Decodable & Encodable

我们经常使用Codable协议进行数据序列化,这里可以参考Codable的设计模式,采用组合方案;

SNSTabBarItemProtocol协议拆分成两个协议:

//协议只包含需要遵守的属性
public protocol SNSTabBarItemElements{
    var itemLabel:UILabel! { get }
    associatedtype itemImageViewType:UIView
    var itemImageView:itemImageViewType! { get }
}
//协议只包含需要遵守的方法
public protocol SNSTabBarItemFunctions{
    //创建TabBarItem内部UI元素
    func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage?)
}
//协议组合
public protocol SNSTabBarItemProtocol: SNSTabBarItemElements & SNSTabBarItemFunctions {
}

经过这样改造之后,我们修改调用处协议:

// 创建自定义图标    
func createCustomIcons(_ containers: [String:UIView]) {
    //..........
    for item in items {   //遍历元素
        //..........
        if let item = item as? SNSTabBarItemFunctions {  //OK,不再报错,避开了关联协议问题
            item.createElements(superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: self.tabBar.bounds.size.height), backgroundImage: nil)
            //..............
        }
        //..........
    }
}

原来的转换为SNSTabBarItemProtocol协议的方式,更改为使用SNSTabBarItemFunctions这个子协议,而两个具体的UITabBarItem子类仍然遵循SNSTabBarItemProtocol协议,保持不变;这样通过组合的方式,我们绕开了关联协议只能修饰泛型的问题,把它变成了当前场景下只使用普通协议,调用协议内限定的函数;

添加泛型函数

既然关联协议只能用作泛型约束,因为它有关联类型要求,那么我们是否可以选择另一个方案:创造一个泛型函数,封装createElements的调用并添加参数的泛型约束,我们来试试:

//item改为泛型参数,遵守SNSTabBarItemProtocol协议
func loopElements<E:SNSTabBarItemProtocol >(item:E,superView:UIView,position: CGRect,backgroundImage: UIImage?){
    item.createElements(superView: superView, position: position, backgroundImage: backgroundImage)
}
//遍历元素内部使用loopElements方法
func createCustomIcons(_ containers: [String:UIView]) {
    //..........
    for item in items {   //遍历元素
        //..........
        loopElements(item: item, superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: 0), backgroundImage: nil)      //错误,Global function 'loopElements(item:superView:position:backgroundImage:)' requires that 'UITabBarItem' conform to 'SNSTabBarItemProtocol'
        //..........
    }
}

修改遍历元素内部的代码,调用loopElements泛型函数,确实满足了关联协议的要求。

但是,新的问题会产生,调用loopElements会提示 Global function 'loopElements(item:superView:position:backgroundImage:)' requires that 'UITabBarItem' conform to 'SNSTabBarItemProtocol' ,这是因为tabbar中的items数组元素,类型只能是UITabBarItem,不能添加SNSTabBarItemProtocol的限制,因为SNSTabBarItemProtocol是关联协议,只能修饰泛型,items中数组元素不是泛型,因此,这个方案不能继续推进成功。

为关联类型添加约束

接下来我们跳出问题本身,继续讨论为关联协议中的关联类型添加约束;它可以进一步来要求遵循的类型满足约束,这在很多场景下具有很多实际价值,能够抽象代码避免重复。

例如,下面的代码定义了MySequence协议,MySequence协议遵循Comparable协议,其中的关联类型Element也遵循Comparable协议。

protocol MySequence: Comparable {
    associatedtype Element: Comparable
    var storage: [Element] { get set }
}

由于对 Element 添加了协议限制,Comparable 协议需要实现的比较方法就可以给出实现;

extension MySequence {
    static func < (lhs: Self, rhs: Self) -> Bool {
        for (left, right) in zip(lhs.storage, rhs.storage) {
            if left < right {
                return true
            }
        }
        return false
    }
}

另外,我们也可以在关联类型约束里使用协议,用 where 从句实现更复杂的约束;

protocol MySequence: Comparable {
    associatedtype Element: Comparable
    associatedtype Slice: MySequence where Slice.Element == Element
    var storage: [Element] { get set }
}

这里协议可以作为它自身的要求出现,即Slice拥有两个约束,它必须遵循 MySequence 协议,同时它的Element的类型必须是和storage数组中元素Element类型相同。

我们也可以为关联类型添加默认值,如下面所示Element默认为Int类型:

protocol MySequence: Comparable {
    associatedtype Element: Comparable = Int
    var storage: [Element] { get set }
}

并且可以为为默认的 Associated Type 提供方法的默认实现。

protocol MySequence4: Comparable {
    associatedtype Element: Comparable = Int
    var storage: [Element] { get set }

    func summed() -> Element
}

Element 现在默认是 Int,接下来通过extension给出函数summed的默认实现。

extension MySequence {
    func summed() -> Element {
        return storage.reduce(0, +) as! Self.Element //Cannot convert value of type '(Int) -> Int' to expected argument type '(Int, Self.Element) throws -> Int'
    }
}

但是此处会提示错误,无法推断出默认类型是Int,即 extension 中的 Element 只受“约束”的影响,即只受 Comparablewhere 从句的影响,并没有接受默认值。所以我们需要针对extension增加限制。

extension MySequence where Element == Int {
    func summed() -> Element {
        return storage.reduce(0, +)
    }
}

只有满足Element类型是Int的,才能使用summed的默认实现。这样就可以保证准确。

结语

本文从业务场景的实例出发,详细讨论了使用关联类型的协议可能会出现的问题,并且对比了与普通协议的不同;我们可以看到关联协议更类似范型参数;如果要使用关联类型的协议,就必须进行范型改造,这种方式使得类型失去了动态派发的能力,需要根据具体情况合理选择。另外,我们也详细介绍了如何为关联类型添加约束,通过添加约束可以实现更复杂的要求,如添加默认类型和默认类型的方法实现,优化代码设计方式,避免重复。

参考

  • https://betterprogramming.pub/swift-protocols-with-associated-types-and-generics-373b2927baed
  • https://zhuanlan.zhihu.com/p/80672557
  • https://www.hackingwithswift.com/example-code/language/how-to-fix-the-error-protocol-can-only-be-used-as-a-generic-constraint-because-it-has-self-or-associated-type-requirements

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Rglm0aJny-WO3zYd9ibqZQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237270次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录