Swift Codable 结构递归地将自身包含为属性

问题描述 投票:0回答:4

我有一个相当大的结构,符合

Codable
,并且它的属性之一需要与其自身具有相同的类型。我正在尝试做的事情的简短示例如下所示:

struct Message: Codable {
    let content: String
    // ...other values
    let reference: Message // <-- Error: Value type 'Message' cannot have a stored property that recursively contains it
}

Swift 似乎不允许结构体递归地将自身包含为其值之一。除了创建一个完整的重复

Message
结构(这会将其变成先有鸡还是先有蛋的问题,其中重复结构不能包含自身等)之外,还有什么方法可以使其工作吗?不创建重复的结构还允许我重用接受并呈现
Message
结构的 SwiftUI 代码。

swift recursion codable
4个回答
4
投票

Sweeper 的方法提供了两个很好的解决方案,都涉及到注入类。但是您还可以尝试其他一些方法,有些有效,有些无效,并且值得了解根本问题。这不仅仅是一个怪癖;这是 Swift 工作方式的重要组成部分。

// FAIL (for abstract reasons)
var manager: Manager
var manager: CollectionOfOne<Manager>

// FAIL (for swift-specific reasons)
var manager: Manager? // when manager is a struct
var manager: Result<Manager, Error> // when manager is a struct

// PASS
var manager: Manager? // when Manager is a class
var manager: Result<Manager, Error> // when manager is a class
var manager: Box<Manager>   // our custom class Box
var manager: [Manager]
var manager: EmptyCollection<Manager>

下面的例子也通过了:

protocol ManagerLike {
    var id: String { get set }
    var mail: String { get set }
    var manager: ManagerLike? { get set }
}

struct Manager: ManagerLike {
    var id: String
    var mail: String
    var manager: any ManagerLike?
}

前两个案例因抽象、逻辑原因而失败。任何“热切”的语言(如 Swift)都不可能构造一个必须包含其自身类型值的类型。这就是无限递归。而一门热切的语言需要先构造出整个价值,然后才能进步。即使使用惰性语言,您也需要某种生成器来构造它。无论 Manager 是结构体还是类(或者枚举或其他任何东西)都是如此。

接下来的两个问题与 Swift 如何实现结构有关。结构体按顺序将其每个字段存储在内存中;没有间接性。因此,结构体的大小取决于其属性的大小,并且 Swift 必须在编译时知道其最终大小。 (在 Rust 中,您可以将类型显式标记为 Sized 来明确表达这一点,但在 Swift 中,它是结构的隐式部分。)

Optional 和 Result 本身只是结构体,因此它们像任何其他结构体一样内联其内容,并且它们的最终大小取决于其内容的大小。由于在编译时无法知道该链有多深,因此无法知道该结构有多大。

另一方面,类将其数据存储在堆上,并实现为指向该存储的指针。无论它们包含什么,它们的大小总是完全相同。所以嵌套的

Manager?
是有效的。这在逻辑上是可能的,因为它最终可以终止(与嵌套的
Manager
不同)。它很大,因为它始终是对象指针的大小。结果是一样的。

Box
只是一个类的一个特定情况,这就是它起作用的原因。

但是

[Manager]
呢?当然我们不知道里面有多少个元素,那么Swift怎么知道它有多大呢?因为数组的大小总是相同的。它通常将其内容存储在堆上,并且在内部仅存储一个指针。在某些情况下,如果内容足够小,它可以内联其内容。但关键是数组本身是固定大小的。它只有一个指向可变大小存储的指针。

EmptyCollection 的工作原理相同。它总是相同的大小,空的。

最后一个例子,使用协议,是一个存在容器的例子,这就是新的

any
所指出的。存在容器与
Box
类型非常相似,只不过它是由编译器创建的,你不能直接通过语言访问它。它将内容移动到堆中(除非内容非常小),因此本身是固定大小的。

这又带来了一个看似有效但实际上无效的示例:

protocol ManagerLike {
    var id: String { get set }
    var mail: String { get set }
    var manager: ManagerLike? { get set }
}

struct Manager<M: ManagerLike> {
    var id: String
    var mail: String
    var manager: M?
}

现在,不再使用

any Manager
,而是用泛型代替。这将编译。所以你可能会想“我只需将
Manager
作为 M 类型传递即可实现我最初的目标!”你会发现规则是......自我执行的:

let m = Manager<Manager<Manager<Manager<...

类型本身现在是无限递归的。那好吧。很难偷偷溜过代数。

关键在于结构必须知道其大小,这意味着其中的所有内容都必须知道其大小,如果其任何属性的大小依赖于结构类型本身的大小,则这是不可能的。


3
投票

一个简单的方法是将结构体更改为类:

class Message: Codable {
    let content: String
    // ...other values
    let reference: Message? // optional - the recursion has to end somewhere right?
}

但这可能会破坏代码的其他部分,因为结构和类具有截然不同的语义。

另一种选择是创建引用类型

Box

class Box<T: Codable>: Codable {
    let wrappedValue: T
    required init(from decoder: Decoder) throws {
        wrappedValue = try T(from: decoder)
    }
    
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

那么,

struct Message: Codable {
    let content: String
    // ...other values
    
    let boxedReference: Box<Message>?
    
    // you can still refer to 'reference' as 'reference' in your swift code
    var reference: Message? { boxedReference?.wrappedValue }
    
    enum CodingKeys: String, CodingKey {
        case content, boxedReference = "reference"
    }
}

1
投票

在 @Sweeper 举报后,我正在回答我自己的问题。

通过将

Message
结构转换为类并更改多个扩展,
Message
可以递归地将自身包含为属性。这是可能的,因为
class
是一个引用类型,允许递归地包含自身。因此,下面的代码将编译:

class Message: Codable { // <-- Message is now a class
    let content: String
    // ...other values
    let reference: Message
}

0
投票

这是 Sweeper 的

Box
类,被重新设计为属性包装器,以使其更好用:

@propertyWrapper
class Boxed<Wrapped: Codable> {
    var wrappedValue: Wrapped
    
    init(wrappedValue: Wrapped) {
        self.wrappedValue = wrappedValue
    }
    
    required init(from decoder: Decoder) throws {
        wrappedValue = try Wrapped(from: decoder)
    }
    
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

// Optional: add automatic Equatable/Hashable conformance
extension Boxed: Equatable where Wrapped: Equatable {
    static func == (lhs: Boxed<Wrapped>, rhs: Boxed<Wrapped>) -> Bool {
        lhs.wrappedValue == rhs.wrappedValue
    }
}

extension Boxed: Hashable where Wrapped: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(wrappedValue)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.