协议只能作为通用约束使用,因为它具有Self或关联类型要求。

158

我有一个名为RequestType的协议,它有一个关联类型Model如下:

public protocol RequestType: class {

    associatedtype Model
    var path: String { get set }

}

public extension RequestType {

    public func executeRequest(completionHandler: Result<Model, NSError> -> Void) {
        request.response(rootKeyPath: rootKeyPath) { [weak self] (response: Response<Model, NSError>) -> Void in
            completionHandler(response.result)
            guard let weakSelf = self else { return }
            if weakSelf.logging { debugPrint(response) }
        }
    }

}

现在,我正试图将所有失败请求排成队列。

public class RequestEventuallyQueue {

    static let requestEventuallyQueue = RequestEventuallyQueue()
    let queue = [RequestType]()

}
但我在第let queue = [RequestType]()行收到错误提示,因为协议 RequestType 只能用作泛型约束,因为它具有 Self 或 associatedType 要求。
6个回答

205
假设我们现在调整了您的协议,添加了一个使用关联类型的例程:
public protocol RequestType: class {
    associatedtype Model
    var path: String { get set }
    
    func frobulateModel(aModel: Model)
}

如果Swift允许你按照自己的方式创建RequestType数组。我可以将这些请求类型的数组传递到函数中:

func handleQueueOfRequests(queue: [RequestType]) {
    // frobulate All The Things!

    for request in queue {
       request.frobulateModel(/* What do I put here? */)
    }
}

我想要 frobulate 所有的事情,但是我需要知道传递到调用中的参数类型。我的一些 RequestType 实体可以使用 LegoModel,一些可以使用 PlasticModel,而另一些则可以使用 PeanutButterAndPeepsModel。Swift 对于这种不确定性感到不满,因此它不允许您声明具有关联类型的协议变量。
同时,例如创建一个 RequestType 数组时,我们知道所有的 RequestType 都使用 LegoModel,这似乎是合理的,实际上也确实如此,但您需要某种方式来表达它。
一种方法是创建一个将真实类型与抽象 Model 类型名称相关联的类(或结构体、枚举):
class LegoRequestType: RequestType {
  typealias Model = LegoModel

  // Implement protocol requirements here
}

现在,声明一个LegoRequestType的数组是完全合理的,因为如果我们想要对它们进行frobulate,我们知道每次都必须传入一个LegoModel
这种关联类型的细微差别使得任何使用它们的协议都很特殊。Swift标准库有一些这样的协议,最著名的是CollectionSequence
为了让您创建一个实现Collection协议的东西数组,或者一组实现序列协议的东西集合,标准库采用了一种称为“类型擦除”的技术来创建结构体类型AnyCollection<T>AnySequence<T>。类型擦除技术在Stack Overflow答案中解释起来相当复杂,但如果您搜索网络,会发现有很多文章可以阅读。
Swift 5.7的存在类型。
Swift 5.7引入了使用any关键字的显式存在类型。这将消除“协议只能用作泛型约束”的错误,但它并不能解决这个示例中的根本问题。 (诚然,这个示例是学术性的,仅用于演示目的,并且由于其限制,在实际代码中可能没有用处。但它也展示了显式存在类型并不是万能药。)
下面是使用Swift 5.7和any关键字的代码示例。
public protocol RequestType: AnyObject {
    associatedtype Model
    var path: String { get set }

    func frobulateModel(aModel: Model)
}

func handleQueueOfRequests(queue: [any RequestType]) {
    // frobulate All The Things!

    for request in queue {
       request.frobulateModel(/* What do I put here? */)
    }
}

现在我们的队列包含了一组存在值,我们不再有关于“类型不能在这里使用,因为Self或AssociatedType约束”的错误。但是它并没有解决这个例子中的潜在问题,因为frobulateModel方法仍然可以接受任意类型(符合RequestType协议的实体相关类型)。
Swift提供了其他机制来帮助弥补这一点。通常情况下,您需要限制Model值以暴露所有Models共享的行为。frobulateModel方法可以被泛型化,并对参数进行约束以遵循该协议。或者您可以使用Swift 5.7的主要关联类型(SE-0346)来帮助约束协议级别的Models行为。
因此,显式存在值可以消除OP所问的错误消息,但它们并不是每种情况的解决方案。
此外,请记住,存在值会导致间接性,从而可能引入性能问题。在他们的WWDC会议上,Apple警告我们要谨慎使用它们。

3
“frobulate” 是什么意思? - Kai Zheng
7
在20世纪80年代,有一个文本冒险游戏系列,以游戏《Zork》开始。在那个游戏系列中,有一个叫做弗罗博兹魔法公司的机构。他们曾经使用“frobulate”(翻译为“调整”、“操作”等意思)的方式来进行一些事情。简单来说,这是一个用于不特定行动的幼稚短语。 - Scott Thompson
6
这个回答是一个很好的借口来使用“frobulate”这个词。 - ScottyBlades
@ScottThompson 我如何针对协议而非实现进行测试? - Michael Vescovo
有关存在性 Any 的更多信息,请访问:https://dev59.com/EVoV5IYBdhLWcg3wXNw9#72537733 - Pranav Kasetti

47

从Swift 5.1 - Xcode 11开始

您可以使用不透明结果类型来实现类似的功能。

想象一下:

protocol ProtocolA {
    associatedtype number
}

class ClassA: ProtocolA {
    typealias number = Double
}

因此,以下代码将会产生错误:

