23

Swift Generics Tutorial: Getting Started [FREE]

 4 years ago
source link: https://www.tuicool.com/articles/EryaemJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Update note : Michael Katz updated this tutorial for Swift 5. Mikael Konutgan wrote the original and Gemma Barlow updated for Swift 3.

Generic programming is a way to write functions and data types while making minimal assumptions about the type of data being used. Swift generics create code that does not get specific about underlying data types, allowing for elegant abstractions that produce cleaner code with fewer bugs. It allows you to write a function once and use it on different types.

You’ll find generics in use throughout Swift, which makes understanding them essential to a complete mastery of the language. An example of a generic you will have already encountered in Swift is the Optional type. You can have an optional of any data type you want, even those types you create yourself, of course. In other words, the Optional data type is generic over the type of value it might contain.

In this tutorial, you’ll experiment in a Swift playground to learn:

  • What exactly generics are.
  • Why they are useful.
  • How to write generic functions and data structures.
  • How to use type constraints.
  • How to extend generic types.

Getting Started

Begin by creating a new playground. In Xcode, go to File ▸ New ▸ Playground… . Select the macOS ▸ Blank template. Click Next and name the playground Generics . Finally, click Create !

As one of the few programmers residing in a kingdom far-far-away, you’ve been summoned to the royal castle to help the Queen with a matter of great importance. She has lost track of how many royal subjects she has and needs some assistance with her calculations.

She requests a function to be written that adds two integers. Add the following to your newly-created playground:

func addInts(x: Int, y: Int) -> Int {
  return x + y
}

addInts(x:y:) takes two Int values and returns their sum. You can give it a try by adding the following code to the playground:

let intSum = addInts(x: 1, y: 2)

This is a simple example that demonstrates Swift’s type safety. You can call this function with two integers, but not any other type.

The Queen is pleased, and immediately requests another add function be written to count her fortune —this time, adding Double values. Create a second function addDoubles(x:y:) :

func addDoubles(x: Double, y: Double) -> Double {
  return x + y
}

let doubleSum = addDoubles(x: 1.0, y: 2.0)

The function signatures of addInts and addDoubles are different, but the function bodies are identical. Not only do you have two functions, but the code inside them is repeated. Generics can be used to reduce these two functions to one and remove the redundant code.

First, however, you’ll look at a few other common occurrences of generic programming in everyday Swift.

Other Examples of Swift Generics

You may not have realized, but some of the most common structures you use, such as arrays , dictionaries , optionals and results are generic types!

Arrays

Add the following to your playground:

let numbers = [1, 2, 3]

let firstNumber = numbers[0]

Here, you create a simple array of three numbers and then take the first number out of that array.

Now Option-click , first on numbers and then on firstNumber . What do you see?

numbers-480x148.png

firstNumbers-480x172.png

Because Swift has type inference , you don’t have to explicitly define the types of your constants, but they both have an exact type. numbers is an [Int] — that is, an array of integers — and firstNumber is an Int .

The Swift Array type is a generic type . Generic types all have at least one type parameter , a placeholder for an as-yet unspecified other type. You need to specify this other type in order to specialize the generic type and actually create an instance of it.

For instance, the type parameter of Array determines what’s in the array. Your array is specialized so it can only contain Int values. This supports Swift’s type safety . When you remove anything from that array, Swift — and more importantly you — know it must be an Int .

You can better see the generic nature of Array by adding a slightly longer version of the same code to the playground:

var numbersAgain: Array<Int> = []
numbersAgain.append(1)
numbersAgain.append(2)
numbersAgain.append(3)

let firstNumberAgain = numbersAgain[0]

Check the types of numbersAgain and firstNumberAgain by Option-clicking on them; the types will be exactly the same as the previous values. Here you specify the type of numbersAgain using explicit generic syntax, by putting Int in angle brackets after Array . You’ve provided Int as the explicit type argument for the type parameter.

Try appending something else to the array, like a String :

numbersAgain.append("All hail Lord Farquaad")

You’ll get an error — something like: Cannot convert value of type ‘String’ to expected argument type ‘Int’ . The compiler is telling you that you can’t add a string to an array of integers. As a method on the generic type Array , append is a so-called generic method . Because this array instance is of the specialized type Array<Int> , its append method is also now specialized to append(_ newElement:Int) . It won’t let you add something of an incorrect type.

Delete the line causing the error. Next you’ll look at another example of generics in the standard library.

Dictionaries

Dictionaries are also generic types and result in type-safe data structures.

Create the following dictionary of magical kingdoms at the end of your playground, and then look up the country code for Freedonia :

