Extend Swift Array to Filter Elements by Type

落花浮王杯 提交于 2019-12-24 00:47:17

问题


How can a swift array be extended to access members of a particular type?

This is relevant if an array contains instances of multiple classes which inherit from the same superclass. Ideally it would enforce type checking appropriately.


Some thoughts and things that don't quite work:

Using the filter(_:) method works fine, but does enforce type safety. For example:

protocol MyProtocol { }
struct TypeA: MyProtocol { }
struct TypeB: MyProtocol { }

let myStructs:[MyProtocol] = [ TypeA(), TypeA(), TypeB() ]

let filteredArray = myStructs.filter({ $0 is TypeA })

the filteredArray contains the correct values, but the type remains [MyProtocol] not [TypeA]. I would expect replacing the last with let filteredArray = myStructs.filter({ $0 is TypeA }) as! [TypeA] would resolve that, but the project fails with EXEC_BAD_INSTRUCTION which I do not understand. Perhaps type casting arrays is not possible?

Ideally this behavior could be wrapped up in an array extension. The following doesn't compile:

extension Array {
    func objectsOfType<T:Element>(type:T.Type) -> [T] {
        return filter { $0 is T } as! [T]
    }
}

Here there seem to be at least two problems: the type constraint T:Element doesn't seem to work. I'm not sure what the correct way to add a constraint based on a generic type. My intention here is to say T is a subtype of Element. Additionally there are compile time errors on line 3, but this could just be the same error propagating.


回答1:


SequenceType has a flatMap() method which acts as an "optional filter":

extension SequenceType {
    /// Return an `Array` containing the non-nil results of mapping
    /// `transform` over `self`.
    ///
    /// - Complexity: O(*M* + *N*), where *M* is the length of `self`
    ///   and *N* is the length of the result.
    @warn_unused_result
    @rethrows public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]
}

Combined with matt's suggestion to use as? instead of is you can use it as

let myStructs:[MyProtocol] = [ TypeA(), TypeA(), TypeB() ]
let filteredArray = myStructs.flatMap { $0 as? TypeA }

Now the type of filteredArray is inferred as [TypeA].

As an extension method it would be

extension Array {
    func objectsOfType<T>(type:T.Type) -> [T] {
        return flatMap { $0 as? T }
    }
}

let filteredArray = myStructs.objectsOfType(TypeA.self)

Note: For Swift >= 4.1, replace flatMap by compactMap.




回答2:


Instead of testing (with is) how about casting (with as)?

let myStructs:[MyProtocol] = [ TypeA(), TypeA(), TypeB() ]
var filteredArray = [TypeA]()
for case let t as TypeA in myStructs {filteredArray.append(t)}



回答3:


Casting arrays does not work in Swift. This is because arrays in Swift use generics, just like you can't cast a custom class, where only the type T changes. (class Custom<T>, Custom<Int>() as! Custom<String>).

What you can do is create an extension method to Array, where you define a method like this:

extension Array {
    func cast<TOut>() -> [TOut] {
        var result: [TOut] = []
        for item in self where item is TOut {
            result.append(item as! TOut)
        }
        return result
    }
}



回答4:


I think the canonical FP answer would be to use filter, as you are, in combination with map:

let filteredArray = myStructs.filter({ $0 is TypeA }).map({ $0 as! TypeA })

alternatively, you can use reduce:

let filtered2 = myStructs.reduce([TypeA]()) {
    if let item = $1 as? TypeA {
        return $0 + [item]
    } else {
        return $0
    }
}

or, somewhat less FP friendly since it mutates an array:

let filtered3 = myStructs.reduce([TypeA]()) { ( var array, value )  in
    if let item = value as? TypeA {
        array.append(item)
    }
    return array
}

which can actually be shortened into the once again FP friendly flatMap:

let filtered4 = myStructs.flatMap { $0 as? TypeA }

And put it in an extension as:

extension Array {
    func elementsWithType<T>() -> [T] {
        return flatMap { $0 as? T }
    }
}

let filtered5 : [TypeA] = myStructs.elementsWithType()


来源:https://stackoverflow.com/questions/33112114/extend-swift-array-to-filter-elements-by-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!