Swift generics and subclasses - arrays

my question might be simple, but it got me puzzled a bit:
Imagine I have an array of different objects which all have a common parent class "MyClass".
var values = [MyClass]()
However they all have their specific subclass like for example "MyClassSubclass".
values.append(MyClassSubclass())
I now want to create a generic method returning me the first object inside this array which is of type MyClassSubclass. I would like to prevent casting the object, but instead have a generic method which takes the subclass object as T parameter and returns me the first occurrence of subclass T inside this array.
I thought of something like this (but surely that does not work):
func getFirst<T: MyClass>(_ ofType : T.Type) -> T?
I guess I'm just stuck and I don't know what to search for, so if someone could help me I would greatly appreciate it.
Edit Example based on the above values:
class MyClass {}
class MyClassSubclass : MyClass {}
class MyClassSubclass2 : MyClass{}
var values = [MyClass]()
values.append(MyClassSubclass())
values.append(MyClassSubclass2())
//Return the first class element appearing here as a subclass type
func getFirst<T>(_ ofType : T.Type) -> T?{}
Thanks

One approach is to iterate over the array and use optional binding
to check if an element is of the given type:
func getFirst<T: MyClass>(ofType: T.Type) -> T? {
for elem in values {
if let item = elem as? T {
return item
}
}
return nil
}
This can be simplified using for case with the as pattern:
func getFirst<T: MyClass>(ofType: T.Type) -> T? {
for case let item as T in values {
return item
}
return nil
}
Another approach is to use flatMap to find all items of the given
type and return the first one:
func getFirst<T: MyClass>(ofType: T.Type) -> T? {
return values.flatMap { $0 as? T }.first
}
If the array can be large and you want to avoid the creation of an
intermediate array then you can use lazy:
func getFirst<T: MyClass>(ofType: T.Type) -> T? {
return values.lazy.flatMap { $0 as? T }.first
}
As an array extension method this would be
extension Array {
func getFirst<T>(ofType: T.Type) -> T? {
return flatMap { $0 as? T }.first
}
}

If you'd like to use global function, which is not recommended, try this
func getFirst<T>(ofType: T.Type) -> T? where T: MyClass {
for value in values where value is T {
return value as? T
}
return nil
}
let first = getFirst(ofType: MyClassSubclass2.self)
The first answer should be better for Swift.

This feels a bit like abuse of generics, but here's an extension:
extension Array where Element == MyClass {
func getFirst<T>(_ ofType: T.Type) -> T? {
return self.first(where: { ofType == type(of: $0) }) as? T
}
}
The method can then be called as let first = values.getFirst(MyClassSubclass.self).
I'd personally prefer simply casting inline for clarity:
let first = values.first(where: { type(of: $0) == MyClassSubclass.self }) as? MyClassSubclass

Related

How to Add an Extension to Swift Array To Conditionally Append?

Is there a way to do this as an extension to Array as opposed to a switch statement that's going to grow and grow?
fileprivate var exteriorColorOptions = [ExteriorColorOption]()
fileprivate var otherOptions = [SomeOtherOption]()
: more options
func add(option:FilteredOption) {
switch(option) {
case let thing as ExteriorColorOption:
exteriorColorOptions.append(thing)
case and on and on
default:
break
}
}
I would like to be able to just do the following with the right extension in place:
exteriorColorOptions.appendIfPossible(option)
otherOptions.appendIfPossible(option)
Note: switch approach came from
Swift: Test class type in switch statement
This should work:
extension Array {
mutating func appendIfPossible<T>(newElement: T) {
if let e = newElement as? Element {
append(e)
}
}
}
The conditional cast newElement as? Element succeeds if the
new element conforms to or is an instance of (a subclass of) the arrays element type Element.
Example:
class A {}
class B: A {}
class C {}
var array: [A] = []
array.appendIfPossible(newElement: B())
print(array) // [B]
array.appendIfPossible(newElement: C())
print(array) // [B]
Actually the answer is correct but maybe not exactly what you want:
extension Array {
mutating func safeAppend(newElement: Element?) {
if let element = newElement {
append(element)
}
}
This one will throw a compile time error in case you try appending an element thats not of the arrays original type.
E.g. you will see an error if you try to append an Int to an array of string [String].

In Swift how can I filter an array of objects conforming to a protocol by their class?

I have a protocol, 'VariousThings', and two classes which conform to it, 'ThingType1' and 'ThingType2'. I've put some objects of these two types of classes into an array containing 'VariousThings'. I now want to just take all the objects out of that array that are of class type 'ThingType2' for example. How can I do this?
Here's what I have so far:
protocol VariousThings: class {
}
class ThingType1: VariousThings {
}
class ThingType2: VariousThings {
}
let array: [VariousThings] = [ThingType1(), ThingType2()]
func itemsMatchingType(type: VariousThings.Type) -> [VariousThings] {
return array.filter { variousThing in
return (variousThing.self === type)
}
}
let justThingTypes1: [VariousThings] = itemsMatchingType(ThingType1)
I would use compactMap instead of filter here in order to give you better type safety. You can use a conditional downcast to filter out the elements you want and generics in order to preserve type information. This takes advantage of the fact that compactMap can filter out nil results from the transform function.
let array: [VariousThings] = [ThingType1(), ThingType2()]
func itemsMatchingType<T : VariousThings>(_ type: T.Type) -> [T] {
return array.compactMap { $0 as? T }
}
let justThingTypes1 = itemsMatchingType(ThingType1.self) // of type [ThingType1]
Now the array you get out of your itemsMatchingType function is [ThingType1] if you pass in ThingType1, rather than simply [VariousThings]. That way you don't have to deal with ugly forced downcasts later down the line.
You could use a generic
func itemsMatchingType<T : VariousThings>(type: T.Type) -> [VariousThings] {
return array.filter { $0 is T }
}
You can use the filter for this:
let justThingsTypes1 = array.filter { $0 is ThingType1 }
let justThingTypes1: [VariousThings] = array.filter {
variousThing in
return Mirror(reflecting: variousThing).subjectType == ThingType1.self
}

Store generic Arrays in NSUserDefaults

I'm trying to store a generic Array in NSUserDefaults but I get the following error: Cannot convert value of type 'Array<T>' to expected argument type 'AnyObject?'.
How can I solve this problem?
public class PropertyStore {
private let userDefaults = NSUserDefaults.standardUserDefaults()
public func loadSet<T>(key: String) -> Set<T>? {
guard let array = userDefaults.objectForKey(key) as? [T] else {
return nil
}
return Set<T>(array)
}
public func saveSet<T>(key: String, value: Set<T>) {
let array = Array(value)
userDefaults.setObject(array, forKey: key) // <- ERROR
}
}
Like #lucasD said, T needs to conform to NSCoding atleast. So the code looks like this.
public func saveSet<T: NSCoding>(key: String, value: Set<T>) {
let array = Array(value)
userDefaults.setObject(array, forKey: key)
}
However, this will not work for many reasons like:
public func loadSet<T: NSCoding>(key: String) -> Set<T>? {
guard let array = userDefaults.objectForKey(key) as? [T] else {
return nil
}
return Set<T>(array)
}
let defaults = PropertyStore()
defaults.saveSet("array", value: [1,2,3])
///defaults.loadSet<Int>("array") ===> Cannot explicitly specialize a function
///defaults.loadSet("array") ===> Cannot infer Type T
//let a: Set<Int>? = defaults.loadSet("array") ==> T cannot be inferred
In the case of loadSet type T cannot be inferred properly because we cannot specify it from outside, as far as i know. I would first try to return NSObject or Set<AnyObject> or Set<NSCoding> and then type cast it explicitly. Let me know if theres a better way though.
You can take a look at this SO post for more information on why a generics parameter cannot be specialised from outside. SO Generics specialization
You should define T as NSCoding conforming class. So when you're going to store the array, you are going to store an array of NSKeyedArchiver.archiveDataWithRootObject() results. Then, to return a Set<T> in the loadSet method, you should unarchive all the objects:
let storedData = NSUserDefaults.standardUserDefaults. ...
return storedData.flatMap { return NSKeyedUnarchiver.unarchiveObjectWithData($0) }

Extend Swift Array to Filter Elements by Type

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.
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.
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)}
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
}
}
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()

