One technique that I like to use that involves an init
-restricted .struct
is to design a robust API that involves temporary objects with strict life-time limitations (like some sort of transaction or some temporary accessor to something). Take, for example this API:
public final class DataBase {
// ...
public func withTransaction<Success>(call function: @escaping (Transaction) throws -> Success) -> Success {
// ...
}
}
In a normal scenario, the Transaction
object can be either easily copied (if it is a struct
) or retained (if it is a class
), which is not what you’d want, given that the transaction has a very specific semantic attached to its life-time (for instance, the same transaction object might be reused for nested transaction-wrapped code, which would coalesce them into a single transaction, which, in turn, would commit the transaction at the end of the outermost call):
func foo() {
myDataBase.withTransaction { transaction in
// The `transaction` object was just created (a transaction has begun).
transaction.changeSomething()
bar()
}
}
func bar() {
myDataBase.withTransaction { transaction in
// The `transaction` object is the same as the one in `foo`.
transaction.changeSomething()
}
}
In this scenario, the transaction either has to be a class
object or a struct
that wraps a class
object. If we simply pass around the class
object itself, then foo
or bar
could accidentally escape the reference to it, thus disrupting the transaction logic and causing the transaction (and all subsequent transactions) to hang indefinitely.
One could argue that it’s a programming error to escape the reference, but it’s easy to accidentally retain it in a closure and it’s incredibly hard to track down the cause of the bug.
So, in order to avoid this problem, I’d make Transaction
a struct
that stores a weak
reference to a class
object that actually implements the transactional functionality. I’d then pass around that wrapper struct
instead of the object, thus, guaranteeing that the transaction will live exactly as long as we need it to. Any “escaped” references will always be weak
, so at the end of the transaction, all references will expire and the worst possible outcome is a force-unwrapping crash, instead of a mysterious hang...
Naturally, you’d make all init
s of the struct
non-public, so that the only way to get it would be to actually start a trabsaction.