let countryCodes = ["Arendelle": "AR", "Genovia": "GN", "Freedonia": "FD"]
let countryCode = countryCodes["Freedonia"]

Check the types of both declarations. You’ll see that countryCodes is a dictionary of String keys and String values — nothing else can ever be in this dictionary. The formal generic type is Dictionary .

Optionals

In the example above, note the type of countryCode is String? . This is in fact just a shorthand for Optional .

If the < and > look familiar, it’s because even Optional is a generic type. Generics are all over the place!

Here the compiler enforces that you can only access the dictionary with string keys and you always get string values returned. An optional type is used to represent countryCode , because there might not be a value corresponding to that key. If you try to look up “The Emerald City”, for example, the value of countryCode would be nil , as it doesn’t exist in your dictionary of magical kingdoms.

Note : For a more detailed introduction to Optionals, check out theProgramming in Swift video on this site.

Add the following to your playground to see the full explicit syntax for creating an optional string:

let optionalName = Optional<String>.some("Princess Moana")
if let name = optionalName {}

Check the type of name , which you’ll see is String .

Optional binding , that is, the if-let construct, is a generic transformation of sorts. It takes a generic value of type T? and gives you a generic value of type T . That means you can use if let with any concrete type.

SwiftGenerics-01-tea.png

It’s T time!

Results

Result is a new type in Swift 5. Like Optional , it is a generic enum with two cases. Instead of something or nothing, a result is either a success or failure . Each case has its own associated generic type, success has a value and failure has an Error .

Consider this case, where the royal magician recruits you to cast some spells. Known spells generate a symbol, but unknown spells fail. The function would look something like this:

enum MagicError: Error {
  case spellFailure
}

func cast(_ spell: String) -> Result<String, MagicError> {
  switch spell {
  case "flowers":
    return .success(":bouquet:")
  case "stars":
    return .success(":sparkles:")
  default:
    return .failure(.spellFailure)
  }
}

Result allows you to write functions that return a value or an error without having use try syntax. As an added bonus, the generic specification of the failure case means that you don’t need to check the type as you would with a catch block. If there’s an error, you can be certain there will be a MagicError in the value associated with the .failure case.

Try out some spells to see Result in action:

let result1 = cast("flowers") // .success(":bouquet:")
let result2 = cast("avada kedavra") // .failure(.spellFailure)

With a grasp of the basics of generics, you can learn about writing your own generic data structures and functions.

Writing a Generic Data Structure

A queue is a data structure kind of like a list or a stack, but one to which you can only add new values to the end ( enqueue them) and only take values from the front ( dequeue them). This concept might be familiar if you’ve ever used OperationQueue — perhaps while making networking requests .

The Queen, happy with your efforts earlier in the tutorial, would now like you to write functionality to help keep track of royal subjects waiting in line to speak with her.

Add the following struct declaration to the end of your playground:

struct Queue<Element> {
}

Queue is a generic type with a type argument , Element , in its generic argument clause . Another way to say this is, Queue is generic over type Element . For example, Queue<Int> and Queue<String> will become concrete types of their own at runtime, that can only enqueue and dequeue strings and integers, respectively.

Add the following property to the queue:

private var elements: [Element] = []

You’ll use this array to hold the elements, which you initialize as an empty array. Note that you can use Element as if it’s a real type, even though it’ll be filled in later. You mark it as private because you don’t want consumers of Queue to access elements . You want to force them to use methods to access the backing store.

Finally, implement the two main queue methods:

mutating func enqueue(newElement: Element) {
  elements.append(newElement)
}

mutating func dequeue() -> Element? {
  guard !elements.isEmpty else { return nil }
  return elements.remove(at: 0)
}

Again, the type parameter Element is available everywhere in the struct body, including inside methods. Making a type generic is like making every one of its methods implicitly generic over the same type. You’ve implemented a type-safe generic data structure, just like the ones in the standard library.

Play around with your new data structure for a bit at the bottom of the playground, enqueuing waiting subjects by adding their royal id to the queue:

var q = Queue<Int>()

q.enqueue(newElement: 4)
q.enqueue(newElement: 2)

q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()

Have some fun by intentionally making as many mistakes as you can to trigger the different error messages related to generics — for example, add a string to your queue. The more you know about these errors now, the easier it will be to recognize and deal with them in more complex projects.

Writing a Generic Function

The Queen has a lot of data to process, and the next piece of code she asks you to write will take a dictionary of keys and values and convert it to a list.

Add the following function to the bottom of the playground:

func pairs<Key, Value>(from dictionary: [Key: Value]) -> [(Key, Value)] {
  return Array(dictionary)
}

