Szybka wycieczka

Na podstawie oryginalnej wycieczki A Swift Tour na Swift.org z modyfikacjami. Autorem oryginalnej treści jest firma Apple Inc. Licencja Creative Commons Uznanie autorstwa 4.0 Międzynarodowe (CC BY 4.0) .
Zobacz na TensorFlow.org Uruchom w Google Colab Zobacz źródło w GitHub

Tradycja podpowiada, że ​​pierwszy program w nowym języku powinien wypisać słowa „Witaj, świecie!” na ekranie. W Swift można to zrobić w jednej linii:

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

Jeśli napisałeś kod w języku C lub Objective-C, ta składnia wygląda znajomo — w Swift ten wiersz kodu stanowi kompletny program. Nie musisz importować osobnej biblioteki, aby uzyskać takie funkcje, jak wejście/wyjście lub obsługa ciągów. Kod napisany w zasięgu globalnym służy jako punkt wejścia dla programu, więc nie potrzebujesz funkcji main() . Nie musisz także wpisywać średników na końcu każdej instrukcji.

Ta wycieczka zawiera wystarczającą ilość informacji, aby rozpocząć pisanie kodu w języku Swift, pokazując, jak wykonywać różne zadania programistyczne. Nie martw się, jeśli czegoś nie rozumiesz — wszystko, co zostało przedstawione w tej wycieczce, zostało szczegółowo wyjaśnione w dalszej części tej książki.

Proste wartości

Użyj let , aby utworzyć stałą i var , aby utworzyć zmienną. Wartość stałej nie musi być znana w czasie kompilacji, ale należy jej przypisać wartość dokładnie raz. Oznacza to, że możesz używać stałych do nazywania wartości, którą określisz raz, ale użyjesz w wielu miejscach.

var myVariable = 42
myVariable = 50
let myConstant = 42

Stała lub zmienna musi być tego samego typu, co wartość, którą chcesz do niej przypisać. Jednak nie zawsze trzeba jawnie pisać typ. Podanie wartości podczas tworzenia stałej lub zmiennej pozwala kompilatorowi wywnioskować jej typ. W powyższym przykładzie kompilator wnioskuje, że myVariable jest liczbą całkowitą, ponieważ jej wartość początkowa jest liczbą całkowitą.

Jeżeli wartość początkowa nie dostarcza wystarczających informacji (lub nie ma wartości początkowej), określ typ wpisując go po zmiennej, oddzielając ją dwukropkiem. Uwaga: użycie Double zamiast Float dla liczb zmiennoprzecinkowych zapewnia większą precyzję i jest domyślnym typem zmiennoprzecinkowym w 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.

Wartości nigdy nie są niejawnie konwertowane na inny typ. Jeśli chcesz przekonwertować wartość na inny typ, jawnie utwórz instancję żądanego typu.

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?

Istnieje jeszcze prostszy sposób dołączania wartości do ciągów znaków: wpisz wartość w nawiasach i wpisz ukośnik odwrotny (``) przed nawiasami. Na przykład:

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.

