Un tour veloce

Adattato dall'originale A Swift Tour su Swift.org con modifiche. Il contenuto originale è stato creato da Apple Inc. Con licenza Creative Commons Attribution 4.0 International (CC BY 4.0) .
Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub

La tradizione suggerisce che il primo programma in una nuova lingua dovrebbe stampare le parole "Hello, world!" sullo schermo. In Swift, questo può essere fatto in una singola riga:

print("Hello, world!")
Hello, world!

Se hai scritto codice in C o Objective-C, questa sintassi ti sembrerà familiare: in Swift, questa riga di codice è un programma completo. Non è necessario importare una libreria separata per funzionalità come input/output o gestione delle stringhe. Il codice scritto in ambito globale viene utilizzato come punto di ingresso per il programma, quindi non è necessaria una funzione main() . Inoltre, non è necessario scrivere il punto e virgola alla fine di ogni istruzione.

Questo tour ti fornisce informazioni sufficienti per iniziare a scrivere codice in Swift mostrandoti come eseguire una serie di attività di programmazione. Non preoccuparti se non capisci qualcosa: tutto ciò che viene introdotto in questo tour è spiegato in dettaglio nel resto del libro.

Valori semplici

Utilizzare let per creare una costante e var per creare una variabile. Non è necessario che il valore di una costante sia noto in fase di compilazione, ma è necessario assegnargli un valore esattamente una volta. Ciò significa che puoi utilizzare le costanti per denominare un valore che determini una volta ma che utilizzi in molti posti.

var myVariable = 42
myVariable = 50
let myConstant = 42

Una costante o variabile deve avere lo stesso tipo del valore che desideri assegnarle. Tuttavia, non è sempre necessario scrivere il tipo in modo esplicito. Fornire un valore quando si crea una costante o una variabile consente al compilatore di dedurne il tipo. Nell'esempio precedente, il compilatore deduce che myVariable è un numero intero perché il suo valore iniziale è un numero intero.

Se il valore iniziale non fornisce informazioni sufficienti (o se non esiste un valore iniziale), specificare il tipo scrivendolo dopo la variabile, separato da due punti. Nota: l'uso di Double invece di Float per i numeri in virgola mobile offre maggiore precisione ed è il tipo in virgola mobile predefinito in Swift.

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
// Experiment:
// Create a constant with an explicit type of `Float` and a value of 4.

I valori non vengono mai convertiti implicitamente in un altro tipo. Se è necessario convertire un valore in un tipo diverso, creare esplicitamente un'istanza del tipo desiderato.

let label = "The width is "
let width = 94
print(label + String(width))
The width is 94

// Experiment:
// Try removing the conversion to `String` from the last line. What error do you get?

Esiste un modo ancora più semplice per includere valori nelle stringhe: scrivere il valore tra parentesi e scrivere una barra rovesciata (``) prima delle parentesi. Per esempio:

let apples = 3
print("I have \(apples) apples.")
I have 3 apples.

let oranges = 5
print("I have \(apples + oranges) pieces of fruit.")
I have 8 pieces of fruit.

// Experiment:
// Use `\()` to include a floating-point calculation in a string and to include someone's name in a
// greeting.

