Generics in Swift

Tram Ho

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:

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:

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:

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:

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:

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:

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:

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, like a > 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 and SignedNumeric give numeric values, like 42 , 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:

Any class that wants to conform to Edible needs to express eat() funciton.

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:

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.

Any class can comply with the Storage protocol to store books and retrieve books. Like the following Bookcase and Booktrunk class:

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:

Here you have changed:

  • Add Item associated type with associatedtype keyword.
  • The store(item:) and retrieve(index:) now be used associated type Item

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.

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:

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.

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 the Storage 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.


Share the news now

Source : Viblo