我有一个相当大的结构,符合
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 代码。
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<...
类型本身现在是无限递归的。那好吧。很难偷偷溜过代数。
关键在于结构必须知道其大小,这意味着其中的所有内容都必须知道其大小,如果其任何属性的大小依赖于结构类型本身的大小,则这是不可能的。
一个简单的方法是将结构体更改为类:
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"
}
}
在 @Sweeper 举报后,我正在回答我自己的问题。
通过将
Message
结构转换为类并更改多个扩展,Message
可以递归地将自身包含为属性。这是可能的,因为 class
是一个引用类型,允许递归地包含自身。因此,下面的代码将编译:
class Message: Codable { // <-- Message is now a class
let content: String
// ...other values
let reference: Message
}
这是 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)
}
}