I'm using swift 5 and try to compile the following code:
protocol BasicProtocol {
associatedtype T
var str: T {get set}
}
struct AItem<U>: BasicProtocol {
typealias T = U
var str: T
init<G: StringProtocol>(str: G) where G == T {
self.str = str
}
}
I got compilation error:
error: Test.playground:10:45: error: same-type requirement makes generic parameters 'G' and 'U' equivalent
init<G: StringProtocol>(str: G) where G == T {
^
How to make them equivalent? or I can't?
Thanks.
Update 1:
This is the problem I encountered: I want to declare struct "AItem", hoping it has a generic type "T". And this generic type will have some restrictions, such as: "T: StringProtocol". Then for some reason, I need to use an array to load these structs, and ensure that the generics of each structure can be set at will.
I learned that there is "type-erase" might can solve this. So I tried this way, but it seemed unsuccessful. The problems mentioned above have occurred.
Update 2:
struct AItem<T: StringProtocol> {
var aStr: T
}
var array: [AItem<Any>] = [AItem(aStr: "asdfasdf")]
Look,If you compile this code, you will get a compilation error:
error: Test.playground:5:13: error: type 'Any' does not conform to protocol 'StringProtocol'
var array: [AItem<Any>] = [AItem(aStr: "asdfasdf")]
^
If I use "var array: [AItem<String>]", I will not be able to put any other non-"String" but implemented "StringProtocol" instance in the array.
This is why I said I want "ensure that the generics of each structure can be set at will".
Update 3:
very thanks for @jweightman, now I update my question again.
protocol ConstraintProtocol {}
extension String: ConstraintProtocol{}
extension Data: ConstraintProtocol{}
extension Int: ConstraintProtocol{}
.......
struct AItem<T = which class has Implemented "ConstraintProtocol"> {
var aPara: T
init(aPara:T) {
self.aPara = aPara
}
}
// make a array to contain them
var anArray: [AItem<Any class which Implemented "ConstraintProtocol">] = [AItem(aPara: "String"), AItem(aPara: 1234), AItem(aPara: Data("a path")), …]
// then I can use any item which in anArray. Maybe I will implement a method to judge these generics and perform the appropriate action.
for curItem in anArray {
var result = handleItem(curItem)
do something...
}
func handleItem<T: ConstraintProtocol>(item: AItem<T>) -> Any? {
if (item.T is ...) {
do someThing
return ......
} else if (item.T is ...) {
do someThing
return ...
}
return nil
}
This is my whole idea, but all of which are pseudo-code.
It seems like type erasure is the answer to your problem. The key idea to the type erasure pattern is to put your strongly typed but incompatible data (like an AItem<String>
and an AItem<Data>
) inside of another data structure which stores them with "less precise" types (usually Any
).
A major drawback of type erasure is that you're discarding type information—if you need to recover it later on to figure out what you need to do with each element in your array, you'll need to try to cast your data to each possible type, which can be messy and brittle. For this reason, I've generally tried to avoid it where possible.
Anyways, here's an example of type erasure based on your pseudo code:
protocol ConstraintProtocol {}
extension String: ConstraintProtocol{}
extension Data: ConstraintProtocol{}
extension Int: ConstraintProtocol{}
struct AItem<T: ConstraintProtocol> {
var aPara: T
init(aPara: T) {
self.aPara = aPara
}
}
struct AnyAItem {
// By construction, this is always some kind of AItem. The loss of type
// safety here is one of the costs of the type erasure pattern.
let wrapped: Any
// Note: all the constructors always initialize `wrapped` to an `AItem`.
// Since the member variable is constant, our program is "type correct"
// even though type erasure isn't "type safe."
init<T: ConstraintProtocol>(_ wrapped: AItem<T>) {
self.wrapped = wrapped
}
init<T: ConstraintProtocol>(aPara: T) {
self.wrapped = AItem(aPara: aPara);
}
// Think about why AnyAItem cannot expose any properties of `wrapped`...
}
var anArray: [AnyAItem] = [
AnyAItem(aPara: "String"),
AnyAItem(aPara: 1234),
AnyAItem(aPara: "a path".data(using: .utf8)!)
]
for curItem in anArray {
let result = handleItem(item: curItem)
print("result = \(result)")
}
// Note that this function is no longer generic. If you want to try to "recover"
// the type information you erased, you will have to do that somewhere. It's up
// to you where you want to do this.
func handleItem(item: AnyAItem) -> String {
if (item.wrapped is AItem<String>) {
return "String"
} else if (item.wrapped is AItem<Data>) {
return "Data"
} else if (item.wrapped is AItem<Int>) {
return "Int"
}
return "unknown"
}
An alternative to type erasure you could consider, which works well if there's a small, finite set of concrete types your generic could take on, would be to use an enum with associated values to define a "sum type". This might not be a good choice if the protocol you're interested in is from a library that you can't change. In practice, the sum type might look like this:
enum AItem {
case string(String)
case data(Data)
case int(Int)
}
var anArray: [AItem] = [
.string("String"),
.int(1234),
.data("a path".data(using: .utf8)!)
]
for curItem in anArray {
let result = handleItem(item: curItem)
print("result = \(result)")
}
func handleItem(item: AItem) -> String {
// Note that no casting is required, and we don't need an unknown case
// because we know all types that might occur at compile time!
switch item {
case .string: return "String"
case .data: return "Data"
case .int: return "Int"
}
}