Comparing protocol references

I have an array of protocols. now I want to remove an item from the array, by finding the index of the protocol with the array. however, when comparing the protocol object with the items in the array, the compiler warns with:
'Protocol' does not conform to AnyObject
protocol SomeProtocol {}
var list:[SomeProtocol] = []
func add(some:SomeProtocol) { list+=some }
func remove(some:SomeProtocol) {
var index = -1
for i in 0...list.count-1 { if [i] === some { index = i } }
if index >= 0 { list.removeAtIndex(index) }
}
If you derive only classes for the protocol, you can change protocol definition to:
protocol SomeProtocol: class {}
Then you will be able to use references with this protocol.
First of all, doing add is super easy, just include this function to make it work:
func +=(inout lhs: [SomeProtocol], rhs: SomeProtocol) {
lhs.append(rhs)
}
Doing remove is a lot trickier because SomeProtocol could apply equally to a class or struct, and only values with class types can be compared with ===.
We could use the == operator instead, but it only takes values that conform to the Equatable protocol, and Equatable can only be used as a generic constraint (so far), otherwise you could use something like protocol<SomeProtocol,Equatable> as your array element type.
If you know for sure that your SomeProtocol will only be applied to classes, consider refactoring your code to work with that class type instead:
protocol SomeProtocol {}
class SomeClass : SomeProtocol {}
var list:[SomeClass] = []
func add(some:SomeClass) {
list += some
}
func remove(some:SomeClass) {
list -= some
}
func +=(inout lhs: [SomeClass], rhs: SomeClass) {
lhs.append(rhs)
}
func -=(inout lhs: [SomeClass], rhs: SomeClass) {
for (i,v) in enumerate(lhs) {
if v === rhs {
lhs.removeAtIndex(i)
break
}
}
}
If you also happen to make SomeClass conform to Equatable the usual array functions will work automatically, and you won't even need to overload += and -=.
Otherwise, if you can't know whether your value will be a class or a struct, it might be better to think about what it means for values of SomeProtocol to be "equal" and require a comparison method:
protocol SomeProtocol {
func isEqualTo(some: SomeProtocol) -> Bool
}
func -=(inout lhs: [SomeProtocol], rhs: SomeProtocol) {
for (i,v) in enumerate(lhs) {
if v.isEqualTo(rhs) {
lhs.removeAtIndex(i)
break
}
}
}
// add functions work the same as above
Alternatively, you could use your original code and write a global comparison function:
func ===(lhs: SomeProtocol, rhs: SomeProtocol) -> Bool {
// return something based on the properties of SomeProtocol
}
I end up using isEqual(to: ) in my protocols to test for instance comparisons:
public protocol fooProtocol {
...
func isEqual(to: fooProtocol) -> Bool
}
public extension fooProtocol {
func isEqual(to: fooProtocol) -> Bool {
let ss = self as! NSObject
let tt = to as! NSObject
return ss === tt
}
}
Seems to work for me.

Resources