I am trying to code a custom Coder
and I am having trouble understanding something, particularly about the Encoder
part.
The Encoder
is only required to have three functions and two variables :
struct MyEncoder : Encoder {
public var codingPath : [CodingKey]
public var userInfo : [CodingUserInfoKey : Any]
public func container<Key>(keyedBy type: Key.Type -> KeyedEncodingContainer<Key> where Key : CodingKey {}
public func unkeyedContainer() -> UnkeyedEncodingContainer {}
public func singleValueContainer() -> SingleValueEncodingContainer {}
}
You will notice, none of them are mutating. They all return something and that's it.
The containers themselves, to generalise, have sub functions to encode specific types into themselves :
struct MySingleValueContainer : SingleValueEncodingContainer {
var codingPath: [CodingKey]
mutating func encode(_ value : someType) throws {}
/// etc with a lot of different types
}
If a user wants to customise how their class is encoded, they may do this :
func encode(to: Encoder) throws {}
And here is my problem ! The encoder persists after the call to encode(to: Encoder)
but it does not mutate so how does the encoded data end up inside it ? Reading the JSON Encoder's source I can see that their containers have a variable containing an Encoder
, which is passed down. But it should be copy-on-write as everything else in Swift, which means the data shouldn't be able to make its way back up the chain !
Is the Encoder
somehow secretly passed as a reference instead of as a value ? That seems like the only logical conclusion but it seems very weird to me... If not, what is going on ?
I have also taken a look at this question and its answer, and although they have helped me they display the same behaviour of magically bringing data up the chain, except it passes down a custom type instead of the Encoder
itself.
CodePudding user response:
Is the
Encoder
somehow secretly passed as a reference instead of as a value?
Close, but not quite. JSONEncoder
and its underlying containers are relatively thin veneer interfaces for reference-based storage. In the swift-corelibs-foundation implementation used on non-Darwin platforms, this is done using RefArray
and RefObject
to store keyed objects; on Darwin platforms, the same is done using NSMutableArray
and NSMutableDictionary
. It's these reference objects that are passed around and shared, so while the individual Encoder
s and containers can be passed around, they're writing to shared storage.
But it should be copy-on-write as everything else in Swift, which means the data shouldn't be able to make its way back up the chain!
One more note on this: all of the relevant types internal to this implementation are all classes (e.g. JSONEncoderImpl
in swift-corelibs-foundation, _JSONEncoder
on Darwin), which means that they wouldn't be copy-on-write, but rather, passed by reference anyway.
Regarding your edit: this is the same approach that the linked answer uses with its fileprivate final class Data
— it's that class which is shared between all of the internal types and passed around.
CodePudding user response:
JSONEncoder.encode
uses JSONEncoderImpl
as the concrete implementation of Encoder
(source):
open func encode<T: Encodable>(_ value: T) throws -> Data {
let value: JSONValue = try encodeAsJSONValue(value)
...
}
func encodeAsJSONValue<T: Encodable>(_ value: T) throws -> JSONValue {
let encoder = JSONEncoderImpl(options: self.options, codingPath: [])
guard let topLevel = try encoder.wrapEncodable(value, for: nil) else {
...
}
return topLevel
}
wrapEncodable
would then eventually call encode(to: Encoder)
on your Codable
type with self
as the argument.
Is the
Encoder
somehow secretly passed as a reference instead of as a value ?
Notice that JSONEncoderImpl
is a class, so there is nothing "secret" about this at all. The encoder is passed as a reference in this particular case.
In general though, the Encoder
implementation could also be a struct
, and be passed by value. After all, all it needs to do is to provide mutable containers into which data can be encoded. As long as you have a class somewhere down the line, it is possible to "seemingly" mutate a struct without mutating
or making the struct a var
. Consider:
private class Bar {
var magic: Int = -1
}
struct Foo: CustomStringConvertible {
private let bar = Bar()
var description: String { "\(bar.magic)" }
func magicallyMutateMyself() {
bar.magic = Int.random(in: 0..<100)
}
}
let foo = Foo()
print(foo) // -1
foo.magicallyMutateMyself()
print(foo) // some other number