在Swift 5.7中使用“some”和“any”关键字来引用范型协议

在Swift 5.7中使用“some”和“any”关键字来引用范型协议

将Swift灵活的泛型系统与面向协议的编程相结合,通常可以导致一些真正强大的实现,同时最大限度地减少代码重复,并能够在代码库中建立明确定义的抽象水平。然而,在Swift 5.7之前编写此类代码时,遇到以下编译器错误是很常见的:

ProtocolX‘ can only be used as a generic constraint because it
has Self or associated type requirements.
--
协议“X”只能用作范型约束,因为它
有Self或关联类型要求

让我们看看Swift 5.7(目前作为Xcode 14的一部分处于测试阶段)如何引入一些关键的新功能,旨在使上述错误成为过去。

不透明参数类型

就像我们在问与答的文章“为什么某些协议,如EquatableHashable,不能直接引用? ”中仔细研究一样,使用范型协议时经常遇到上述编译器错误的原因是,一旦协议定义了关联类型,编译器就会开始限制该协议的引用方式。

例如,假设正在开发一个处理各种组的应用程序,为了能够尽可能多地重用组处理代码,我们选择将核心的Group类型定义为范型协议,让每个实现类型定义它包含的Item值:

protocol Group {
    associatedtype Item

    var items: [Item] { get }
    var users: [User] { get }
}

现在,由于该关联类型Item,我们无法直接引用 Group协议——即使在与组的Items无关的代码中也是如此,例如计算从给定组的用户列表中显示哪些名称的函数:

// Error: Protocol ‘Group’ can only be used as a generic constraint
// because it has Self or associated type requirements.
func namesOfUsers(addedTo group: Group) -> [String] {
    group.users.compactMap { user in
        isUserAnonymous(user) ? nil : user.name
    }
}

当使用低于5.7的Swift版本时,解决上述问题的一种方法是让namesOfUsers函数范型化,然后按上述错误消息提示,仅使用Group协议作为范型约束——像这样:

func namesOfUsers<T: Group>(addedTo group: T) -> [String] {
    group.users.compactMap { user in
        isUserAnonymous(user) ? nil : user.name
    }
}

当然,这种技术没有问题,但与使用非范型协议或任何其他形式的Swift类型(包括具体的泛型类型)相比,它确实使函数声明变得更加复杂。

谢天谢地,Swift 5.7扩展了some关键字(在Swift 5.1中引入),使其也适用于函数参数,干净地解决了的此类问题。因此,就像声明SwiftUI视图从其body属性返回 some View 一样,现在也可以让 namesOfUsers函数接受some Group作为其输入:

func namesOfUsers(addedTo group: some Group) -> [String] {
    group.users.compactMap { user in
        isUserAnonymous(user) ? nil : user.name
    }
}

就像使用some关键字来定义不透明的返回类型一样(就像在构建SwiftUI视图时所做的那样),编译器将自动推断在每个调用点传递给函数的实际具体类型,而无需编写任何额外的代码。干脆利落!

主要关联类型

然而,有时,我们可能想对给定的参数添加一些要求,而不仅仅是要求它符合某个协议。例如,假设现在正在开发一个应用程序,允许用户将他们最喜欢的文章添加为书签,如下, bookmarkAritcles方法允许将文章数组传递到书签:

class BookmarksController {
    ...

    func bookmarkArticles(_ articles: [Article]) {
        ...
    }
}

然而,并非所有的调用点都可能使用数组存储文章。例如,以下ArticleSelectionController使用字典来跟踪在UITableViewUICollectionView中为哪个IndexPath选择的文章。因此,当将该文章集合传递到bookmarkArticles方法时,首先需要手动将其转换为数组——像这样:

class ArticleSelectionController {
    var selection = [IndexPath: Article]()
    private let bookmarksController: BookmarksController
    ...

    func bookmarkSelection() {
        bookmarksController.bookmarkArticles(Array(selection.values))
        ...
    }
}

如果 bookmarkArticles方法能够适用于任何包含文章值的集合不是更好吗?简单地将其参数类型更改为some Collection 行不通,因为这不足以指定正在寻找的具有特定Element类型作为输入的集合。

