协议类型无法符合协议,因为只有具体类型才能符合协议。

32

在应用程序中,我们有两种类型的贴纸,String类型和Bitmap类型。每个贴纸包都可以包含这两种类型。以下是我声明模型的方式:

// Mark: - Models

protocol Sticker: Codable {
}

public struct StickerString: Sticker,  Codable, Equatable {
    let fontName: String
    let character: String
}

public struct StickerBitmap: Sticker,  Codable, Equatable {
    let imageName: String
}

用户选择了一些贴纸并使用后,我们希望将这些贴纸保存到 UserDefaults 中,以便向其显示“最近使用”的贴纸选项卡。我正在尝试解码保存的 [Sticker] 数组:

let recentStickers = try? JSONDecoder().decode([Sticker].self, from: data)

但我遇到了以下编译错误:

Protocol type 'Sticker' cannot conform to 'Decodable' because only concrete types can conform to protocols

我不明白为什么我声明了 Sticker 作为实现了 Decodable 还遵循 Codable 的协议,任何帮助将不胜感激!


错误告诉你出了什么问题:协议不能符合协议。decode 的第一个参数必须是具体类型。解决方案是使用一个受限于 Codable 的泛型类型。 - vadian
@vadian 嘿,Vadian!谢谢你的回复。可能是我英语水平不够好,我不明白“具体类型”是什么意思。我会尝试像你写的那样提出一种通用类型的解决方案。 - Roi Mulia
你的代码包含两个具体类型,StickerStringStickerBitmap - vadian
@vadian 我在 Sticker 中添加了 associatedtype,并在 StickerStringStickerBitmap 中使用 typealias 分别指定它们自己的类型,但是仍然出现相同的错误。我是否能够声明一个 [Sticker] 数组,或者只能使用其中一种具体类型? - Roi Mulia
3个回答

27
不要使用协议,而是使用泛型。
声明一个简单的函数。
func decodeStickers<T : Decodable>(from data : Data) throws -> T
{
    return try JSONDecoder().decode(T.self, from: data)
}

T 可以是单个对象,也可以是数组。


在你的结构体中删除 Sticker 协议。你还可以删除 Equatable,因为它会在结构体中自动生成。

public struct StickerString : Codable {
    let fontName: String
    let character: String
}

public struct StickerBitmap : Codable {
    let imageName: String
}

要解码其中一种贴纸类型,请注释该类型

let imageStickers = """
[{"imageName":"Foo"},{"imageName":"Bar"}]
"""    
let stickerData = Data(imageStickers.utf8)

let recentStickers : [StickerBitmap] = try! decodeStickers(from: stickerData)
print(recentStickers.first?.imageName)

并且

let stringSticker = """
{"fontName":"Times","character":""}
"""    
let stickerData = Data(stringSticker.utf8)

let sticker : StickerString = try! decodeStickers(from: stickerData)
print(sticker.character)

为了解码一个由StickerStringStickerBitmap类型组成的数组,需要声明一个带有关联值的包装枚举。
enum Sticker: Codable {

    case string(StickerString)
    case image(StickerBitmap)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let stringData = try container.decode(StickerString.self)
            self = .string(stringData)
        } catch DecodingError.keyNotFound {
            let imageData = try container.decode(StickerBitmap.self)
            self = .image(imageData)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
            case .string(let string) : try container.encode(string)
            case .image(let image) : try container.encode(image)
        }
    }
}

然后你可以解码。
let stickers = """
[{"imageName":"Foo"},{"imageName":"Bar"}, {"fontName":"Times","character":""}]
"""

let stickerData = Data(stickers.utf8)
let recentStickers = try! JSONDecoder().decode([Sticker].self, from: stickerData)
print(recentStickers)

在表格视图中,只需对枚举进行switch操作。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let sticker = stickers[indexPath.row]
    switch sticker {
    case .string(let stringSticker): 
        let cell = tableView.dequeueReusableCell(withCellIdentifier: "StringStickerCell", for: indexPath) as! StringStickerCell
        // update UI
        return cell
    case .image(let imageSticker): 
        let cell = tableView.dequeueReusableCell(withCellIdentifier: "ImageStickerCell", for: indexPath) as! ImageStickerCell
        // update UI
        return cell
    }
}

