3 Nuances of Swift Extensions

How often do we take an initial cursory look at some documentation, shake our heads and say, “Ok, sure! Got it!”, and then some time later get to the actual usage of that perceived understanding only to find out, “Woah – this is behaving differently than I expected! I wonder if the documentation says anything about this?!”

A few discussions I’ve had recently have prompted me to question what I thought I knew about Swift extensions. I have read documentation about extensions and I thought I understood them pretty thoroughly. However, these conversations, along with some experimentation done on my own revealed a few nuances that I didn’t pick up on before.

Update: Almost immediately after publishing this article, the Swift community chimed in and helped me figure out my fundamental hiccup which prompted the aforementioned experimentation in the first place. I’ve written a follow-up article called “Clarifying Swift Access Control”, describing that misunderstanding. I recommend giving that one a read to avoid making the same mistake I did!

Three nuances of extensions

In particular, the following three nuances challenged what I thought I knew about Swift extensions:

  1. The visibility Swift extensions have into the Type they’re extending. Can they see things marked private, for example?
  2. How that visibility is affected by where the extension is defined. If I have the source for a Type that I’m writing an extension for, does defining it within that same source file vs defining it in a separate file affect what it can “see”?
  3. The default access modifiers of the extension’s “members” and how specifying them or not specifying them affect what an extension exposes as public API for the Type it’s extending.

Before I begin, suppose that I have a public struct called Person. It has some private properties, name, gender, and age. An enum encapsulates the idea of Gender. The struct looks something like this:

 1public struct Person {
 2    private var name: String
 3    private var gender: Gender
 4    private var age: Int
 5    
 6    public init(name: String, gender: Gender, age: Int) {
 7        self.name = name
 8        self.gender = gender
 9        self.age = age
10    }
11    
12    public func howOldAreYou() -> String {
13        return formattedAge()
14    }
15    
16    // private func, simply to show extension visibility traits in the analysis below...
17    private func formattedAge() -> String {
18        switch self.gender {
19        case .Male:
20            return "I'm \(self.age)."
21        case .Female:
22            return "Not telling."
23        }
24    }
25    
26    public enum Gender {
27        case Male
28        case Female
29    }
30}

Now, suppose that I wanted to extend Person and inspect the three nuances about the extension’s capabilities and behaviors that I introduced at the beginning of this article…

Extensions and visibility into extended Type

When I introduced that first nuance about visibility into the extended Type, I asked the question, “Can they see things marked private?” The answer surprised me at first: Yes…they can.

However, here’s where the second nuance comes in: It absolutely does matter where the extension is defined.

Defined within same file

Extensions defined within the same file as the Type they’re extending have access to private members of that Type.

For example, defining an extension to Person within Person.swift allows the extension to access private properties and functions! Who knew?!

1extension Person {
2    func getAge() -> Int {
3        return age // compiles, even though age is --private--
4    }
5    
6    func getFormattedAge() -> String {
7        return formattedAge() // compiles, even though formattedAge is --private--
8    }
9}

“What?? Why?”, I thought to myself…

My reasoning as to why extensions defined within the same file behave this way is because when it comes down to it, I could have just written the extension’s implementation as part of the Type itself and it would have had the same effect.

I’m in the source file of the Type I’m “extending”. So whether I write the additional functionality as an extension for the Type, or just define what would have been in the extension inside the Type, itself, the net effect is the same.

Therefore, the compiler essentially says, “I see this extension being defined, but there’s really no point. It’s in the same file that the Type is defined in… so the developer could have just written all this code within the Type itself… so I’ll let him/her refer to private code blocks.”

Update: My reasoning above reveals that I truly didn’t have an understanding of Swift access control. I recommend giving my followup article titled “Clarifying Swift Access Control” a read for more details!

Defined in a separate file

Moving the extension definition into a separate file, however, causes the extension to lose that visibility into the Type it’s extending.

Following the inverse of my previous reasoning about private visibility when the extension is defined within the same file, this behavior actually makes sense to me.

Most of the time, you’d be writing an extension for Types that you don’t have the source to. In that scenario, extensions would have the same visibility that any client of the Type’s exposed API would have, namely, the things marked public.

PersonExtensions.swift

Default extension access control

The final nuance also yielded some semi-surprising results for me. Apple’s documentation says it, but until I experimented and saw it in action, I didn’t catch the nuance around the default access control modifiers applied to extensions.

Default access when no explicit access modifiers specified

In short, when you declare an extension but specify no explicit access modifiers (ie, you just use the default), the extension’s default access level depends on the access level of the Type it’s extending.

  • If the Type is public or internal, the extension’s implementation “members” will be internal by default. The “surprise” for me I think is that extensions for public Types have internal members by default, unless you specify otherwise.
  • If the Type is private, the extension’s implementation “members” will be private by default.

Here’s what the extension looks like if we analyze it from the perspective of using no explicitly declared access modifiers (note that to gain access to private properties and functions, I’m declaring the extension within Person.swift):

 1public struct Person {
 2    // ...
 3    
 4    // ...
 5}
 6
 7extension Person {
 8    func getAge() -> Int {
 9        return age
10    }
11    
12    func getFormattedAge() -> String {
13        return formattedAge()
14    }
15}

Using the default access modifiers as shown in the code snippet above exposes access to the extension’s new API to instances within the same module. However, it does not expose additional public API for the Type it’s extending to a client of that Type that’s in another module (for example, the unit test target, which is another Swift module).

Same Module
Extended API within the same module

Different Module (test target)
Extended API in a different module

For some reason, I had it in my head that if a Type that I’m extending is public, the extension’s members would default to public. I don’t know why I thought that, but thankfully my experimentation cleared up!

Default access when using default extension declaration, but specify public for implementation

Adding public access control modifiers to the extension implementation’s members makes those members visible everywhere (that is, both within the same module, and within the test target).

The location of the extension’s declaration, be it within the same source file as the Type it’s extending, or in a separate source file, does not matter in terms of what the extension exposes when adding public members… But only extensions declared within the same source file as the Type it’s extending can see private members of that Type, as we discovered previously.

Extensions declared in same and separate source files
Public extension members defined within the same source file, and in separate source files...

Public extension members visible in different module (test target)
Regardless of where the extension is declared, all public members are visible to  other modules (such as the test target).

But notice that on the line where I’ve written extension Person { ... } that I haven’t specified an access control modifier for the extension, itself. I’ve only added public to its members. Even still, the new functions are visible to the test target which is a different module.

In other words, there’s no need to write public extension Person { ... }. Since Person is public, the extension just uses the Type’s access level for its own declaration.

Wrapping up

The three nuances about Swift extensions that were analyzed here were “surprising” enough to me to warrant some experimentation. My hope is that the analysis that was done will help clear up these subtleties for others who are struggling with understanding how Swift extensions behave!

comments powered by Disqus