Swift generic coercion misunderstanding
The difference is that Array
(and Set
and Dictionary
) get special treatment from the compiler, allowing for covariance (I go into this in slightly more detail in this Q&A).
However arbitrary generic types are invariant, meaning that X<T>
is a completely unrelated type to X<U>
if T != U
– any other typing relation between T
and U
(such as subtyping) is irrelevant. Applied to your case, Signal<ChildClass>
and Signal<BaseProtocol>
are unrelated types, even though ChildClass
is a subtype of BaseProtocol
(see also this Q&A).
One reason for this is it would completely break generic reference types that define contravariant things (such as function parameters and property setters) with respect to T
.
For example, if you had implemented Signal
as:
class Signal<T> {
var t: T
init(t: T) {
self.t = t
}
}
If you were able to say:
let signalInt = Signal(t: 5)
let signalAny: Signal<Any> = signalInt
you could then say:
signalAny.t = "wassup" // assigning a String to a Signal<Int>'s `t` property.
which is completely wrong, as you cannot assign a String
to an Int
property.
The reason why this kind of thing is safe for Array
is that it's a value type – thus when you do:
let intArray = [2, 3, 4]
var anyArray : [Any] = intArray
anyArray.append("wassup")
there are no problems, as anyArray
is a copy of intArray
– thus the contravariance of append(_:)
is not a problem.
However, this cannot be applied to arbitrary generic value types, as value types can contain any number of generic reference types, which leads us back down the dangerous road of allowing an illegal operation for generic reference types that define contravariant things.
As Rob says in his answer, the solution for reference types, if you need to maintain a reference to the same underlying instance, is to use a type-eraser.
If we consider the example:
protocol BaseProtocol {}
class ChildClass: BaseProtocol {}
class AnotherChild : BaseProtocol {}
class Signal<T> {
var t: T
init(t: T) {
self.t = t
}
}
let childSignal = Signal(t: ChildClass())
let anotherSignal = Signal(t: AnotherChild())
A type-eraser that wraps any Signal<T>
instance where T
conforms to BaseProtocol
could look like this:
struct AnyBaseProtocolSignal {
private let _t: () -> BaseProtocol
var t: BaseProtocol { return _t() }
init<T : BaseProtocol>(_ base: Signal<T>) {
_t = { base.t }
}
}
// ...
let signals = [AnyBaseProtocolSignal(childSignal), AnyBaseProtocolSignal(anotherSignal)]
This now lets us talk in terms of heterogenous types of Signal
where the T
is some type that conforms to BaseProtocol
.
However one problem with this wrapper is that we're restricted to talking in terms of BaseProtocol
. What if we had AnotherProtocol
and wanted a type-eraser for Signal
instances where T
conforms to AnotherProtocol
?
One solution to this is to pass a transform
function to the type-eraser, allowing us to perform an arbitrary upcast.
struct AnySignal<T> {
private let _t: () -> T
var t: T { return _t() }
init<U>(_ base: Signal<U>, transform: @escaping (U) -> T) {
_t = { transform(base.t) }
}
}
Now we can talk in terms of heterogenous types of Signal
where T
is some type that's convertible to some U
, which is specified at the creation of the type-eraser.
let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal, transform: { $0 }),
AnySignal(anotherSignal, transform: { $0 })
// or AnySignal(childSignal, transform: { $0 as BaseProtocol })
// to be explicit.
]
However, the passing of the same transform
function to each initialiser is a little unwieldy.
In Swift 3.1 (available with Xcode 8.3 beta), you can lift this burden from the caller by defining your own initialiser specifically for BaseProtocol
in an extension:
extension AnySignal where T == BaseProtocol {
init<U : BaseProtocol>(_ base: Signal<U>) {
self.init(base, transform: { $0 })
}
}
(and repeat for any other protocol types you want to convert to)
Now you can just say:
let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal),
AnySignal(anotherSignal)
]
(You can actually remove the explicit type annotation for the array here, and the compiler will infer it to be [AnySignal<BaseProtocol>]
– but if you're going to allow for more convenience initialisers, I would keep it explicit)
The solution for value types, or reference types where you want to specifically create a new instance, to is perform a conversion from Signal<T>
(where T
conforms to BaseProtocol
) to Signal<BaseProtocol>
.
In Swift 3.1, you can do this by defining a (convenience) initialiser in an extension for Signal
types where T == BaseProtocol
:
extension Signal where T == BaseProtocol {
convenience init<T : BaseProtocol>(other: Signal<T>) {
self.init(t: other.t)
}
}
// ...
let signals: [Signal<BaseProtocol>] = [
Signal(other: childSignal),
Signal(other: anotherSignal)
]
Pre Swift 3.1, this can be achieved with an instance method:
extension Signal where T : BaseProtocol {
func asBaseProtocol() -> Signal<BaseProtocol> {
return Signal<BaseProtocol>(t: t)
}
}
// ...
let signals: [Signal<BaseProtocol>] = [
childSignal.asBaseProtocol(),
anotherSignal.asBaseProtocol()
]
The procedure in both cases would be similar for a struct
.
Swift issue: About Type Casting for AnyObject
This is a little confusing because of the bridging that Swift does to Objective-C, where most things are classes and there are fewer value-types. To see what Swift does without the bridge, create a new playground and delete the import ...
statement at the top, then try casting an Int
to Any
(no problem) and AnyObject
:
let num = 23 // 23
let anyNum: Any = num // 23
let anyObjectNum: AnyObject = num
// error: type 'Int' does not conform to protocol 'AnyObject'
Now add import Foundation
at the top of your playground:
import Foundation
let num = 23 // 23
let anyNum: Any = num // 23
let anyObjectNum: AnyObject = num // 23
The error goes away - why? When it sees the attempt to cast an Int
to AnyObject
, Swift first bridges num
to be an instance of the Objective-C NSNumber
class, found in Foundation
, then casts it to the desired AnyObject
. The same thing is happening in your array.
You can more or less prove this is the case using the is
keyword - since NSNumber
bridges to all the numeric types in Swift, it returns true
in some funny cases:
anyNum is Int // true
anyNum is Double // false
anyObjectNum is Int // true
anyObjectNum is UInt // true
anyObjectNum is Double // true
anyObjectNum is Float // true
Cannot convert value of type 'Character' to type 'String' in coercion
Looking at the documentation, you can see, that Character
is just one of the several representations of String
in Swift
(emphasis added to the relevant parts by me)
A string is a series of characters, such as "hello, world" or
"albatross". Swift strings are represented by the String type. The
contents of a String can be accessed in various ways, including as a
collection of Character values.
In Swift
, String
is not just an array of Character
s unlike in some other languages. In Swift
, Character
is just a way to represent a String
instance in a certain way. String
s can be represented using View
s, such as CharacterView
, utf8View
, etc.
One of the key principles behind the architecture of Swift
's String
type was Unicode correctness, which is one of the reasons String
s are not just simply an array of Character
s.
For more information about the changes to String
in Swift4
, see the String Manifesto.
To be more specific about why casting doesn't work. There are two kinds of castings, type casting and bridge-casting. Type casting is only possible between classes, where inheritance is involved. You can either upcast a subclass to its superclass, which always succeeds or you can try to downcast a superclass to a subclass, which only works if a subclass instance was first upcasted to its superclass.
It should be quite clear from the above explanation why type casting doesn't work between Character
and String
, since the neither of the two types inherit from each other.
For bridge casting, this is a method Apple introduced for interoperability between some Swift
and Foundation
types, such as String
and NSString
, but since both String
and Character
are Swift
types, bridge casting has nothing to do with this problem either.
How to fix 'Cannot convert value of type '[Any]' to type 'String' in coercion' error in swift
Got the solution:
var arr = data[0]
yourString: String = (arr as AnyObject).description
Swift 4 - Generic Type Conversion issue - Cannot convert to expected type '(_) - Void'
In your f2
, the type of closure
is @escaping (ValueType, @escaping (ValueType2) -> Void) -> Void
.
The type of the first parameter is ValueType
.
But you are using it as:
closure(value!, s1.f2)
The type of the first argument value!
is Value<ValueType>
, not ValueType
.
If you change the type of closure
to @escaping (Value<ValueType>, @escaping (ValueType2) -> Void) -> Void
, your code would compile without any issue.
You may have found some issue about generics, but at least, your code example is not describing the issue.
Related Topics
In Swift, How to Extend a Typealias
"Raw Value for Enum Case Is Not Unique" for Swift Enum with Float Raw Values
Swift 3: Converting Data to String Returns a Nil Value
Getting Data Out of Nsdata with Swift
Generic Within a Generic in Swift
Ios15 Uttype Deprecations for Url-Extension
Sktexture Nearest Filtering Mode Doesn't Work (Making Pixel Art)
Use Background Image on Uisearchcontroller iOS 11
Can't Create Default Closure Parameter in Array Extension Method in Swift
How to Implement a Billboard Effect (Lookat Camera) in Realitykit
Nssortdescriptor Sorting Using Nsdate in Swift
How to Set Priority on Constraints in Swift
How to Define Static Constant in a Generic Class in Swift
Swift/Cloudkit: After Record Changed, Upload Triggers "Service Record Changed"
Swift- How to Display Image Over Button
How to Convert an Anykeypath to a Writablekeypath