嘿,Vadian!感谢您详细的回复。问题是是否可以将“StickerString”和“StickerBitmap”组合在同一个数组中? - Roi Mulia
从你的回答中我理解到,我们可以使用泛型来解码/编码我的数据。但是我正在尝试理解一些事情:最终,当我们将数据解码回贴纸时,我需要有选项在同一个数组中使用两种类型的贴纸,因为用户可以从多个贴纸包中选择贴纸。如果这两种贴纸类型之间没有关系,我们如何实现这一点呢? - Roi Mulia
1
数组必须看起来像 [.string(StickerString()), .image(StickerBitmap())]。在 cellForRow 中,像 encode 方法一样切换枚举。 - vadian
1
结合使用Codable是最有效的方式。没有Codable,使用较简单的协议作为公共类型就足够了。 - vadian
应该使用DecodingError.typeMismatch而不是DecodingError.keyNotFound - barola_mes
显示剩余2条评论

1
这里发生的事情有点不言自明。

JSONDecoder().decode(/* swift here is expecting class or struct that conforms to Codable */.self, from: data)

但是让我们假设你能够在你的协议中传递一个协议

protocol Sticker: Codable {
}

您期望Swift从数据中解码的属性在哪里?

您将属性添加到了

public struct StickerString: Sticker,  Codable, Equatable { // it should have redundendant conformance as well as you are conforming to Coddle again
    let fontName: String // here is the properties you are expected to be decoded with the coding keys
    let character: String // here is the properties you are expected to be decoded with the coding keys
}

只要您想要解码的类型是动态的,我建议您按照以下方式操作。
class GenericService< /* here you can pass your class or struct that conforms to Codable */ GenericResponseModel: Codable> {

func buildObjectFromResponse(data: Data?) -> GenericResponseModel? {
        var object : GenericResponseModel?
        do {
            object = try JSONDecoder().decode(GenericResponseModel.self , from: data!)
        } catch (let error){
            print(error)
        }
        return object
    }

}

通过这个类,您可以传递符合Codable的任何类型甚至列表。 然后,您可以使用下面的方法将类型检查与解码过程分离。
private func handleDecodingTypes (stickers: [Sticker]){
        for sticker in stickers {
            if sticker is StickerString {
                /* do the decoding here */
            }
            if sticker is StickerBitmap {
                /* do the decoding here */
            }
        }
    }

1
嗨Karem!谢谢你的回答。从你和Vadian的回答中我理解到,我们可以使用泛型来解码/编码我的数据。但是我正在尝试理解一些东西:最终,当我们将数据解码回Stickers时,我需要有选项在同一个数组中同时使用两种类型的贴纸,因为用户可以从多个贴纸包中选择贴纸。如果我们没有两种贴纸类型之间的关系,我们如何实现这一点? - Roi Mulia
我已经编辑了答案,我的主要目标是解耦将对象转换为特定具体类型的过程和解码过程,因为解码过程不应意识到其解码内容。 - karem_gohar

0

Sticker协议并不是确认/实现Codable,而是继承自Codable。正如错误信息所提示的那样,协议不能符合其他协议,只有具体类型才能这样做。

protocol Sticker: Codable //This is Protocol inheritance

通过声明

public struct StickerString: Sticker

这意味着Sticker字符串符合Sticker,而Sticker是Codable的子类,因此StickerString最终符合Codable。没有必要再次声明符合性,即:

public struct StickerString: Sticker,  Codable //Conformance to Codable is redundant

现在来到解码部分。
  let recentStickers = try? JSONDecoder().decode([Sticker].self, from: data)

decode 方法需要具体的类型。它没有关于基础类型或其属性的任何信息,因为 Sticker 仅仅是继承自 Codable 的协议本身,并没有任何属性/特性。 编译器在解码了 StrickerString 和 StickerBitmap 后不会有任何问题合并两者。

let stickerString = try JSONDecoder().decode(StickerString.self, from: data)
let stickerBitmap = try JSONDecoder().decode(StickerBitmap.self, from: data)
let stickers : [Sticker] = [stickerString, stickerBitmap]

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