1

I have the following code:

protocol TestProtocol {
    var string: String {get}
}

struct TestStructOne : TestProtocol {
    let string = "test string"
    var stringTwo = "test string three"
}

struct TestStructTwo : TestProtocol {
    let string = "test string two"
}

var testStructOnes = [TestStructOne(), TestStructOne(), TestStructOne()]

// works
var protocolArrayOne: [TestProtocol] = [TestProtocol]()
for testStruct in testStructOnes.filter({ $0.stringTwo == "test string three" }) {
    protocolArrayOne.append(testStruct)
}

// does not work, cannot copnvert value of type '[TestStructOne]' to specified type '[TestProtocol]'
var protocolArrayTwo: [TestProtocol] = testStructOnes.filter({ $0.stringTwo == "test string three" })

I don't understand why the last line doesn't work. Can anybody fill me in? I don't see how it is different than iterating through the array and manually adding each element.

Hamish
  • 78,605
  • 19
  • 187
  • 280
FreaknBigPanda
  • 1,127
  • 12
  • 17

1 Answers1

3

This is due to a limitation of Swift arrays when it comes to implicitly converting their type, which is a consequence on the invariance of generics in Swift, and is discussed more in this Q&A.

The key to the solution is as you cannot convert the array directly from [TestStructOne] to [TestProtocol] – instead you have to convert each element separately, which is why your method of using a for loop and manually appending the elements to a new array worked.

The simplest way to solve this problem with functional programming is usually through using map in order to allow each element to get up-cast to TestProtocol (Swift can infer this from the explicit type annotation):

let protocolArrayTwo: [TestProtocol] = testStructOnes.filter {
    $0.stringTwo == "test string three"
}.map{$0}

However, in your case this is inefficient as you're having to iterate through your array twice, which incidentally is also what your for loop is doing (filtering and then looping over the elements).

Instead, you could use a for loop with a given where condition in order to filter out the elements you want by appending them to a new array in a single iteration:

var protocolArray = [TestProtocol]()
for testStruct in testStructOnes where testStruct.stringTwo == "test string three"  {
    protocolArray.append(testStruct)
}

Or if you like functional programming, you could use flatMap in order to both filter out the elements you don't want (by taking advantage of the fact that flatMap filters out nil) and transforming them to your protocol type:

let protocolArray : [TestProtocol] = testStructOnes.flatMap {
    $0.stringTwo == "test string three" ? $0 : nil
}

Much like the map, the explicit type annotation here will allow flatMap to implicitly convert each element individually to your TestProtocol.

Community
  • 1
  • 1
Hamish
  • 78,605
  • 19
  • 187
  • 280