然而,可以再次使用一组范型类型约束来解决这个问题:

class BookmarksController {
    ...

    func bookmarkArticles<T: Collection>(
        _ articles: T
    ) where T.Element == Article {
        ...
    }
}

同样,这没有什么问题——但Swift 5.7再次引入了一种更轻量级的方式来表达上述声明,其工作方式与专门化具体的泛型类型(如Array<Article>)时完全相同。也就是说,现在可以通过在协议名称后的角括号中添加该类型,简单地告诉编译器我们的输入集合应包含什么Element类型:

class BookmarksController {
    ...

    func bookmarkArticles(_ articles: some Collection<Article>) {
        ...
    }
}

非常酷!我们甚至可以嵌套这些类型的声明——因此,如果我们想让我们的BookmarksController能够为符合范型ContentItem协议的任何类型的值添加书签,那么可以将some ContentItem指定为集合的预期Element类型,而不是使用具体的Article类型:

protocol ContentItem: Identifiable where ID == UUID {
    var title: String { get }
    var imageURL: URL { get }
}

class BookmarksController {
    ...

    func bookmark(_ items: some Collection<some ContentItem>) {
        ...
    }
}

上述工作要归功Swift 新特性主关联类型,以及Swift的Collection协议将Element声明为此类关联类型,例如:

protocol Collection<Element>: Sequence {
    associatedtype Element
    ...
}

当然,作为适当的Swift特性,我们也可以在自己的协议中使用主要关联类型(语法完全相同)。

存在的和“any”关键字

最后,让我们更进一步,将ArticleSelectionController转换为范型类型,可用于选择任何符合ContentItem的值,而不仅仅是文章。由于我们现在希望混合多个都符合同一协议的具体类型,some关键字不起作用——因为正如之前看到的,它的工作原理是让编译器为每个调用点推断单个具体类型,而不是多个类型。

这就是新的any关键字(在Swift 5.6中引入)的来源,它使我们能够将ContentItem协议称为存在的。现在,这样做确实有一定的性能和内存影响,因为它实际上是自动形式的类型擦除,但在动态存储异构元素集合的情况下,它非常有用。

例如,通过简单地使用any ContentItem作为selection字典的值类型,现在可以在该字典中存储符合该协议的任何值:

class ContentSelectionController {
    var selection = [IndexPath: any ContentItem]()
    private let bookmarksController: BookmarksController
    ...

    func bookmarkSelection() {
        bookmarksController.bookmark(selection.values)
        ...
    }
}

然而,上述更改确实引入了一个新的编译器错误,因为BookmarksController期望收到一个包含所有具有完全相同类型的值的集合——而在新的ContentSelectionControlle实现中并非如此。

谢天谢地,解决这个问题很简单,只需将一些some ContentItem替换为bookmark方法声明中的any ContentItem

class BookmarksController {
    ...

    func bookmark(_ items: some Collection<any ContentItem>) {
        ...
    }
}

甚至可以混合anysome引用,编译器将自动在两者之间进行翻译。例如,如果针对单元素重载bookmark方法,使其简单地调用第一个bookmark方法,可以这样做(即使第一个方法的items包含any ContentItem,第二个方法也接受some ContentItem):

class BookmarksController {
    ...

    func bookmark(_ items: some Collection<any ContentItem>) {
        for item in items {
            bookmark(item)
        }
    }

    func bookmark(_ item: some ContentItem) {
        ...
    }
}

同样,重要的是要强调,使用any确实在幕后引入类型擦除,即使这一切都是由编译器自动完成的——因此使用静态类型(使用some关键字时仍然如此)绝对是尽可能首选的方式。

结论

Swift 5.7不仅使Swift的泛型系统更强大,而且可以说也使其更易于访问,因为它减少了使用范型类型约束和其他更高级的范型编程技术以能够引用某些协议的需要。

泛型绝对不是解决每个问题的正确工具,但事实证明,能够以更轻量级的方式使用Swift泛型系统绝对是一个巨大的胜利。

感谢您的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值