使用带有 typealias 的协议作为属性

Posted

技术标签:

【中文标题】使用带有 typealias 的协议作为属性【英文标题】:Using protocol with typealias as a property 【发布时间】:2015-12-06 16:36:47 【问题描述】:

我有一个带有 typealias 的协议:

protocol Archivable 
    typealias DataType

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

以及符合该协议的类:

class Archiver: Archivable 
    typealias DataType = Int

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

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

我想将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' 只能用作通用约束,因为它具有 Self 或关联类型要求

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

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

【问题讨论】:

阅读:the link @RMenke 感谢您的链接,所以基本上这种方法不起作用:\ 目前泛型只是部分实现。 Swift 3 的目标之一是使其完全通用。使一切通用化是最新的趋势,但它与 Swift 的工作方式并不一致。每种类型都必须在编译时知道,而 Swift 中的泛型还不能在每种情况下都做到这一点。我个人尽量避免使用泛型。 @DánielNagy 如果你有空闲时间:***.com/questions/34285246/… 【参考方案1】:

根据您实际尝试执行的操作,这可以使用类型擦除来实现。如果您按照 cmets 中发布的链接 R Menke 中的说明进行操作,您就可以实现您想要做的事情。由于您在TestClass 中的属性似乎是一个让,我将假设您在编译时已经知道DataType 的类型。首先,您需要像这样设置一个类型已擦除的Archivable 类:

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 包装在您的 TestClass 中:

class TestClass 
    let archiver = AnyArchiver(Archiver())

通过类型推断,Swift 将键入TestClass'archiver let constant 作为AnyArchiver&lt;Int&gt;。这样做可以确保您不必创建十几个协议来定义 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

我写了一篇关于 goes into even more detail 的帖子,以防您在使用这种方法时遇到任何问题。我希望这会有所帮助!

【讨论】:

我读过你关于通用协议的帖子,我很喜欢它。你善于解释事情。但有一件事让我感到困惑——“The Swift Programming Language”一书中没有“通用协议”一词,因为 Swift 中没有通用协议:) 查找关联类型。我称它们为泛型协议,因为它们与泛型类非常相似,并且本质上不太具体。【参考方案2】:

当你尝试声明和分配archiver:

let archiver: Archivable = Archiver()

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

来自“The Swift Programming Language (Swift 2)”一书:

关联类型为一个类型提供一个占位符名称(或别名) 用作协议的一部分。实际使用的类型 在协议被采用之前不指定关联类型。

所以你需要声明继承自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)”一书:

协议定义了适合特定任务或功能的方法、属性和其他要求的蓝图。

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

如果您通过初始化参数传递archivable,则TestClass 可以是通用的:

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")
    

【讨论】:

"当您尝试声明和分配归档器时:let archiver: Archivable = Archiver() 它必须具有具体类型。"这实际上是不正确的。那行特定的代码是完全合法的。问题出在其他地方。 @VinceO'Sullivan 那行代码是不合法的,并给出编译器错误Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requirements。问题全在于这个错误。 编译错误不是因为那行是非法的,而是因为类和/或协议的定义方式。请看下面我的回答。那行编译得很好。 @VinceO'Sullivan 编译错误是因为该行在 OP 在他的问题中发布的其他代码的上下文中是非法的,我正在回答它。【参考方案3】:

Hector 在上面给出了一个更复杂但最终更好的解决方案,但我想我还是会发布另一种答案。它更简单,但从长远来看可能不太灵活。

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()

【讨论】:

您是否刚刚将typealias 置于全局范围内?这使得它非泛型 是的,我做到了。是的,它确实。你说的原样非法的线路现在是合法的。 让我们解决所有这样的问题......它编译得很好,但它没有做我们需要做的事情。

以上是关于使用带有 typealias 的协议作为属性的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin typealias属性

Kotlin typealias属性

为实现多个协议的任何对象定义一个 Swift 类型别名

带有标志属性的 Enum.TryParse

带有标志属性的 Enum.TryParse

使用 typealias 从回调转移到委托