Type information loss when comparing generic variables with an array of metatypes

This has plagued me for eons. I'm not even sure which exact terms to use to describe this issue in its most simple form.

Basically:

class ComponentA: Component {}
class ComponentB: Component {}
class ComponentC: Component {}

// func findComponent <ComponentType: Component> (ofType componentClass: ComponentType.Type,
//                                               in entity: [Component]) -> ComponentType?
// ...

// Correctly returns componentA
findComponent(ofType: ComponentA.self, in: [componentA, componentB, componentC])

// Correctly fails to find componentC
findComponent(ofType: ComponentC.self, in: [componentA, componentB])

// Correctly returns componentB
let componentsToFind = [ComponentB.self]
findComponent(ofType: componentsToFind[0], in: [componentA, componentB, componentC])

// Incorrectly returns componentA because it now matches it with `Component` (the superclass)
let componentsToFind2 = [ComponentB.self, ComponentC.self]
findComponent(ofType: componentsToFind2[0], in: [componentA, componentB, componentC])

For the full example, please see this gist and try it in an Xcode Playground:

In coComponent, ComponentType is the generic type known at compile time, while componentClass is a type at runtime which could be a subtype of ComponentType. What you really want is to search for RelayComponent<componentClass>.self, but I don't think there's a way to do that in Swift without a giant switch statement over all possible subclasses of the class, though maybe someone else knows of a way.

The issue can be simplified as this:

func printInfo<C: Component>(c: C.Type) {
	print("\(C.self) vs \(c)")
}
printInfo(c: TestComponentA.self as Component.Type)

This will print Component vs TestComponentA, while your code assumed that it would be TestComponentA vs TestComponentA

Here's an alternate way to do things that fixes it:
Add two computed properties to Component (you may want to use different names):

var componentType: Component.Type? { return type(of: self) }
var baseComponent: Component? { return self }

Override those methods in RelayComponent:

override var componentType: Component.Type? { return target?.componentType }
override var baseComponent: Component? { return target?.baseComponent }

Use those instead of type(of:) in component(ofType:):

func component <ComponentType> (ofType componentClass: ComponentType.Type) -> ComponentType?
	where ComponentType : Component
{
	return components.first { component in
		return (component.componentType == componentClass) // (component is ComponentType)
	}?.baseComponent as? ComponentType
}

(You no longer need the explicit check for RelayComponent in coComponent, relay components will now be found and unwrapped by component(ofType:))

1 Like

Thanks!

Ah but this is a little tricky, because I cannot change the implementation of component(ofType:) as it's an Apple API (from GameplayKit.GKEntity)

Overriding declarations in extensions is not supported
Overriding non-open instance method outside of its defining module

Update: Fixed!

I modified another method that I had added to GKEntity, componentOrRelay(ofType:).

Although it's not a 100% fix because it doesn't apply to the base GKEntity.component(ofType:), it's good enough for my purposes. :)

Thanks again!

Terms of Service

Privacy Policy

Cookie Policy