var objectA: ProtocolA = ClassA() /* Protocol can only be used as a generic constraint because it has Self or associatedType requirements */

但是,通过在类型之前添加 some 关键字使类型不透明会解决该问题,通常这就是我们想要的:

但是,在类型之前添加 some 关键字可以将类型不透明化,从而解决该问题,并且通常这就是我们想要的:

var objectA: some ProtocolA = ClassA()

1
注意:仅支持 iOS 13.0.0 或更高版本。 - Andrea Leganza

13

Swift 5.1

这是一个示例,说明如何通过实现关联类型和基础协议来使用通用协议:

import Foundation

protocol SelectOptionDataModelProtocolBase: class{}

protocol SelectOptionDataModelProtocol: SelectOptionDataModelProtocolBase {
    associatedtype T
    
    var options: Array<T> { get }
    
    var selectedIndex: Int { get set }
    
}

class SelectOptionDataModel<A>: SelectOptionDataModelProtocol {
    typealias T = A
    
    var options: Array<T>
    
    var selectedIndex: Int
    
    init(selectedIndex _selectedIndex: Int, options _options: Array<T>) {
        self.options = _options
        self.selectedIndex = _selectedIndex
    }
    
}

以下是一个示例视图控制器:

import UIKit

struct Car {
    var name: String?
    var speed: Int?
}

class SelectOptionViewController: UIViewController {
    
    // MARK: - IB Outlets
    
    // MARK: - Properties
    
    var dataModel1: SelectOptionDataModelProtocolBase?
    var dataModel2: SelectOptionDataModelProtocolBase?
    var dataModel3: SelectOptionDataModelProtocolBase?

    // MARK: - Initialisation
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    convenience init() {
        self.init(title: "Settings ViewController")
    }
    
    init(title _title: String) {
        super.init(nibName: nil, bundle: nil)
        
        self.title = _title
        
        self.dataModel1 = SelectOptionDataModel<String>(selectedIndex: 0, options: ["option 1", "option 2", "option 3"])
        self.dataModel2 = SelectOptionDataModel<Int>(selectedIndex: 0, options: [1, 2, 3])
        self.dataModel3 = SelectOptionDataModel<Car>(selectedIndex: 0, options: [Car(name: "BMW", speed: 90), Car(name: "Toyota", speed: 60), Car(name: "Subaru", speed: 120)])

    }
    
    // MARK: - IB Actions
    
    
    // MARK: - View Life Cycle

    
}

9

您的代码稍作修改便可实现此功能。在协议层次结构的顶部添加一个空的、不关联类型的协议即可。像这样...

public protocol RequestTypeBase: class{}

public protocol RequestType: RequestTypeBase {

    associatedtype Model
    var path: Model? { get set } //Make it type of Model

}
public class RequestEventuallyQueue {

    static let requestEventuallyQueue = RequestEventuallyQueue()
    var queue = [RequestTypeBase]() //This has to be 'var' not 'let'

}

另一个例子是,通过从协议RequestType派生的类创建一个队列,并将队列传递给一个函数以打印适当的类型。
public class RequestA<AType>: RequestType{
   public typealias Model = AType
   public var path: AType?
}
public class RequestB<BType>: RequestType{
   public typealias Model = BType
   public var path: BType?
}

var queue = [RequestTypeBase]()

let aRequest: RequestA = RequestA<String>()
aRequest.path = "xyz://pathA"

queue.append(aRequest)

let bRequest: RequestB = RequestB<String>()
bRequest.path = "xyz://pathB"

queue.append(bRequest)

let bURLRequest: RequestB = RequestB<URL>()
bURLRequest.path = URL(string: "xyz://bURLPath")

queue.append(bURLRequest)

func showFailed(requests: [RequestTypeBase]){

    for request in requests{
        if let request = request as? RequestA<String>{
            print(request.path!)
        }else if let request = request as? RequestB<String>{
            print(request.path!)
        }else if let request = request as? RequestB<URL>{
            print(request.path!)
        }

    }
}

showFailed(requests: queue)

9

这个错误也可能发生在以下情况下:

protocol MyProtocol {
    assosciatedtype SomeClass
    func myFunc() -> SomeClass
}

struct MyStuct {
    var myVar = MyProtocol
}

在这种情况下,您只需要使用泛型即可解决问题:
protocol MyProtocol {
    assosciatedtype SomeClass
    func myFunc() -> SomeClass
}

struct MyStuct<T: MyProtocol> {
    var myVar = T
}

7

Swift 5.7中的存在性any

现在我们可以通过在调用处简单地使用any关键字来解决“此协议不能用作泛型约束,因为它具有Self 或者 associatedType要求”的问题:

let queue = [any RequestType]()

Xcode 14现在建议使用"fix-it"来解决这个问题,错误信息将会消失!

注意:尽可能使用改进后的泛型语法

目前,泛型比存在性的any更全面和高效,因此我们可能更喜欢使用存在性的any,尽管它有一些限制。

为了更容易地使用正确的泛型语法,我们可以使用some关键字来指定一个带有单个泛型参数的函数的泛型(这称为主要关联类型)。

func addEntries1(_ entries: some Collection<MailmapEntry>, to mailmap: inout some Mailmap) {
    for entry in entries {
        mailmap.addEntry(entry)
    }
}

func addEntries2(_ entries: any Collection<MailmapEntry>, to mailmap: inout any Mailmap) {
    for entry in entries {
        mailmap.addEntry(entry)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接