Utilizza tre virgolette doppie ( """ ) per le stringhe che occupano più righe. Il rientro all'inizio di ogni riga tra virgolette viene rimosso, purché corrisponda al rientro delle virgolette di chiusura. Ad esempio:

let quotation = """
    Even though there's whitespace to the left,
    the actual lines aren't indented.
        Except for this line.
    Double quotes (") can appear without being escaped.

    I still have \(apples + oranges) pieces of fruit.
    """
print(quotation)
Even though there's whitespace to the left,
the actual lines aren't indented.
    Except for this line.
Double quotes (") can appear without being escaped.

I still have 8 pieces of fruit.

Crea array e dizionari utilizzando parentesi ( [] ) e accedi ai relativi elementi scrivendo l'indice o la chiave tra parentesi. È consentita una virgola dopo l'ultimo elemento.

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"

var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"
print(occupations)
["Jayne": "Public Relations", "Kaylee": "Mechanic", "Malcolm": "Captain"]

Le matrici crescono automaticamente man mano che aggiungi elementi.

shoppingList.append("blue paint")
print(shoppingList)
["catfish", "bottle of water", "tulips", "blue paint", "blue paint"]

Per creare un array o un dizionario vuoto, utilizzare la sintassi dell'inizializzatore.

let emptyArray = [String]()
let emptyDictionary = [String: Float]()

Se è possibile dedurre le informazioni sul tipo, è possibile scrivere un array vuoto come [] e un dizionario vuoto come [:] , ad esempio quando si imposta un nuovo valore per una variabile o si passa un argomento a una funzione.

shoppingList = []
occupations = [:]

Flusso di controllo

Usa if e switch per creare condizionali e usa for - in , for , while e repeat - while per creare loop. Le parentesi attorno alla condizione o alla variabile del ciclo sono facoltative. Sono necessari i tutori attorno al corpo.

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)
11

In un'istruzione if , il condizionale deve essere un'espressione booleana: ciò significa che un codice come if score { ... } è un errore, non un confronto implicito con zero.

Puoi utilizzare if e let insieme per lavorare con valori che potrebbero mancare. Questi valori sono rappresentati come opzionali. Un valore facoltativo contiene un valore oppure contiene nil per indicare che manca un valore. Scrivere un punto interrogativo ( ? ) dopo il tipo di valore per contrassegnare il valore come facoltativo.

var optionalString: String? = "Hello"
print(optionalString == nil)
false

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}
print(greeting)
Hello, John Appleseed

// Experiment:
// Change `optionalName` to `nil`. What greeting do you get?
// Add an `else` clause that sets a different greeting if `optionalName` is `nil`.

Se il valore facoltativo è nil , il condizionale è false e il codice tra parentesi graffe viene ignorato. Altrimenti, il valore opzionale viene scartato e assegnato alla costante dopo let , che rende disponibile il valore scartato all'interno del blocco di codice.

Un altro modo per gestire i valori facoltativi è fornire un valore predefinito utilizzando il ?? operatore. Se manca il valore facoltativo, viene utilizzato il valore predefinito.

let nickName: String? = nil
let fullName: String = "John Appleseed"
print("Hi \(nickName ?? fullName)")
Hi John Appleseed

Gli switch supportano qualsiasi tipo di dati e un'ampia varietà di operazioni di confronto: non si limitano a numeri interi e test di uguaglianza.

let vegetable = "red pepper"
switch vegetable {
case "celery":
    print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
    print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
    print("Is it a spicy \(x)?")
default:
    print("Everything tastes good in soup.")
}
Is it a spicy red pepper?

// Experiment:
// Try removing the default case. What error do you get?

Nota come let può essere utilizzato in un modello per assegnare a una costante il valore che corrisponde a quella parte di un modello.

Dopo aver eseguito il codice all'interno del caso switch corrispondente, il programma esce dall'istruzione switch. L'esecuzione non continua al caso successivo, quindi non è necessario interrompere esplicitamente lo switch alla fine del codice di ciascun caso.

Si utilizza for - in per scorrere gli elementi in un dizionario fornendo una coppia di nomi da utilizzare per ciascuna coppia chiave-valore. I dizionari sono una raccolta non ordinata, quindi le relative chiavi e valori vengono ripetuti in un ordine arbitrario.

let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)
25

// Experiment:
// Add another variable to keep track of which kind of number was the largest, as well as what that
// largest number was.

Utilizzare while per ripetere un blocco di codice finché non cambia una condizione. La condizione di un ciclo può invece essere alla fine, garantendo che il ciclo venga eseguito almeno una volta.

var n = 2
while n < 100 {
    n = n * 2
}

print(n)
128

var m = 2
repeat {
    m = m * 2
} while m < 100

print(m)
128

È possibile mantenere un indice in un ciclo, utilizzando ..< per creare un intervallo di indici o scrivendo un'inizializzazione, una condizione e un incremento espliciti. Questi due cicli fanno la stessa cosa:

var total = 0
for i in 0..<4 {
    total += i
}

print(total)
6

Utilizzare ..< per creare un intervallo che ometta il valore superiore e utilizzare ... per creare un intervallo che includa entrambi i valori.

Funzioni e Chiusure

Utilizzare func per dichiarare una funzione. Chiama una funzione facendo seguire al suo nome un elenco di argomenti tra parentesi. Utilizzare -> per separare i nomi e i tipi dei parametri dal tipo restituito dalla funzione.

func greet(name: String, day: String) -> String {
    return "Hello \(name), today is \(day)."
}
print(greet(name: "Bob", day: "Tuesday"))
Hello Bob, today is Tuesday.

// Experiment:
// Remove the `day` parameter. Add a parameter to include today’s lunch special in the greeting.

Per impostazione predefinita, le funzioni utilizzano i nomi dei parametri come etichette per i propri argomenti. Scrivi un'etichetta di argomento personalizzata prima del nome del parametro oppure scrivi _ per non utilizzare alcuna etichetta di argomento.

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
print(greet("John", on: "Wednesday"))
Hello John, today is Wednesday.

Utilizza una tupla per creare un valore composto, ad esempio per restituire più valori da una funzione. È possibile fare riferimento agli elementi di una tupla tramite nome o numero.

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }

    return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)
120
120

Le funzioni possono essere annidate. Le funzioni nidificate hanno accesso alle variabili dichiarate nella funzione esterna. È possibile utilizzare funzioni nidificate per organizzare il codice in una funzione lunga o complessa.

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
print(returnFifteen())
15

Le funzioni sono di prima classe. Ciò significa che una funzione può restituire un'altra funzione come valore.

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
print(increment(7))
8

Una funzione può accettare un'altra funzione come uno dei suoi argomenti.

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
print(hasAnyMatches(list: numbers, condition: lessThanTen))
true

Le funzioni sono in realtà un caso speciale di chiusure: blocchi di codice che possono essere richiamati in seguito. Il codice in una chiusura ha accesso a cose come variabili e funzioni che erano disponibili nell'ambito in cui è stata creata la chiusura, anche se la chiusura si trova in un ambito diverso quando viene eseguita: ne hai già visto un esempio con le funzioni annidate. È possibile scrivere una chiusura senza nome racchiudendo il codice tra parentesi graffe ( {} ). Utilizzare in per separare gli argomenti e restituire il tipo dal corpo.

let mappedNumbers = numbers.map({ (number: Int) -> Int in
    let result = 3 * number
    return result
})
print(mappedNumbers)
[60, 57, 21, 36]

// Experiment:
// Rewrite the closure to return zero for all odd numbers.

Hai diverse opzioni per scrivere le chiusure in modo più conciso. Quando il tipo di chiusura è già noto, ad esempio il callback per un delegato, è possibile omettere il tipo dei relativi parametri, il tipo restituito o entrambi. Le chiusure di singole istruzioni restituiscono implicitamente il valore della loro unica istruzione.

let mappedNumbers2 = numbers.map({ number in 3 * number })
print(mappedNumbers2)
[60, 57, 21, 36]

Puoi fare riferimento ai parametri per numero anziché per nome: questo approccio è particolarmente utile nelle chiusure molto brevi. Una chiusura passata come ultimo argomento ad una funzione può apparire immediatamente dopo le parentesi. Quando la chiusura è l'unico argomento di una funzione, puoi omettere completamente le parentesi.

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
[20, 19, 12, 7]

Oggetti e classi

Utilizzare class seguita dal nome della classe per creare una classe. Una dichiarazione di proprietà in una classe viene scritta allo stesso modo di una dichiarazione di costante o variabile, tranne che è nel contesto di una classe. Allo stesso modo, le dichiarazioni di metodi e funzioni sono scritte allo stesso modo.

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}
// Experiment:
// Add a constant property with `let`, and add another method that takes an argument.

Crea un'istanza di una classe inserendo parentesi dopo il nome della classe. Utilizza la sintassi del punto per accedere alle proprietà e ai metodi dell'istanza.

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

A questa versione della classe Shape manca qualcosa di importante: un inizializzatore per impostare la classe quando viene creata un'istanza. Usa init per crearne uno.

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(name: String) {
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

Notare come viene utilizzato self per distinguere la proprietà name dall'argomento name dell'inizializzatore. Gli argomenti all'inizializzatore vengono passati come una chiamata di funzione quando crei un'istanza della classe. Ogni proprietà necessita di un valore assegnato, nella sua dichiarazione (come con numberOfSides ) o nell'inizializzatore (come con name ).

Utilizzare deinit per creare un deinitializzatore se è necessario eseguire qualche pulizia prima che l'oggetto venga deallocato.

Le sottoclassi includono il nome della superclasse dopo il nome della classe, separato da due punti. Non è necessario che le classi creino una sottoclasse di qualsiasi classe root standard, quindi è possibile includere o omettere una superclasse secondo necessità.

I metodi su una sottoclasse che sovrascrivono l'implementazione della superclasse sono contrassegnati con override : la sovrascrittura accidentale di un metodo, senza override , viene rilevata dal compilatore come un errore. Il compilatore rileva anche i metodi con override che in realtà non sovrascrivono alcun metodo nella superclasse.

class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
print(test.area())
print(test.simpleDescription())
27.040000000000003
A square with sides of length 5.2.

// Experiment:
// - Make another subclass of `NamedShape` called `Circle` that takes a radius and a name as
//   arguments to its initializer.
// - Implement an `area()` and a `simpleDescription()` method on the `Circle` class.

Oltre alle proprietà semplici archiviate, le proprietà possono avere un getter e un setter.

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triangle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)
9.3
3.3000000000000003

Nel setter per perimeter , il nuovo valore ha il nome implicito newValue . È possibile fornire un nome esplicito tra parentesi dopo set .

Si noti che l'inizializzatore per la classe EquilateralTriangle prevede tre passaggi diversi:

  1. Impostazione del valore delle proprietà dichiarate dalla sottoclasse.

  2. Chiamare l'inizializzatore della superclasse.

  3. Modifica del valore delle proprietà definite dalla superclasse. A questo punto è possibile eseguire anche qualsiasi lavoro di configurazione aggiuntivo che utilizzi metodi, getter o setter.

Se non è necessario calcolare la proprietà ma è comunque necessario fornire il codice da eseguire prima e dopo l'impostazione di un nuovo valore, utilizzare willSet e didSet . Il codice fornito viene eseguito ogni volta che il valore cambia all'esterno di un inizializzatore. Ad esempio, la classe seguente garantisce che la lunghezza del lato del triangolo sia sempre uguale alla lunghezza del lato del quadrato.

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }
    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
10.0
10.0
50.0

Quando lavori con valori opzionali, puoi scrivere ? prima di operazioni come metodi, proprietà e abbonamento. Se il valore prima di ? è nil , tutto dopo il ? viene ignorato e il valore dell'intera espressione è nil . Altrimenti, il valore facoltativo viene scartato e tutto ciò che segue ? agisce sul valore scartato. In entrambi i casi, il valore dell'intera espressione è un valore facoltativo.

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
print(optionalSquare?.sideLength)
Optional(2.5)

Enumerazioni e strutture

Utilizzare enum per creare un'enumerazione. Come le classi e tutti gli altri tipi con nome, alle enumerazioni possono essere associati metodi.

enum Rank: Int {
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king

    func simpleDescription() -> String {
        switch self {
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
let ace = Rank.ace
print(ace)
let aceRawValue = ace.rawValue
print(aceRawValue)
ace
1

// Experiment:
// Write a function that compares two `Rank` values by comparing their raw values.

Per impostazione predefinita, Swift assegna i valori grezzi iniziando da zero e incrementando ogni volta di uno, ma puoi modificare questo comportamento specificando esplicitamente i valori. Nell'esempio precedente, ad Ace viene assegnato esplicitamente il valore grezzo 1 e il resto dei valori grezzi viene assegnato in ordine. È inoltre possibile utilizzare stringhe o numeri a virgola mobile come tipo non elaborato di un'enumerazione. Utilizzare la proprietà rawValue per accedere al valore non elaborato di un caso di enumerazione.

Utilizzare l'inizializzatore init?(rawValue:) per creare un'istanza di un'enumerazione da un valore non elaborato. Restituisce il caso di enumerazione corrispondente al valore grezzo oppure nil se non esiste Rank corrispondente.

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

I valori case di un'enumerazione sono valori effettivi, non solo un altro modo di scrivere i relativi valori grezzi. Infatti, nei casi in cui non esiste un valore grezzo significativo, non è necessario fornirne uno.

enum Suit {
    case spades, hearts, diamonds, clubs

    func simpleDescription() -> String {
        switch self {
        case .spades:
            return "spades"
        case .hearts:
            return "hearts"
        case .diamonds:
            return "diamonds"
        case .clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()
// Experiment:
// Add a `color()` method to `Suit` that returns "black" for spades and clubs, and returns "red" for
// hearts and diamonds.

Notare i due modi in cui viene fatto riferimento al caso Hearts dell'enumerazione sopra: Quando si assegna un valore alla costante hearts , al caso dell'enumerazione Suit.Hearts viene fatto riferimento con il suo nome completo perché per la costante non è specificato un tipo esplicito. All'interno dello switch, il caso dell'enumerazione è indicato con la forma abbreviata .Hearts perché è già noto che il valore di self è un seme. È possibile utilizzare la forma abbreviata ogni volta che il tipo del valore è già noto.

Se un'enumerazione ha valori grezzi, tali valori vengono determinati come parte della dichiarazione, il che significa che ogni istanza di un particolare caso di enumerazione ha sempre lo stesso valore grezzo. Un'altra scelta per i casi di enumerazione è quella di avere valori associati al caso: questi valori vengono determinati quando si crea l'istanza e possono essere diversi per ogni istanza di un caso di enumerazione. È possibile considerare i valori associati come se si comportassero come proprietà archiviate dell'istanza del caso di enumerazione.

Consideriamo ad esempio il caso di richiedere ad un server gli orari di alba e tramonto. Il server risponde con le informazioni richieste oppure con una descrizione di cosa è andato storto.

enum ServerResponse {
    case result(String, String)
    case failure(String)
}

let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")

switch success {
case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
    print("Failure...  \(message)")
}
Sunrise is at 6:00 am and sunset is at 8:09 pm.

// Experiment:
// Add a third case to `ServerResponse` and to the switch.

Notare come gli orari di alba e tramonto vengono estratti dal valore ServerResponse come parte della corrispondenza del valore con i casi di commutazione.

Usa struct per creare una struttura. Le strutture supportano molti degli stessi comportamenti delle classi, inclusi metodi e inizializzatori. Una delle differenze più importanti tra strutture e classi è che le strutture vengono sempre copiate quando vengono passate nel codice, ma le classi vengono passate per riferimento.

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
// Experiment:
// Write a function that returns an array containing a full deck of cards, with one card of each
// combination of rank and suit.

Protocolli ed estensioni

Utilizzare protocol per dichiarare un protocollo.

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

Classi, enumerazioni e strutture possono tutte adottare protocolli.

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
print(b.adjust())
print(b.simpleDescription)
()
A simple structure (adjusted)

// Experiment:
// Add another requirement to `ExampleProtocol`.
// What changes do you need to make to `SimpleClass` and `SimpleStructure` so that they still
// conform to the protocol?

Si noti l'uso della parola chiave mutating nella dichiarazione di SimpleStructure per contrassegnare un metodo che modifica la struttura. La dichiarazione di SimpleClass non necessita che nessuno dei suoi metodi sia contrassegnato come mutante perché i metodi su una classe possono sempre modificare la classe.

Utilizza extension per aggiungere funzionalità a un tipo esistente, ad esempio nuovi metodi e proprietà calcolate. Puoi utilizzare un'estensione per aggiungere la conformità al protocollo a un tipo dichiarato altrove o anche a un tipo importato da una libreria o da un framework.

extension Int: ExampleProtocol {
    public var simpleDescription: String {
        return "The number \(self)"
    }
    public mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)
The number 7

// Experiment:
// Write an extension for the `Double` type that adds an `absoluteValue` property.

È possibile utilizzare un nome di protocollo proprio come qualsiasi altro tipo con nome, ad esempio per creare una raccolta di oggetti che hanno tipi diversi ma che sono tutti conformi a un singolo protocollo. Quando si lavora con valori il cui tipo è un tipo di protocollo, i metodi esterni alla definizione del protocollo non sono disponibili.

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
A very simple class.  Now 100% adjusted.

// Uncomment to see the error.
// protocolValue.anotherProperty

Anche se la variabile protocolValue ha un tipo di runtime SimpleClass , il compilatore la considera come il tipo specificato di ExampleProtocol . Ciò significa che non è possibile accedere accidentalmente a metodi o proprietà implementate dalla classe oltre alla conformità al protocollo.

Gestione degli errori

Rappresenti gli errori utilizzando qualsiasi tipo che adotti il ​​protocollo Error .

enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

Utilizza throw per generare un errore e throws per contrassegnare una funzione che può generare un errore. Se si genera un errore in una funzione, la funzione ritorna immediatamente e il codice che ha chiamato la funzione gestisce l'errore.

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

Esistono diversi modi per gestire gli errori. Un modo è usare do-catch . All'interno del blocco do , contrassegni il codice che può generare un errore scrivendo try davanti ad esso. All'interno del blocco catch , all'errore viene assegnato automaticamente il nome error a meno che non gli venga assegnato un nome diverso.

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}
Job sent

// Experiment:
// Change the printer name to `"Never Has Toner"`, so that the `send(job:toPrinter:)` function
// throws an error.

È possibile fornire più blocchi catch che gestiscono errori specifici. Scrivi un pattern dopo catch proprio come fai dopo case in uno switch.

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    print(printerResponse)
} catch PrinterError.onFire {
    print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
    print("Printer error: \(printerError).")
} catch {
    print(error)
}
Job sent

// Experiment:
// Add code to throw an error inside the `do` block.
// What kind of error do you need to throw so that the error is handled by the first `catch` block?
// What about the second and third blocks?

Un altro modo per gestire gli errori è utilizzare try? per convertire il risultato in un facoltativo. Se la funzione genera un errore, l'errore specifico viene scartato e il risultato è nil . Altrimenti, il risultato è un facoltativo contenente il valore restituito dalla funzione.

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

Utilizzare defer per scrivere un blocco di codice che viene eseguito dopo tutto il resto del codice nella funzione, appena prima che la funzione ritorni. Il codice viene eseguito indipendentemente dal fatto che la funzione generi un errore. È possibile utilizzare defer per scrivere il codice di installazione e quello di pulizia uno accanto all'altro, anche se devono essere eseguiti in momenti diversi.

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }

    let result = fridgeContent.contains(food)
    return result
}
print(fridgeContains("banana"))
print(fridgeIsOpen)
false
false

Generici

Scrivi un nome tra parentesi angolari per creare una funzione o un tipo generico.

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
print(makeArray(repeating: "knock", numberOfTimes: 4))
["knock", "knock", "knock", "knock"]

È possibile creare forme generiche di funzioni e metodi, nonché classi, enumerazioni e strutture.

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
print(possibleInteger)
some(100)

Utilizzare where dopo il nome del tipo per specificare un elenco di requisiti, ad esempio per richiedere al tipo di implementare un protocollo, per richiedere che due tipi siano uguali o per richiedere che una classe abbia una particolare superclasse.

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
    where T.Element: Equatable, T.Element == U.Element
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
print(anyCommonElements([1, 2, 3], [3]))
true

Scrivere <T: Equatable> equivale a scrivere <T> ... where T: Equatable .