Take a good look at the function declaration, parameter list and return type.

The function is generic over two types that you’ve named Key and Value . The only parameter is a dictionary with a key-value pair of type Key and Value . The return value is an array of tuples of the form — you guessed it — (Key, Value) .

You can use pairs(from:) on any valid dictionary and it will work, thanks to generics:

let somePairs = pairs(from: ["minimum": 199, "maximum": 299])
// result is [("maximum", 299), ("minimum", 199)]

let morePairs = pairs(from: [1: "Swift", 2: "Generics", 3: "Rule"])
// result is [(1, "Swift"), (2, "Generics"), (3, "Rule")]

Of course, since you can’t control the order in which the dictionary items go into the array, you may see an order of tuple values in your playground more like “Generics”, “Rule”, “Swift”, and indeed, they kind of do! :]

At runtime, each possible Key and Value will act as a separate function, filling in the concrete types in the function declaration and body. The first call to pairs(from:) returns an array of (String, Int) tuples. The second call uses a flipped order of types in the tuple and returns an array of (Int, String) tuples.

You created a single function that can return different types with different calls. You can see how keeping your logic in one place can simplify your code. Instead of needing two different functions, you handled both calls with one function.

Now that you know the basics of creating and working with generic types and functions, it’s time to move on to some more advanced features. You’ve already seen how useful generics are to limit things by type, but you can add additional constraints as well as extend your generic types to make them even more useful.

Constraining a Generic Type

Wishing to analyze the ages of a small set of her most loyal subjects, the Queen requests a function to sort an array and find the middle value.

When you add the following function to your playground:

func mid<T>(array: [T]) -> T? {
  guard !array.isEmpty else { return nil }
  return array.sorted()[(array.count - 1) / 2]
}

You’ll get an error. The problem is that for sorted() to work, the elements of the array need to be Comparable . You need to somehow tell Swift that mid can take any array as long as the element type implements Comparable .

Change the function declaration to the following:

func mid<T: Comparable>(array: [T]) -> T? {
  guard !array.isEmpty else { return nil }
  return array.sorted()[(array.count - 1) / 2]
}

Here, you use the : syntax to add a type constraint to the generic type parameter T . You can now only call the function with an array of Comparable elements, so that sorted() will always work! Try out the constrained function by adding:

mid(array: [3, 5, 1, 2, 4]) // 3

You’ve already seen this when using Result : The Failure type is constrained to Error .

Cleaning Up the Add Functions

Now that you know about type constraints, you can create a generic version of the add functions from the beginning of the playground — this will be much more elegant, and please the Queen greatly. Add the following protocol and extensions to your playground:

protocol Summable { static func +(lhs: Self, rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}

First, you create a Summable protocol that says any type that conforms must have the addition operator + available. Then, you specify that the Int and Double types conform to it.

Now using a generic parameter T and a type constraint, you can create a generic function add :

func add<T: Summable>(x: T, y: T) -> T {
  return x + y
}

You’ve reduced your two functions (actually more, since you would have needed more for other Summable types) down to one and removed the redundant code. You can use the new function on both integers and doubles:

let addIntSum = add(x: 1, y: 2) // 3
let addDoubleSum = add(x: 1.0, y: 2.0) // 3.0

And you can also use it on other types, such as strings:

extension String: Summable {}
let addString = add(x: "Generics", y: " are Awesome!!! :]")

By adding other conforming types to Summable , your add(x:y:) function becomes more widely useful thanks to its generics-powered definition! Her Royal Highness awards you the kingdom’s highest honor for your efforts.

Extending a Generic Type

A Court Jester has been assisting the Queen by keeping watch over the waiting royal subjects, and letting the Queen know which subject is next, prior to officially greeting them. He peeks through the window of her sitting room to do so. You can model his behavior using an extension, applied to our generic Queue type from earlier in the tutorial.

Extend the Queue type and add the following method right below the Queue definition:

extension Queue {
  func peek() -> Element? {
    return elements.first
  }
}

peek returns the first element without dequeuing it. Extending a generic type is easy! The generic type parameter is visible just as in the original definition’s body. You can use your extension to peek into a queue:

q.enqueue(newElement: 5)
q.enqueue(newElement: 3)
q.peek() // 5

You’ll see the value 5 as the first element in the queue, but nothing has been dequeued and the queue has the same number of elements as before.

Royal Challenge : Extend the Queue type to implement a function isHomogeneous that checks if all elements of the queue are equal. You’ll need to add a type constraint in the Queue declaration to ensure its elements can be checked for equality to each other.

SwiftGenerics-02-jester.png

[spoiler title=”Homogeneous queue”]

Answer :

First edit the definition of Queue so that Element conforms to the Equatable protocol:

struct Queue<Element: Equatable> {

Then compose isHomogeneous() at the bottom of your playground:

extension Queue {
  func isHomogeneous() -> Bool {
    guard let first = elements.first else { return true }
    return !elements.contains { $0 != first }
  }
}

Finally, test it out:

var h = Queue<Int>()
h.enqueue(newElement: 4)
h.enqueue(newElement: 4)
h.isHomogeneous() // true
h.enqueue(newElement: 2)
h.isHomogeneous() // false

[/spoiler]

Subclassing a Generic Type

Swift has the ability to subclass generic classes. This can be useful in some cases, such as to create a concrete subclass of a generic class.

Add the following generic class to the playground:

class Box<T> {
  // Just a plain old box.
}

Here you define a Box class. The box can contain anything, and that’s why it’s a generic class. There are two ways you could subclass Box :

