使用带有类型别名的协议作为 属性

Using protocol with typealias as a property

我有一个带有类型别名的协议:

protocol Archivable {
    typealias DataType

    func save(data: DataType, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> DataType
}

和符合该协议的class:

class Archiver: Archivable {
    typealias DataType = Int

    func save(data: DataType, withNewName newName: String) throws {
        //saving
    }

    func load(fromFileName fileName: String) throws -> DataType {
        //loading
    }
}

我想在另一个 class 中将 Archivable 用作 属性,例如:

class TestClass {

    let arciver: Archivable = Archiver() //error here: Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments
}

但它失败了

Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments

我的目标是 TestClass 应该只将 Archiver 视为 Archiveable,所以如果我想更改 saving/loading 机制,我只需要创建一个新的符合Archivable的class设置为TestClass中的属性,但我不知道这是否可行,如果是,那么如何。

而且我想避免使用 AnyObject 而不是 DataType。

当您尝试声明和赋值时 archiver:

let archiver: Archivable = Archiver()

它必须有具体类型。 Archivable 不是具体类型,因为它是具有关联类型的协议。

来自 "The Swift Programming Language (Swift 2)" 本书:

An associated type gives a placeholder name (or alias) to a type that is used as part of the protocol. The actual type to use for that associated type is not specified until the protocol is adopted.

因此需要声明继承自Archivable的协议并指定关联类型:

protocol IntArchivable: Archivable {
    func save(data: Int, withNewName newName: String) throws

    func load(fromFileName fileName: String) throws -> Int
}

然后就可以采用这个协议了:

class Archiver: IntArchivable {
    func save(data: Int, withNewName newName: String) throws {
        //saving
    }

    func load(fromFileName fileName: String) throws -> Int {
        //loading
    }
}

现在 Swift 中没有真正通用的协议,因此您不能这样声明 archiver

let archiver: Archivable<Int> = Archiver()

但问题是您不需要这样做,我会解释原因。

来自 "The Swift Programming Language (Swift 2)" 本书:

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.

所以基本上,当你想将 archiver 声明为 Archivable<Int> 时,你的意思是你不希望使用 archiver 的某些代码了解其具体 class 并可以访问它的其他方法、属性等。 很明显,这段代码应该包装在单独的 class、方法或函数中,并且 archiver 应该作为参数传递到那里,并且这个 class、方法或函数将是通用的。

在你的情况下,如果你通过初始化参数传递 archivableTestClass 可以是通用的:

class TestClass<T, A: Archivable where A.DataType == T> {
    private let archivable: A

    init(archivable: A) {
        self.archivable = archivable
    }

    func test(data: T) {
        try? archivable.save(data, withNewName: "Hello")
    }
}

或者它可以有接受 archivable 作为参数的通用方法:

class TestClass {        
    func test<T, A: Archivable where A.DataType == T>(data: T, archivable: A) {
        try? archivable.save(data, withNewName: "Hello")
    }
}

根据您实际尝试做的事情,这可以使用类型擦除来工作。如果您按照评论中 link R Menke post 中的说明进行操作,您可以实现您想要做的事情。由于 TestClass 中的 属性 似乎是一个 let,我假设您在编译时已经知道 DataType 的类型。首先你需要设置一个类型 erased Archivable class 像这样:

class AnyArchiver<T>: Archivable {
    private let _save: ((T, String) throws -> Void)
    private let _load: (String throws -> T)

    init<U: Archivable where U.DataType == T>(_ archiver: U) {
        _save = archiver.save
        _load = archiver.load
    }

    func save(data: T, withNewName newName: String) throws {
        try _save(data, newName)
    }

    func load(fromFileName fileName: String) throws -> T {
        return try _load(fileName)
    }
}

很像 Swift 的 AnySequence,您可以像这样将 Archiver 包装在 class 中的 TestClass 中:

class TestClass {
    let archiver = AnyArchiver(Archiver())
}

通过类型推断,Swift 将类型 TestClass' archiver let 常量作为 AnyArchiver<Int>。这样做将确保您不必创建一打协议来定义 DataType 类似于 StringArchiverArrayArchiverIntArchiver 等。相反,您可以选择使用这样的泛型来定义变量:

let intArchiver: AnyArchiver<Int>
let stringArchiver: AnyArchiver<String>
let modelArchiver: AnyArchiver<Model>

而不是像这样复制代码:

protocol IntArchivable: Archivable {
    func save(data: Int, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> Int
}
protocol StringArchivable: Archivable {
    func save(data: String, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> String
}
protocol ModelArchivable: Archivable {
    func save(data: Model, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> Model
}

let intArchiver: IntArchivable
let stringArchiver: StringArchivable
let modelArchiver: ModelArchivable

我写了一个 post 关于这个 goes into even more detail 以防你 运行 使用这种方法遇到任何问题。希望对您有所帮助!

Hector 在上面给出了一个更复杂但最终更好的解决方案,但我认为无论如何我都会 post 替代答案。它更简单,但从长远来看可能不太灵活。

typealias DataType = Int

protocol Archivable {
    var data: DataType { get set }

    func save(data: DataType, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> DataType
}

class Archiver: Archivable {
    var data:DataType = 0

    func save(data: DataType, withNewName newName: String) throws {
        //saving
    }

    func load(fromFileName fileName: String) throws -> DataType {
        return data
    }
}

class TestClass {
    let arciver: Archivable = Archiver()
}