Store objects of generic types in a data structure


(Hugo Lundin) #1

Dear all,

I am currently playing with a concept in Swift to store statistics in apps using generic types. One of the key parts are the ability to register properties (for example booleans, strings and integers - but also more complex data types such as dates):

stats.add(description: "iOS Version", property: {
   return UIDevice.current.systemVersion as? String
}))

stats.add(description: "HealthKit authorized", property: {
   return HealthKit.authorized
}))

I've played with different kind of implementations of this, but feel that Swift limits me in some ways, because I can not find a way to store different types in a list.

I have tried to implement it in Java, and this works:

import java.util.ArrayList;
import java.util.List;
public class Property<T> {
    private String description;
    private T property;

    public Property(String description, T property){
        this.description = description;
        this.property = property;
    }

    public String getDescription() {
        return description;
    }

    public T getProperty() {
        return property;
    }
}

public class Analytics {
    private List<Property> properties = new ArrayList<>();

    public Analytics() {

    }

    public static void main(String[] args) {
        Analytics stats = new Analytics();
        stats.add(new Property("HealthKit", false));
        stats.add(new Property("iOS Version", "9.3"));
        stats.show();
    }

    public void add(Property property) {
        properties.add(property);
    }

    public void show() {
        for (Property p : properties) {
            System.out.println(p.getDescription() + ": " + p.getProperty());
        }
    }
}

How would I be able to achieve this in Swift? My current idea of this approach does not seem to work, but also the idea to create specific subclasses for every type (and then type cast the different properties), gives a lot of work, and there would be a limitation for which types are available.

Thank you for your time.

Hugo Lundin.


(Brent Royal-Gordon) #2

I would create a protocol with the same interface (other than initializers) as your `Property` type, but with an `Any` equivalent to any `T`-taking API. Then make your `Array` use the protocol, not `Property` itself, as its element type.

  // Let's say this is your existing type:
  struct Property<T> {
    let description: String
    let property: T
    
    init(description: String, property: T) {
      self.description = description
      self.property = property
    }
  }

  // Create a protocol equivalent that doesn't use T:
  protocol AnyProperty {
    var description: String { get }
    var anyProperty: Any { get }
  }
  
  // Then conform `Property` to it:
  extension Property: AnyProperty {
    var anyProperty: Any {
      return property
    }
  }
  
  // Now, in your Analytics class:
  class Analytics {
    private var properties: [AnyProperty]
    
    func add(_ property: AnyProperty) {
      properties.append(property)
    }
    
    func show() {
      for p in properties {
        print("\(p.description): \(p.anyProperty)")
      }
    }
  }

  // And do:
  let stats = Analytics()
  stats.add(Property(description: "HealthKit", property: false))
  stats.add(Property(description: "iOS Version", property: "9.3"))
  stats.show()

This is called "type erasure"; we say that the `AnyProperty` protocol is "erasing" the type of the `T` generic parameter because it essentially hides it behind the protocol. Java automatically erases all type parameters, which is convenient here, but it can lead to bugs. (For instance, the generic parameter of `List` is similarly erased, so you can cast a `List<Property>` up to `List<Object>`, then add non-`Property` objects to it. When you later run `show()`, it will throw an exception.) Swift is more strict, which prevents bugs but sometimes means you need to explicitly erase types.

Here we've used a protocol to erase the type of `Property`, but that often isn't an option—for instance, when you cast an instance to a protocol type, the "wrapper" around it doesn't itself conform to any protocols. When it isn't, you have to make a more complicated type-erasing wrapper—usually either by making a closure that calls each method and storing them in properties, or by writing a non-generic abstract superclass which declares all the visible members and a generic subclass that overrides the superclass declarations.

Here's an article on how to write type-erased wrappers in more complicated situations: <https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/>

Hope this helps,

···

On Apr 8, 2017, at 5:22 AM, Hugo Lundin via swift-users <swift-users@swift.org> wrote:

How would I be able to achieve this in Swift? My current idea of this approach does not seem to work, but also the idea to create specific subclasses for every type (and then type cast the different properties), gives a lot of work, and there would be a limitation for which types are available.

--
Brent Royal-Gordon
Architechies