Subclass constraint acts as default generic?

Consider the following example:

class SuperDooperFoo {
    required init() {}
}

class SuperFoo: SuperDooperFoo {
    required init() {}
}

class Foo: SuperFoo {
    required init() { super.init() }
}

func create<T: SuperFoo>() -> T {
    return T()
}

func ffoo() -> SuperDooperFoo {
    return create()
}

I was expecting an error in ffoo() call - since there's no indication of concrete type except for subclass constraint. In actuality Swift infers and uses SuperFoo as a type. I can always add:

func ffoo() -> SuperDooperFoo {
    return create() as Foo
}

to supply another SuperFoo descendant. Is this behavior expected? I thought that I would always need to supply a concrete type to the generic function.

Note: Given below is my understanding, I could be wrong.

In each of those cases you are indirectly giving explicit types based on either the requirement of the return type of create (case 1) or the casting (case 2).

Case 1:

func ffoo() -> SuperDooperFoo {
    return create()
}

Explanation:

  • Based on definition of create, T needs to be an instance of SuperFoo (or a subclass of SuperFoo).
  • Any instance of SuperFoo is also an instance of SuperDooperFoo .
  • So in this case T is of the type SuperFoo.
  • It satisfies the constraints and so compiles without errors.

Case 2:

func ffoo() -> SuperDooperFoo {
    return create() as Foo
}

Explanation:

  • Based on definition of create, T needs to be an instance of SuperFoo (or a subclass of SuperFoo).
  • By specifying as Foo, you are defining the return type of create.
  • Since there is a casting to Foo the expectation is T is of the type Foo.
  • Any instance of Foo is also an instance of SuperDooperFoo.
  • So it satisfies the constraints and therefore compiles without errors.

After adding log statements:

class SuperDooperFoo {
    required init() {
        print("SuperDooperFoo - init")
    }
}

class SuperFoo: SuperDooperFoo {
    required init() {
        print("SuperFoo - init")
    }
}

class Foo: SuperFoo {
    required init() {
        print("Foo - init")
        super.init()
    }
}

func create<T: SuperFoo>() -> T {
    return T()
}

func ffoo1() -> SuperDooperFoo {
    print("ffoo1\n=========")
    return create()
}

func ffoo2() -> SuperDooperFoo {
    print("ffoo2\n=========")
    return create() as Foo
}

//Invoking the functions:
ffoo1()
print("\n")
ffoo2()

Output:

ffoo1
=========
SuperFoo - init
SuperDooperFoo - init


ffoo2
=========
Foo - init
SuperFoo - init
SuperDooperFoo - init
3 Likes

Thank you for your detailed response. This is what I understand as well.
The thing I found surprising was that in case 1 generic was resolved based only on SuperFoo constraint (as an instance of SuperFoo).