Generics is one of the most powerful features in the Swift programming language. But it will be a bit confusing at the beginning. In this article we’ll see how generics work in Swift, and the cool things you can do with it.
Here’s what you will understand after reading this article:
- What problems do Generics use to solve?
- Placeholder types and generic functions
- Generic type and binding to protocols
- Work with associated types
- Combine protocols and generics
How to use Generics in Swift
You also know Swift has a Strong type system. Once you declared a variable is a String, you cannot assign an integer to it. Type like this:
1 2 3 4 | <span class="token keyword">var</span> text <span class="token punctuation">:</span> <span class="token builtin">String</span> <span class="token operator">=</span> <span class="token string">"Hello world!"</span> text <span class="token operator">=</span> <span class="token number">5</span> <span class="token comment">// Output: error: cannot assign value of type 'Int' to type 'String'</span> |
A String is always a String. You cannot assign a value of type Int
to a variable of type String
/
This strictness is generally a good thing, as it helps you avoid code errors. But what if you want to work with a data type that doesn’t want this strictness?
See the following example. You create a function that adds one number to another. Like this:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">func</span> <span class="token function">addition</span> <span class="token punctuation">(</span> a <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Int</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token function">addition</span> <span class="token punctuation">(</span> a <span class="token punctuation">:</span> <span class="token number">42</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token number">99</span> <span class="token punctuation">)</span> <span class="token function">print</span> <span class="token punctuation">(</span> result <span class="token punctuation">)</span> <span class="token comment">// Output: 141</span> |
The function takes 2 parameters a
and b
type Int
, and returns a value of type Int
. The +
operator adds the number and returns the result.
What if you want to extend your function so that it can also add other types of numbers like Float
and Double
? Are you going to write a new function of type:
1 2 3 4 5 | <span class="token keyword">func</span> <span class="token function">addition</span> <span class="token punctuation">(</span> a <span class="token punctuation">:</span> <span class="token builtin">Double</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token builtin">Double</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Double</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> |
Looks like your code has been duplicated. Overall this is pretty bad because according to the DRY principle , Don’t repeat yourself.
So to be able to reuse your code without specifying a specific type like addition(a:b:)
then Generics
was generated.
Work with Generic Functions and Placeholder Types
With generics you can write clean, flexible, and reusable code. You can avoid writing duplicate code. Let’s take the old function addition(a:b)
and change it to a generic function. Like this:
1 2 3 4 5 | <span class="token keyword">func</span> addition <span class="token operator"><</span> T <span class="token punctuation">:</span> <span class="token builtin">Numeric</span> <span class="token operator">></span> <span class="token punctuation">(</span> a <span class="token punctuation">:</span> T <span class="token punctuation">,</span> b <span class="token punctuation">:</span> T <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> T <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> |
The <T: Numeeric>
adds a type constraint to the placeholder. It thinks that T
needs to conform to Numeric
protocol. This is a built-in protocol in Swifr for any numeric value like Int
, Double
.
In other words, you would not have used function addition(a:b:)
to add two UIViewController
or UILabel
objects. It makes no sense. You can only use a conform value according to the Numeric
protocol.
Let’s try another example. Here is a generic function that can find the index of a value in an array:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">func</span> findIndex <span class="token operator"><</span> T <span class="token operator">></span> <span class="token punctuation">(</span> of foundItem <span class="token punctuation">:</span> T <span class="token punctuation">,</span> <span class="token keyword">in</span> items <span class="token punctuation">:</span> <span class="token punctuation">[</span> T <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Int</span> <span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token keyword">for</span> <span class="token punctuation">(</span> index <span class="token punctuation">,</span> item <span class="token punctuation">)</span> <span class="token keyword">in</span> items <span class="token punctuation">.</span> <span class="token function">enumerated</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> item <span class="token operator">==</span> foundItem <span class="token punctuation">{</span> <span class="token keyword">return</span> index <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token constant">nil</span> <span class="token punctuation">}</span> |
The above function uses the foundItem
parameter to find the item
in the array using a loop. When it is found, it returns the index
of the found item
. Function return nil
if it can’t find the item, so the return type of findIndex(of:in:)
would be Int?
Placeholder type T
is used in the declaration function. It tells Swift that this function can find any item in any array, provided that foundItem
and items in the array are of the same type. That means you want to find the T
value in the array T
Here’s how to use the function:
1 2 3 4 5 6 7 | <span class="token keyword">let</span> names <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token string">"Ford"</span> <span class="token punctuation">,</span> <span class="token string">"Arthur"</span> <span class="token punctuation">,</span> <span class="token string">"Trillian"</span> <span class="token punctuation">,</span> <span class="token string">"Zaphod"</span> <span class="token punctuation">,</span> <span class="token string">"Deep Thought"</span> <span class="token punctuation">]</span> <span class="token keyword">if</span> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token function">findIndex</span> <span class="token punctuation">(</span> of <span class="token punctuation">:</span> <span class="token string">"Zaphod"</span> <span class="token punctuation">,</span> <span class="token keyword">in</span> <span class="token punctuation">:</span> names <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">print</span> <span class="token punctuation">(</span> result <span class="token punctuation">)</span> <span class="token comment">// Output: 3</span> <span class="token punctuation">}</span> |
But maybe the above function cannot compile. We need another type constraint on T
Since we use the ==
operator in the function to see if two items are equal, T
needs to conform to the Equatable
protocol. Otherwise you can’t use ==
operator. Like this:
1 2 | findIndex <span class="token operator"><</span> T <span class="token punctuation">:</span> <span class="token builtin">Equatable</span> <span class="token operator">></span> <span class="token punctuation">(</span> of foundItem <span class="token punctuation">:</span> T <span class="token punctuation">,</span> <span class="token keyword">in</span> items <span class="token punctuation">:</span> <span class="token punctuation">[</span> T <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Int</span> <span class="token operator">?</span> |
Equatable protocol, it is the protocol declares operator ==
. The ==
operator is used to decide whether two values are equal or not.
Swfit provides some basic protocols:
Equatable
for comparing equal to or k equals of 2 values.Comparable
for value comparison, likea > b
Hashable
for values that can be “hased”, which is a unique integer representation of that value (often used for dictionary keys)CustomStringConvertible
for values can be expressed as String, a useful protocol for quickly converting custom objects into printable String.Numeric
andSignedNumeric
give numeric values, like42
,3.1415
Strideable
for offset and measurable values, like sequences, steps, and ranges
And of course you can also define your own protocol so that the generic placeholder can conform. Next we will discuss protocols and generics in more depth.
Combines Generics, Protocols and Associated Types.
So we looked at:
- The generic function uses the placeholder value to specify the input and output of the function
- The protocol binds those things
Have you heard about the protocol before, right? A protocol specifies the functions that a class to conform to will have to apply it. When it passes the requested functionality, the class is said to be comformed under that protocol.
Take a look at the example. Imagine you have a restaurant that sells some food products. A customer comes to your restaurant, and wants to eat something. He doesn’t care what exactly he eats. As long as it is edible.
The client defines a protocol:
1 2 3 4 | <span class="token keyword">protocol</span> <span class="token builtin">Edible</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">eat</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Any class that wants to conform to Edible
needs to express eat()
funciton.
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">Apple</span> <span class="token punctuation">:</span> <span class="token builtin">Edible</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">eat</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">print</span> <span class="token punctuation">(</span> <span class="token string">"Omnomnom!"</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Protocol helps you to write flexible and reusable code. It also helps you to interconnect your code in an inconsistent way. The customer does not need to know the exact part of what he eats, he just needs to know it has the function eat()
. He can eat anything, as long as Edible
.
But what does this have to do with genenric?
Let’s start with another hypothetical scenario. You are going to a department store, to buy a bookcase. And you have two requirements for that bookcase:
- You don’t have to put the book in the bookcase
- It doesn’t even need to be a bookcase, it could also be a storage box, a locker, a wardrobe or a wardrobe.
- Hmm … you just want “something” where you can put the “item” in and take the item out
So we have a Storage
protocol:
1 2 3 4 5 6 | <span class="token keyword">protocol</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token builtin">Book</span> <span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">retrieve</span> <span class="token punctuation">(</span> index <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Book</span> <span class="token punctuation">}</span> |
Protocol Storage
declares two functions, one to store books and one to retrieve books, according to its index. Let’s assume that the Book is a simple Struct with title and author.
1 2 3 4 5 | <span class="token keyword">struct</span> <span class="token builtin">Book</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> title <span class="token operator">=</span> <span class="token string">""</span> <span class="token keyword">var</span> author <span class="token operator">=</span> <span class="token string">""</span> <span class="token punctuation">}</span> |
Any class can comply with the Storage
protocol to store books and retrieve books. Like the following Bookcase
and Booktrunk
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Bookcase</span> <span class="token punctuation">:</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> books <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token builtin">Book</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token builtin">Book</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> books <span class="token punctuation">.</span> <span class="token function">append</span> <span class="token punctuation">(</span> item <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">retrieve</span> <span class="token punctuation">(</span> index <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> books <span class="token punctuation">[</span> index <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
The Bookcasee
class above stores books in the book array. It adopts the functions from protocol Storage
to store and retrieve books. You can ONLY use Bookcase
to store Book
objects.
However, you don’t just want to store the Book
, you want to store anything in any storage. From there, generic was used.
We made a few changes to the code. First we need to add a associated type
in Storage
. You can define a generic type in the protocol by using associated type
. It is the same as the placeholder type we have looked at. So that:
1 2 3 4 5 6 7 | <span class="token keyword">protocol</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> associatedtype <span class="token builtin">Item</span> <span class="token keyword">func</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token builtin">Item</span> <span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">retrieve</span> <span class="token punctuation">(</span> index <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Item</span> <span class="token punctuation">}</span> |
Here you have changed:
- Add
Item
associated type withassociatedtype
keyword. - The
store(item:)
andretrieve(index:)
now be used associated typeItem
See, it’s the same placeholder type. Instead of just using Book
, the conform class to the Storage
protocol can now store any of these types of Item
. Because we work with the protocol, the class can decide for itself how to store the item.
Think of the associated type like associating a generic type with a protocol for which there is no need to define the type. The generic itself is undefined because it depends on the class that conforms to the protocol. It is done in detail
Now we implement the Storage
protocol in the Trunk
class. This Trunk
class can store any item, not just a book.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Trunk</span> <span class="token operator"><</span> <span class="token builtin">Item</span> <span class="token operator">></span> <span class="token punctuation">:</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> items <span class="token punctuation">:</span> <span class="token punctuation">[</span> <span class="token builtin">Item</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token builtin">Item</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token builtin">Item</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> items <span class="token punctuation">.</span> <span class="token function">append</span> <span class="token punctuation">(</span> item <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">retrieve</span> <span class="token punctuation">(</span> index <span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">Item</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> items <span class="token punctuation">[</span> index <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Let’s see how to define the Trunk
class with an attached <Item>
. It is a placeholder, and it is used throughout the class. Trunk
class has a simple array that can store and retrieve items.
We create a trunk
to store the book:
1 2 3 4 5 6 | <span class="token keyword">let</span> bookTrunk <span class="token operator">=</span> <span class="token builtin">Trunk</span> <span class="token operator"><</span> <span class="token builtin">Book</span> <span class="token operator">></span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> bookTrunk <span class="token punctuation">.</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token function">Book</span> <span class="token punctuation">(</span> title <span class="token punctuation">:</span> <span class="token string">"1984"</span> <span class="token punctuation">,</span> author <span class="token punctuation">:</span> <span class="token string">"George Orwell"</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> bookTrunk <span class="token punctuation">.</span> <span class="token function">store</span> <span class="token punctuation">(</span> item <span class="token punctuation">:</span> <span class="token function">Book</span> <span class="token punctuation">(</span> title <span class="token punctuation">:</span> <span class="token string">"Brave New World"</span> <span class="token punctuation">,</span> author <span class="token punctuation">:</span> <span class="token string">"Aldous Huxley"</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token function">print</span> <span class="token punctuation">(</span> bookTrunk <span class="token punctuation">.</span> <span class="token function">retrieve</span> <span class="token punctuation">(</span> index <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> title <span class="token punctuation">)</span> <span class="token comment">// Output: Brave New World</span> |
In the first line of code, the Book
type is used when bookTrunk
type. Now the Trunk
class uses the Book
struct instead of the Item
placeholder.
As such, the code will definitely become more flexible. We define a Shoe
class with size
and brand
. Can we store it in the trunk k? Yes of course.
1 2 3 4 5 6 | let shoeTrunk = Trunk<Shoe>() shoeTrunk.store(item: Shoe(size: 42, brand: "Nike")) shoeTrunk.store(item: Shoe(size: 99, brand: "Adidas")) print(shoeTrunk.retrieve(index: 0).brand) // Output: Nike |
And now we can save everything.
So in general:
- Protocol
Storage
defines a associated type. This type must be determined by the class that conforms to theStorage
protocol. Trunk
class uses a generic placeholder to implement the function.
=> The Associated type and the generic placeholder are specified when we determine that Trunk
will be Book
. This indicates the specific types the code will use. As we grow we can define these types flexibly.
The Generic Storage
protocol only specifies when the class to comply with it will need to include a function to store and a function to access. It does not specify how the item is stored or retrieved, nor the type of the item. As a result we can create any storage that can store any item.
Since Swift 5.1 we have a different approach to the generic work: opaque types . some
keyword helps you ẩn
a specific type that a property or function returns. The specific type is determined by the implementation itself. So opaque types are sometimes called reverse generics.