【问题标题】:When to use type erasure in Swift?何时在 Swift 中使用类型擦除?
【发布时间】:2021-04-30 16:52:58
【问题描述】:

关于如何在 Swift 中进行类型擦除已经存在很多问题,而且我看到类型擦除经常被描述为处理具有关联类型和泛型类型的协议的重要模式。

然而,在我看来,需要类型擦除通常是设计问题的征兆——你本质上是在“丢弃”类型信息(即将值放入容器或将其传递给函数),这通常无论如何,最终都需要通过冗长而脆弱的向下转换来恢复。也许我不明白像AnyHashable 这样的“类型”的用例——带有self 的PAT/协议只能用作通用约束,因为它们不是具体类型,这让我想知道有什么令人信服的理由想要具体化它们。

简而言之,什么时候在 Swift 中使用类型擦除是一个的主意?我正在寻找一些关于何时使用此模式的一般指南,以及一些实际用例的示例,其中类型擦除比其替代方案更可取。

【问题讨论】:

标签: swift design-patterns type-erasure


【解决方案1】:

我试图找到一个简单的类型擦除示例。根据我的经验,它通常会变得更复杂,我会尽量避免它。但有时就是这样。

终于和以前一样复杂了,用的是老式的语言。除了旧式语言会因崩溃而伤害您,而 swift 在构建时会伤害您。

它是一种强类型语言,所以它不适合泛型。

假设您需要管理文档中的一些形状。 这些形状是Identifiables,这意味着它们有一个id,其类型由关联的类型确定。在这种情况下为 Int。

下面的代码不会编译,因为它不能直接使用Shape 协议,因为id 的类型是由符合Shape 协议的对象定义的关联类型

import Foundation

protocol Shape: AnyShape, Identifiable {
    var name: String { get }
}

struct Square: Shape {
    var id: Int = 0
    var name: String { "Square" }
}

func selectShape(_ shape: Shape) {
    print("\(shape.name) selected")
}

通过添加类型擦除形状,您可以将其传递给函数。 因此,这将构建:

import Foundation

protocol AnyShape {
    var name: String { get }
}

protocol Shape: AnyShape, Identifiable {
    
}

struct Square: Shape {
    var id: Int = 0
    var name: String { "Square" }
}

func selectShape(_ shape: AnyShape) {
    print("\(shape.name) selected")
}

简单的用例。

假设现在我们的应用连接到两个形状制造商的服务器以获取他们的目录并与我们的同步。

我们知道世界各地的形状就是形状,但是数据库中的ACME Shape Factory索引是Int,而Shapers Club使用UUID ..

正如你所说,此时我们需要“恢复”类型。 这正是查看 AnyHashable 源文档时所解释的内容。

Cast 无法避免,对于应用程序的安全性和模型的稳固性来说,这最终是一件好事。

第一部分是协议,随着情况的增多,可能会比较冗长复杂,但是会在app的通信基础框架中,不应该经常变动。

import Foundation

// MARK: - Protocols

protocol AnyShape {
    var baseID: Any { get }
    var name: String { get }
}

// Common functions to all shapes

extension AnyShape {
    
    func sameObject(as shape: AnyShape) -> Bool {
        switch shape.baseID.self {
        case is Int:
            guard let l = baseID as? UUID , let r = shape.baseID as? UUID else { return false }
            return l == r
        case is UUID:
            guard let l = baseID as? UUID , let r = shape.baseID as? UUID else { return false }
            return l == r
        default:
            return false
        }
    }

    func sameShape(as shape: AnyShape) -> Bool {
        return name == shape.name
    }

    func selectShape(_ shape: AnyShape) {
        print("\(shape.name) selected")
    }
}

protocol Shape: AnyShape, Identifiable {
    
}

extension Shape {
    var baseID: Any { id }
}

第二部分是模型 - 随着我们与更多的形状制造商合作,这有望发展。

可以对形状进行的敏感操作不在此代码中。所以创建和调整模型和 api 没有问题。

// MARK: - Models

struct ACME_ShapeFactory_Model {
    struct Square: Shape {
        var id: Int = 0
        var name: String { "Square" }

        var ACME_Special_Feature: Bool
    }
}

struct ShapersClub_Model {
    struct Square: Shape {
        var id: UUID = UUID()
        var name: String { "Square" }

        var promoCode: String
    }
}

测试

let shape1: AnyShape = ACME_ShapeFactory_Model.Square()
let shape2: AnyShape = ShapersClub_Model.Square()
let shape3: AnyShape = ShapersClub_Model.Square()

Compare two different shapes references from different manufacturers

shape1.sameObject(as: shape2) : false
-> Logic, it can't be the same item if it comes from different manufacturers

Compare two different shapes references from same manufacturers

shape2.sameObject(as: shape3) : false
-> This is a new shape, sync in catalog

Compare two identical shapes references from same manufacturers 

shape2.sameObject(as: shape2) : true
-> We already have this one in the catalog

Compare two shapes from different manufacturers

shape1.sameShape(as: shape2) : true
-> Dear customer, we have two kind of squares from two manufacturers

就是这样。我希望这可能会有所帮助。 欢迎任何更正或评论。

最后,我为我的 Shapes 制造商的名字感到非常自豪:)

【讨论】:

  • 哈哈,ACME Shape Factory 和Shapers Club 太棒了!不过,对于这个形状示例,我会考虑使用“求和类型”模式。考虑:struct ACMEShape { ... } struct ShapersClubShape { ... } enum Shape { case acme(ACMEShape) case shapersClub(ShapersClubShape) var baseID: Any { switch(self) { ... } } ... } 这样做的好处是允许“穷举切换”,消除了强制转换的需要,并且无需区分ShapeAnyShape。您认为类型擦除方法有什么优势?你喜欢什么时候?
  • 是的,这也是一种有效的方法。首先,我会考虑structs 而不是enum,因为structs 可以在swift 中用作枚举(太棒了!),但具有在源代码之外可扩展的优势。它使架构更具可扩展性。然后,做一个switch(self) 是一种铸造。老实说,我无法在 cmets 中回答您的问题。也许我没有正确的答案。有很多关于这个主题的文章。我认为最好的方法是尝试。我个人选择总是先尝试强类型、少数泛型和不擦除类型。然后我看到了…… 8)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-06-24
  • 1970-01-01
  • 1970-01-01
  • 2017-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多