W przypadku ciągów znaków zajmujących wiele wierszy należy użyć trzech podwójnych cudzysłowów ( """ ). Wcięcie na początku każdego wiersza w cudzysłowie jest usuwane, o ile odpowiada wcięciu cudzysłowu zamykającego. Na przykład:

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.

Twórz tablice i słowniki za pomocą nawiasów ( [] ) i uzyskuj dostęp do ich elementów, wpisując indeks lub klucz w nawiasach. Po ostatnim elemencie dozwolony jest przecinek.

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"]

Tablice powiększają się automatycznie w miarę dodawania elementów.

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

Aby utworzyć pustą tablicę lub słownik, użyj składni inicjatora.

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

Jeśli można wywnioskować informacje o typie, możesz zapisać pustą tablicę jako [] i pusty słownik jako [:] — na przykład, ustawiając nową wartość zmiennej lub przekazując argument do funkcji.

shoppingList = []
occupations = [:]

Kontrola przepływu

Użyj if i switch , aby utworzyć warunki, i użyj for - in , for , while i repeat - while aby utworzyć pętle. Nawiasy wokół warunku lub zmiennej pętli są opcjonalne. Wymagane są szelki wokół ciała.

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

W instrukcji if warunek musi być wyrażeniem boolowskim — oznacza to, że kod taki jak if score { ... } jest błędem, a nie ukrytym porównaniem do zera.

Możesz użyć if i let razem, aby pracować z wartościami, których może brakować. Wartości te są reprezentowane jako opcjonalne. Wartość opcjonalna zawiera wartość lub zawiera nil aby wskazać, że brakuje wartości. Wpisz znak zapytania ( ? ) po typie wartości, aby oznaczyć wartość jako opcjonalną.

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`.

Jeśli opcjonalna wartość to nil , warunek ma wartość false i kod w nawiasach klamrowych jest pomijany. W przeciwnym razie opcjonalna wartość jest rozpakowywana i przypisywana do stałej po let , co sprawia, że ​​rozpakowana wartość jest dostępna w bloku kodu.

Innym sposobem obsługi wartości opcjonalnych jest podanie wartości domyślnej za pomocą znaku ?? operator. Jeśli brakuje wartości opcjonalnej, zamiast niej zostanie użyta wartość domyślna.

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

Przełączniki obsługują dowolny rodzaj danych i szeroką gamę operacji porównawczych — nie ograniczają się one do liczb całkowitych i testów równości.

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?

Zwróć uwagę, jak można użyć let we wzorcu, aby przypisać wartość pasującą do tej części wzorca do stałej.

Po wykonaniu pasującego kodu znajdującego się w przełączniku program kończy działanie instrukcji switch. Wykonywanie nie jest kontynuowane w następnym przypadku, więc nie ma potrzeby jawnego przerywania przełącznika na końcu kodu każdego przypadku.

Używasz for - in do iteracji po elementach słownika, podając parę nazw do użycia dla każdej pary klucz-wartość. Słowniki są kolekcją nieuporządkowaną, więc ich klucze i wartości są iterowane w dowolnej kolejności.

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.

Użyj while , aby powtarzać blok kodu, aż do zmiany warunku. Zamiast tego warunek pętli może znajdować się na końcu, co gwarantuje, że pętla zostanie uruchomiona przynajmniej raz.

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

Możesz utrzymać indeks w pętli — albo używając ..< aby utworzyć zakres indeksów, albo pisząc jawną inicjalizację, warunek i przyrost. Te dwie pętle robią to samo:

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

print(total)
6

Użyj ..< , aby utworzyć zakres pomijający górną wartość, i użyj ... aby utworzyć zakres zawierający obie wartości.

Funkcje i zamknięcia

Użyj func , aby zadeklarować funkcję. Wywołaj funkcję, po jej nazwie z listą argumentów w nawiasach. Użyj -> aby oddzielić nazwy i typy parametrów od typu zwracanego przez funkcję.

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.

Domyślnie funkcje używają nazw parametrów jako etykiet argumentów. Napisz niestandardową etykietę argumentu przed nazwą parametru lub napisz _ , aby nie używać etykiety argumentu.

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

Użyj krotki, aby utworzyć wartość złożoną — na przykład, aby zwrócić wiele wartości z funkcji. Do elementów krotki można odwoływać się za pomocą nazwy lub numeru.

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

Funkcje można zagnieżdżać. Funkcje zagnieżdżone mają dostęp do zmiennych, które zostały zadeklarowane w funkcji zewnętrznej. Funkcji zagnieżdżonych można używać do organizowania kodu w funkcję, która jest długa lub złożona.

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

Funkcje są typu pierwszorzędnego. Oznacza to, że funkcja może zwrócić inną funkcję jako swoją wartość.

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

Funkcja może przyjmować inną funkcję jako jeden z argumentów.

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

Funkcje są właściwie szczególnym przypadkiem domknięć: blokami kodu, które można wywołać później. Kod w zamknięciu ma dostęp do takich rzeczy, jak zmienne i funkcje, które były dostępne w zakresie, w którym utworzono zamknięcie, nawet jeśli zamknięcie znajduje się w innym zakresie w momencie jego wykonania — przykład tego widziałeś już w przypadku funkcji zagnieżdżonych. Możesz napisać zamknięcie bez nazwy, otaczając kod nawiasami klamrowymi ( {} ). Użyj in , aby oddzielić argumenty i typ zwracany od treści.

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.

Masz kilka możliwości bardziej zwięzłego pisania zamknięć. Gdy typ zamknięcia jest już znany, na przykład wywołanie zwrotne dla delegata, można pominąć typ jego parametrów, typ zwracany lub jedno i drugie. Zamknięcia pojedynczych instrukcji domyślnie zwracają wartość ich jedynej instrukcji.

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

Do parametrów można odwoływać się po numerze, a nie po nazwie — to podejście jest szczególnie przydatne w przypadku bardzo krótkich zamknięć. Zamknięcie przekazane jako ostatni argument funkcji może pojawić się bezpośrednio po nawiasach. Jeśli zamknięcie jest jedynym argumentem funkcji, można całkowicie pominąć nawiasy.

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

Obiekty i klasy

Użyj class , po której następuje nazwa klasy, aby utworzyć klasę. Deklarację właściwości w klasie zapisuje się w taki sam sposób, jak deklarację stałej lub zmiennej, z tą różnicą, że odbywa się ona w kontekście klasy. Podobnie deklaracje metod i funkcji są pisane w ten sam sposób.

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.

Utwórz instancję klasy, umieszczając nawiasy po nazwie klasy. Użyj składni kropki, aby uzyskać dostęp do właściwości i metod instancji.

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

W tej wersji klasy Shape brakuje czegoś ważnego: inicjatora konfigurującego klasę podczas tworzenia instancji. Użyj init , aby go utworzyć.

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

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

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

Zwróć uwagę, jak self służy do odróżnienia właściwości name od argumentu name od inicjatora. Argumenty do inicjatora są przekazywane podobnie jak wywołanie funkcji podczas tworzenia instancji klasy. Każda właściwość wymaga przypisania wartości — albo w deklaracji (jak w przypadku numberOfSides ), albo w inicjatorze (jak w przypadku name ).

Użyj deinit , aby utworzyć deinicjalizator, jeśli chcesz przeprowadzić czyszczenie przed zwolnieniem obiektu.

Podklasy zawierają nazwę nadklasy po nazwie klasy, oddzieloną dwukropkiem. Nie ma wymogu, aby klasy tworzyły podklasy dowolnej standardowej klasy głównej, więc w razie potrzeby możesz dodać lub pominąć nadklasę.

Metody podklasy, które zastępują implementację nadklasy, są oznaczone jako override — przypadkowe przesłonięcie metody bez override jest wykrywane przez kompilator jako błąd. Kompilator wykrywa również metody z override , które w rzeczywistości nie zastępują żadnej metody w nadklasie.

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.

Oprócz prostych właściwości, które są przechowywane, właściwości mogą mieć funkcję pobierającą i ustawiającą.

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

W metodzie ustawiającej perimeter nowa wartość ma domyślną nazwę newValue . Po set możesz podać wyraźną nazwę w nawiasach.

Zauważ, że inicjator klasy EquilateralTriangle składa się z trzech różnych kroków:

  1. Ustawienie wartości właściwości deklarowanych przez podklasę.

  2. Wywołanie inicjatora nadklasy.

  3. Zmiana wartości właściwości zdefiniowanych przez nadklasę. W tym momencie można również wykonać wszelkie dodatkowe prace konfiguracyjne wykorzystujące metody, moduły pobierające i ustawiające.

Jeśli nie musisz obliczać właściwości, ale nadal musisz podać kod uruchamiany przed i po ustawieniu nowej wartości, użyj willSet i didSet . Podany kod jest uruchamiany za każdym razem, gdy wartość zmienia się poza inicjatorem. Na przykład poniższa klasa zapewnia, że ​​długość boku trójkąta jest zawsze taka sama jak długość boku kwadratu.

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

Podczas pracy z wartościami opcjonalnymi możesz napisać ? przed operacjami, takimi jak metody, właściwości i indeksy dolne. Jeśli wartość przed ? wynosi nil , wszystko po ? jest ignorowany, a wartość całego wyrażenia wynosi nil . W przeciwnym razie opcjonalna wartość zostanie rozpakowana i wszystko po ? działa na wartość nieopakowaną. W obu przypadkach wartość całego wyrażenia jest wartością opcjonalną.

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

Wyliczenia i struktury

Użyj enum , aby utworzyć wyliczenie. Podobnie jak klasy i wszystkie inne nazwane typy, z wyliczeniami mogą być powiązane metody.

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.

Domyślnie Swift przypisuje surowe wartości, zaczynając od zera i zwiększając za każdym razem o jeden, ale możesz zmienić to zachowanie, jawnie określając wartości. W powyższym przykładzie Asowi wyraźnie przypisano surową wartość 1 , a pozostałe surowe wartości zostały przypisane w odpowiedniej kolejności. Jako surowego typu wyliczenia można także używać ciągów lub liczb zmiennoprzecinkowych. Użyj właściwości rawValue , aby uzyskać dostęp do nieprzetworzonej wartości przypadku wyliczenia.

Użyj inicjatora init?(rawValue:) , aby utworzyć instancję wyliczenia na podstawie surowej wartości. Zwraca przypadek wyliczenia pasujący do wartości surowej lub nil , jeśli nie ma pasującego Rank .

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

Wartości wielkości wyliczeń są wartościami rzeczywistymi, a nie tylko innym sposobem zapisywania ich surowych wartości. W rzeczywistości w przypadkach, gdy nie ma znaczącej wartości surowej, nie musisz jej podawać.

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.

Zwróć uwagę na dwa sposoby, w jakie przypadek Hearts w wyliczeniu jest wspomniany powyżej: Podczas przypisywania wartości do stałej hearts , przypadek wyliczeniowy Suit.Hearts jest określany jego pełną nazwą, ponieważ stała nie ma określonego jawnego typu. Wewnątrz przełącznika przypadek wyliczenia jest określany w skróconej formie .Hearts , ponieważ wartość self jest już znana jako kolor. Możesz użyć skróconej formy w dowolnym momencie, gdy typ wartości jest już znany.

Jeśli wyliczenie ma wartości surowe, wartości te są określane w ramach deklaracji, co oznacza, że ​​każde wystąpienie konkretnego przypadku wyliczenia ma zawsze tę samą wartość surową. Inną opcją w przypadku przypadków wyliczeniowych jest skojarzenie wartości z przypadkami — wartości te są określane podczas tworzenia instancji i mogą być różne dla każdego wystąpienia przypadku wyliczeniowego. Można myśleć o powiązanych wartościach jako o przechowywanych właściwościach instancji przypadku wyliczenia.

Rozważmy na przykład przypadek zażądania z serwera godziny wschodu i zachodu słońca. Serwer albo odpowiada żądanymi informacjami, albo odpowiada opisem tego, co poszło nie tak.

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.

Zwróć uwagę, jak czasy wschodu i zachodu słońca są wyodrębniane z wartości ServerResponse w ramach dopasowywania wartości do przypadków przełączenia.

Użyj struct aby utworzyć strukturę. Struktury obsługują wiele takich samych zachowań jak klasy, w tym metody i inicjatory. Jedną z najważniejszych różnic między strukturami i klasami jest to, że struktury są zawsze kopiowane, gdy są przekazywane w kodzie, ale klasy są przekazywane przez referencję.

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.

Protokoły i rozszerzenia

Użyj protocol , aby zadeklarować protokół.

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

Klasy, wyliczenia i struktury mogą przyjmować protokoły.

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?

Zwróć uwagę na użycie słowa kluczowego mutating w deklaracji SimpleStructure w celu oznaczenia metody modyfikującej strukturę. Deklaracja SimpleClass nie wymaga oznaczenia żadnej z jej metod jako mutującej, ponieważ metody klasy zawsze mogą ją modyfikować.

Użyj extension aby dodać funkcje do istniejącego typu, takie jak nowe metody i obliczone właściwości. Możesz użyć rozszerzenia, aby dodać zgodność protokołu z typem zadeklarowanym gdzie indziej lub nawet z typem zaimportowanym z biblioteki lub platformy.

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.

Nazwy protokołu można używać tak samo, jak każdego innego nazwanego typu — na przykład do tworzenia kolekcji obiektów różnych typów, ale zgodnych z jednym protokołem. Podczas pracy z wartościami, których typem jest typ protokołu, metody spoza definicji protokołu nie są dostępne.

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

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

Mimo że zmienna protocolValue ma typ wykonawczy SimpleClass , kompilator traktuje ją jako dany typ ExampleProtocol . Oznacza to, że nie można przypadkowo uzyskać dostępu do metod lub właściwości, które klasa implementuje oprócz zgodności z protokołem.

Obsługa błędów

Błędy reprezentujesz przy użyciu dowolnego typu, który przyjmuje protokół Error .

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

Użyj throw , aby zgłosić błąd, i throws , aby zaznaczyć funkcję, która może zgłosić błąd. Jeśli zgłosisz błąd w funkcji, funkcja natychmiast powraca, a kod, który wywołał tę funkcję, obsługuje błąd.

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

Istnieje kilka sposobów radzenia sobie z błędami. Jednym ze sposobów jest użycie do-catch . Wewnątrz bloku do zaznaczasz kod, który może zgłosić błąd, wpisując przed nim try. Wewnątrz bloku catch automatycznie zostanie nadana nazwa error , chyba że nadasz mu inną nazwę.

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.

Możesz podać wiele bloków catch , które obsługują określone błędy. Po catch piszesz wzór tak samo, jak robisz to po case w przełączniku.

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?

Innym sposobem radzenia sobie z błędami jest użycie try? aby przekonwertować wynik na opcjonalny. Jeśli funkcja zgłosi błąd, konkretny błąd zostanie odrzucony, a wynikiem będzie nil . W przeciwnym razie wynik jest opcjonalny i zawiera wartość zwróconą przez funkcję.

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

Użyj opcji defer , aby napisać blok kodu, który będzie wykonywany po wszystkich innych kodach funkcji, tuż przed powrotem funkcji. Kod jest wykonywany niezależnie od tego, czy funkcja zgłosi błąd. Możesz użyć defer , aby zapisać kod instalacyjny i czyszczący obok siebie, nawet jeśli muszą być wykonywane w różnym czasie.

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

Genetyki

Wpisz nazwę w nawiasach ostrych, aby utworzyć ogólną funkcję lub typ.

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"]

Można tworzyć ogólne formy funkcji i metod, a także klasy, wyliczenia i struktury.

// 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)

Użyj where po nazwie typu, aby określić listę wymagań — na przykład, aby wymagać od typu implementacji protokołu, wymagać, aby dwa typy były takie same lub wymagać, aby klasa miała określoną nadklasę.

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

Zapisanie <T: Equatable> jest równoznaczne z zapisaniem <T> ... where T: Equatable .