  1. You might want to extend what the box does and how it works but keep it generic, so you can still put anything in the box;
  2. You might want to have a specialized subclass that always knows what’s in it.

Swift allows both. Add this to your playground:

class Gift<T>: Box<T> {
  // By default, a gift box is wrapped with plain white paper
  func wrap() {
    print("Wrap with plain white paper.")
  }
}

class Rose {
  // Flower of choice for fairytale dramas
}

class ValentinesBox: Gift<Rose> {
  // A rose for your valentine
}

class Shoe {
  // Just regular footwear
}

class GlassSlipper: Shoe {
  // A single shoe, destined for a princess
}

class ShoeBox: Box<Shoe> {
  // A box that can contain shoes
}

You define two Box subclasses here: Gift and ShoeBox . Gift is a special kind of Box , separated so that you may have different methods and properties defined on it, such as wrap() . However, it still has a generic on the type, meaning it could contain anything. Shoe and GlassSlipper , a very special type of shoe, have been declared, and can be placed within an instance of ShoeBox for delivery (or presentation to an appropriate suitor).

SwiftGenerics-03-glass-slipper.png

Declare instances of each class under the subclass declarations:

let box = Box<Rose>() // A regular box that can contain a rose
let gift = Gift<Rose>() // A gift box that can contain a rose
let shoeBox = ShoeBox()

Notice that the ShoeBox initializer doesn’t need to take the generic type parameter anymore, since it’s fixed in the declaration of ShoeBox .

Next, declare a new instance of the subclass ValentinesBox — a box containing a rose, a magical gift specifically for Valentine’s Day.

let valentines = ValentinesBox()

While a standard box is wrapped with white paper, you’d like your holiday gift to be a little fancier. Add the following method to ValentinesBox :

override func wrap() {
  print("Wrap with ♥♥♥ paper.")
}

Finally, compare the results of wrapping both of these types by adding the following code to your playground:

gift.wrap() // plain white paper
valentines.wrap() // ♥♥♥ paper

ValentinesBox , though constructed using generics, operates as a standard subclass with methods that may be inherited and overridden from a superclass. How elegant!

Enumerations With Associated Values

The queen is pleased with your work and wants to offer you a reward: Your choice of a generic treasure or a medal.

Add the following declaration to the end of your playground:

enum Reward<T> {
  case treasureChest(T)
  case medal

  var message: String {
    switch self {
    case .treasureChest(let treasure):
      return "You got a chest filled with \(treasure)."
    case .medal:
      return "Stand proud, you earned a medal!"
    }
  }
}

This syntax allows you to write an enum where at least one of the cases is a generic box. With the message var, you can get the value back out. In the Result example illustrated above, both the success and failure cases are generic with different types.

To get the associated value back out, use it like this:

let message = Reward.treasureChest(":moneybag:").message
print(message)

Congratulations and enjoy your reward!

Where to Go From Here?

Swift generics are at the core of many common language features, such as arrays and optionals. You’ve seen how to use them to build elegant, reusable code that will result in fewer bugs — code fit for royalty.

For more information, read through the Generics chapter and the Generic Parameters and Arguments language reference chapter of Apple’s guide, The Swift Programming Language . You’ll find more detailed information about generics in Swift, as well as some handy examples.

A good next topic, to build upon what you’ve learned in this tutorial, is Protocol Oriented Programming — see Introducing Protocol Oriented Programming for more details.

Generics in Swift are an integral feature that you’ll use every day to write powerful and type-safe abstractions. Improve your commonly-used code by remembering to ask “Can I genericize this?”


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK