Swift generics: How does it work and the problems it solves

As you probably know, Swift is always considered as one of the most popular and trust-using programming languages in the world. Not only as simple as a language, but Swift also runs as a smart tool with tons of outstanding features. And, one of them must mention Swift generics. Maybe you’ve heard of Swift but don’t know anything about Swift generics. And, this blog below of ArrowHiTech will help you widen your knowledge about this area. So, let’s explore right away! 

What are Swift generics? 

Swift generics

Can’t help but mention Swift generics when coming to the Swift programming language. In fact, Swift generics is always considered as one of the most essential features of this language. Thanks to them, you can easily generalize as well as reuse the code in a specific way that hardly other features can do. Not only that, Swift generics are also a somewhat advanced feature that has proven to be a stumbling hurdle for many developers. What’s more, the iOS SDK makes considerable use of generic, which is especially true in SwiftUI.

How does Swift generics work?

In fact, as you probably know Swift has a powerful type system. Then, you can’t merely assign an integer value to a variable that’s been declared as a string.

Swift generics

To illustrate, let’s look at the syntax below:

var text:String = “Hello world!”

text = 5

// Output: error: cannot assign value of type ‘Int’ to type ‘String’

Besides, if you want to work well with less rigid data types? Then, let’s refer to the following example:

func addition(a: Int, b: Int) -> Int

{

    return a + b

}

let result = addition(a: 42, b: 99)

print(result)

// Output: 141

What’s more, Swift generics accept two Int parameters, a and b, and return an Int value. Then, the + operator takes two numbers and adds them together, returning the outcome. 

Hence, if you expect to add other number types to your function, including Float and Double? Let’s follow below:

func addition(a: Double, b: Double) -> Double

{

    return a + b

}

Best of all, you can even reuse your code without having to describe the types that the addition(a:b:) function can work with. Also, Swift generics play a significant role in this process. 

How does Swift generics work with Placeholder Types and Generic Functions?

work with Placeholder Types and Generic Functions

First and foremost, Swift generics allow you to write code that is explicit, versatile, and reusable. Moreover, you don’t have to write the same code twice, and you may create generic code in a more expressive way.

Let’s explore the instance below. Then, you will understand how to convert the original addition(a:b:) function to a Swift generics function

func addition<T: Numeric>(a: T, b: T) -> T

{

    return a + b

}

Let’s take a look at how Swift generics works:

#1. The function’s parameters and return type are of type T rather than Int. A placeholder type is what it’s called. 

#2. <T: Numeric> is used to define the placeholder type T. This tells Swift that T is a placeholder within the addition(a:b:) method, not a real type. 

Firstly, the placeholder T doesn’t say what kind of T it is; it merely says that both a and b, as well as the function return value, must be of the same type T. Besides, the function’s inputs and outputs must be of the same type. Then, this can be seen in the Swift code above. 

In addition, any type can be used using the generic addition(a:b:) function as long as it complies with Numeric. Besides, integers, doubles, decimal values, and so on can all be added with this function. Best of all, it’s reusable and adaptable, with no need for redundant code.

So, let’s have a glance at the following lines of code:

let a = addition(a: 42, b: 99)

print(a)

// Output: 141

let b = addition(a: 0.99, b: 0.33)

print(b)

// Output: 1.32

let c = addition(a: Double.pi, b: Double.pi)

print(c)

// Output: 6.28318530717959

Way to work with Generic Type Constraints

func addition<T: Numeric>(a: T, b: T) -> T

{

    return a + b

}

To begin, the type constraint is added to the placeholder with the <T: Numeric> syntax. It specifies that T must follow the Numeric protocol. Simply speaking, this is a Swift protocol that may be used with any numeric value, such as Int and Double.

Then, let’s take a look at the following instance:

func findIndex<T>(of foundItem: T, in items: [T]) -> Int?

{

    for (index, item) in items.enumerated()

    {

        if item == foundItem {

            return index

        }

    }

    return nil

}

For more details, this above code describes the looks for the parameter foundItem in the items array by comparing it to each item. Besides, when a match is detected, the index of the detected object is returned. When the function can’t find the item, it returns nil, hence the return type of findIndex(of:in:) is Int??

In the function declaration, the placeholder type T is utilized. Then, it informs Swift generics that this function can find any item in any array if the foundItem and the array’s elements are of the same type. 

Then, let’s explore the way to apply this function:

let names = [“Ford”, “Arthur”, “Trillian”, “Zaphod”, “Deep Thought”]

if let result = findIndex(of: “Zaphod”, in: names) {

    print(result)

    // Output: 3

}

In order to determine if two items are equal, we use the equality operator == in the function, which means T  must follow the Equatable protocol. 

findIndex<T: Equatable>(of foundItem: T, in items: [T]) -> Int?

Furthermore, the Equatable protocol is a protocol that declares the == operator, as its name suggests (as a function). Then, to see if two values are equal, use the == operator.

Swift’s protocols

In reality, Swift generics come with a handful of these fundamental protocols:

#1. Equatable refers to a set of values that can be either equal or unequal.

#2. For values that can be compared, such as a > b, Comparable is used.

#3. Hashable refers to values that can be “hashed,” or converted into a unique integer representation (often used for dictionary keys)

#4. For values that can be expressed as a string, CustomStringConvertible is a useful mechanism for fast converting custom objects into printed strings.

#5. For numbers, such as 42 and 3.1415, Numeric and SignedNumeric are used.

#6. Values that may be offset and measured, such as sequences, steps, and ranges, are Strideable.

The combination of Generics, Protocols and Associated Types

To begin, a protocol defines the functions that a compliant class must implement. Besides, the class is deemed to comply with the protocol when it implements the needed functionalities. 

Now, assume you own a restaurant that sells specific foods. Then, a diner walks into your restaurant and requests a meal. He doesn’t care what he’s eating as long as it’s edible.

So, let’s take a look at the instance about the protocol of diner below:

protocol Edible {

    func eat()

}

Besides, any class that wishes to be Edible must implement the eat() function, which looks like below:

class Apple: Edible

{

    func eat() {

        print(“Omnomnom!”)

    }

}

In reality, Protocols in Swift generics will assist you in writing reusable and flexible code. Not only that, they can also support you in loosely coupling your code. Besides, the client doesn’t need to know the precise implementation of what he’ll eat; all he needs to know is that it has an eat() function. Then, he is able to eat whatever he wants as long as it is Edible!

How about the Storage protocol? 

protocol Storage

{

    func store(item: Book)

    func retrieve(index: Int) -> Book

}

In fact, Storage protocol defines two functions, one to store a book and the other to retrieve a book based on its index. Besides, let’s imagine that a Book is a basic struct that defines a title and author for the sake of thoroughness.

struct Book {

    var title = “”

    var author = “”

}

Then, the Storage protocol can be used by any class to store and retrieve books, including the Bookcase and Booktrunk classes. So, let’s have a glance at an example:

class Bookcase: Storage

{

    var books = [Book]()

    func store(item: Book) {

        books.append(item)

    }

    func retrieve(index: Int) {

        return books[index]

    }

}

In particular, the books array in the Bookcase class is used to hold books.Then, it uses the Storage protocol’s features to store and retrieve books. As a result, Bookcase can only be used to store Book objects. 

However, in some cases, you will not only want to store books! Then, you will want to be able to store any object in any storage location. Thus, Swift generics play an important role in this.

In order to achieve this goal, let’s make some changes to codes like below:

protocol Storage

{

    associatedtype Item

    func store(item: Item)

    func retrieve(index: Int) -> Item

}

How is the Storage protocol implemented in a Trunk class? 

class Trunk<Item>: Storage

{

    var items:[Item] = [Item]()

    func store(item: Item) {

        items.append(item)

    }

    func retrieve(index: Int) -> Item {

        return items[index]

    }

}

Now, let’s explore the method to fill the trunk with books:

let bookTrunk = Trunk<Book>()

bookTrunk.store(item: Book(title: “1984”, author: “George Orwell”))

bookTrunk.store(item: Book(title: “Brave New World”, author: “Aldous Huxley”))

print(bookTrunk.retrieve(index: 1).title)

// Output: Brave New World

Not only that, you also can store Shoe class with a size and a brand in a truck like: 

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

Then, coming to another example with Storage protocol:

class FreightShip<Item>: Storage

{

    func store(item: Item) {

        // Load onto the ship…

    }

    func retrieve(index: Int) -> Item {

        // Unload from the ship…

    }

}

How do Storage protocol and Trunk class work in Swift generics? 

#1. Storage protocol: this protocol lets us know about an associated type. Then, the class that uses the Storage protocol must determine this type.

#2. Trunk class: implements its functions by using a generic placeholder.

Finally, when we define Trunk using Book, the related type and generic placeholder are finalized. In fact, this specifies the types that will be used by the code. Not only that, we have the freedom to specify these types ourselves when writing our implementation. 

Additionally, the single requirement of the generic Storage protocol is that every class that implements it must have a function to store and retrieve any object. However, it doesn’t say how this object should be stored or retrieved, or even what kind of item it should be. Hence, we can easily design any type of storage for any type of thing. 

The final line 

The ArrowHiTech team hopes this blog above is useful for both newbies and developers. In addition to Swift generics, Swift language also contains a lot of other features such as: Swift array, Swift loop, Swift function, etc. All of them are very special and beneficial for your business. 

In fact, it’s not too difficult to understand Swift generics but in order to apply it to practicals, it’s not really easy. So, if you get any trouble when using this feature, let’s ArrowHiTech know. With many years of experience in Swift and Objective-C IOS App Development Services, we commit to resolve your difficulties effectively and quickly.  

Tags

Share