To understand the origin of the question, let's start with some code:
protocol MyProtocol {
var val1: Int { get set }
}
struct StructA: MyProtocol {
var val1: Int
var structAVal: Int
}
struct StructB: MyProtocol {
var val1: Int
var structBVal: Int
var thirdProperty: Int
}
And then I have a struct with a heterogeneous array of type MyProtocol
:
struct Values {
var arr: [MyProtocol] = [StructA(val1: 0, structAVal: 0), StructB(val1: 0, structBVal: 0)]
}
if I was to change one of the values with a method in Values
such as:
struct Values {
var arr: [MyProtocol] = [StructA(val1: 0, structAVal: 0), StructB(val1: 0, structBVal: 0)]
mutating func set<T: MyProtocol>(at index: Int, _ newValue: T) {
arr[index] = newValue
}
}
That would be smooth.
The problem which I am facing is, say I wanted to change var thirdProperty: Int
in the structB
item in var arr: [MyProtocol]
, I would not be able to do so which my mutating func set<T: MyProtocol>(at index: Int, _ newValue: T)
, since It only knows of MyProtocol
types.
So my 2 cents to resolve this matter was using a closure something like this:
mutating func set<T: MyProtocol>(at index: Int, closure: (T?) -> (T)) {
arr[index] = closure(arr[index] as? T)
}
The problem with this is that every time I invoke this method, I would first need to downcast the parameter (from MyProtocol
to StructB
). which seems more of a workaround which could invite unwanted behaviours along the road.
So I started thinking maybe there is a way to constraint the generic parameter to a sibling parameter something like this (pseudo code):
mutating func set<T: MyProtocol>(type: MyProtocol.Type, at index: Int, closure: (T?) -> (T)) where T == type {
arr[index] = closure(arr[index] as? T)
}
Which as you guessed, does not compile.
Any thought on how to approach this matter in a better manner. T.I.A
PGDev's solution gets to the heart of the question, but IMO the following is a bit easier to use:
enum Error: Swift.Error { case unexpectedType }
mutating func set<T: MyProtocol>(type: T.Type = T.self, at index: Int,
applying: ((inout T) throws -> Void)) throws {
guard var value = arr[index] as? T else { throw Error.unexpectedType }
try applying(&value)
arr[index] = value
}
...
var v = Values()
try v.set(type: StructB.self, at: 1) {
$0.thirdProperty = 20
}
The = T.self
syntax allows this to be simplified a little when the type is known:
func updateThirdProperty(v: inout StructB) {
v.thirdProperty = 20
}
try v.set(at: 1, applying: updateThirdProperty)
Another approach that is more flexible, but slightly harder on the caller, would be a closure that returns MyProtocol, so the updating function can modify the type. I'd only add this if it were actually useful in your program:
mutating func set<T: MyProtocol>(type: T.Type = T.self, at index: Int,
applying: ((T) throws -> MyProtocol)) throws {
guard let value = arr[index] as? T else { throw Error.unexpectedType }
arr[index] = try applying(value)
}
...
try v.set(type: StructB.self, at: 1) {
var value = $0
value.thirdProperty = 20
return value // This could return a StructA, or any other MyProtocol
}
(Which is very close to PGDev's example, but doesn't require Optionals.)