Fallible class initializer and protocol extension

Consider the following protocol:

protocol Foo: RawRepresentable, Codable where RawValue == Int {}

With a concrete type implementation:

final class Bar: Foo {
    let rawValue: Int

    init?(rawValue: Int) {
        if rawValue == 0 {
            return nil
        }
        self.rawValue = rawValue
    }
}

Now suppose we provide default implementation of Codable for any class adopting Foo:

extension Foo {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let value = try container.decode(Int.self)

        if let instance = Self.init(rawValue: value) {
            self = instance
        } else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Corrupt data."
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = try encoder.singleValueContainer()

        try container.encode(rawValue)
    }
}

Note that I use self = instance assignment because we have a fallible init?(rawValue: Data) initializer. Now that seems to be legal for structs however, what's going on with classes in that case? How does Swift handle that internally?

Certainly if I add AnyObject requirement to Foo, then this code does not compile. But it works just fine if I don't.

The source above can be tested with:

struct Baz: Codable {
    var bar: Bar
}

for i in 0 ... 1 {
    do {
        let baz = try JSONDecoder().decode(
            Baz.self,
            from: "{ \"bar\": \(i) }".data(using: .utf8)!
        )
        print("Baz.bar (\(i)) = \(baz.bar.rawValue)")
    } catch {
        print("Got error (\(i): \(error)")
    }
}

which outputs:

Got error (0: dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "bar", intValue: nil)], debugDescription: "Corrupt data.", underlyingError: nil))
Baz.bar (1) = 1

The prohibition on mutable methods for classes (and class constrained protocols) is artificial. It's only disallowed because it's almost never what you meant to do when you know your working with a reference type. Unconstrained protocols simply do the same thing for mutating methods when passed an existential holding a reference type that they do for a value type. It overwrites the value of self, which in this case is a box holding a reference.

You can do the same thing for reference types, without an existential, using static or free functions.

final class NonZero {
    var value: Int
    init?(_ value: Int) {
         guard value != 0 else { return nil }
         self.value = value
    }
}

func potentiallyReplace(_ x: inout NonZero, with y: Int) {
    guard let y = NonZero(y) else { return }
    x = y
}

potentiallyReplace does exactly the same thing a mutating method would have done, so there's no technical limitation stopping mutating methods from working with classes. It's a restriction designed to discourage using class types when a value type would be more appropriate.

1 Like