Link Search Menu Expand Document

Приведення типів

Приведення типів – це спосіб перевірити тип екземпляру, або працювати з екземпляром як з іншим батьківським класом чи класом-нащадком з іншого місця в ієрархії класів.

Приведення типів у Swift реалізоване у вигляді операторів is та as. Ці оператори дають простий та виразний спосіб перевірити тип значення чи привести значення до іншого типу.

За допомогою приведення типу можна перевірити, чи підпорядковується тип протоколу, як описано у підрозділі Перевірка на підпорядкованість протоколу.

Визначення ієрархії класів для приведення типів

Приведення типів можна використовувати з ієрархією класів для перевірки типу певного екземпляра класу, та для приведення екземпляру до іншого класу в цій ієрархії. У трьох зразках коду далі оголошено ієрархію класів та масив, що містить екземпляри цих класів, для подальшого використання у прикладах з приведенням типів.

У першому фрагменті коду визначено базовий клас на ім’я MediaItem. Цей клас реалізовує базову функціональність елементу цифрової медіа-бібліотеки. Зокрема, в ньому оголошено властивість name типу String для представлення назви елементу, та ініціалізатор init name. (Вважатимемо, що всі елементи медіа-бібліотеки, включно з усіма фільмами та піснями, мають назву).

class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

У наступному прикладі оголошено два класи-нащадки MediaItem. Перший нащадок, клас Movie, інкапсулює додаткову інформацію про кінофільм. Він для представлення інформації про режисера додає властивість director до базового класу MediaItem, та відповідний ініціалізатор. Другий клас-нащадок, Song, для представлення інформації про виконавця додає властивість artist та відповідний ініціалізатор поверх базового класу:

class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

У останньому фрагменті створюється масив на ім’я library, котрий містить медіа-бібліотеку: два екземпляри Movie та три екземпляри Song. Тип масиву library виведено через ініціалізацію його літералом масиву. Система перевірки типів Swift здатна визначити, що класи Movie та Song мають спільний батьківський клас MediaItem, і тому визначає [MediaItem] типом масиву library:

let library = [
    Movie(name: "Дике поле", director: "Сергій Жадан"),
    Song(name: "Земля", artist: "Riffmaster"),
    Movie(name: "Захар Беркут", director: "Ахтем Сейтаблаєв"),
    Song(name: "100% плагіат", artist: "Тартак"),
    Song(name: "Аркан", artist: "Карна")
]
// тип масиву "library" визначено як [MediaItem]

Елементи, що зберігаються в бібліотеці, все ще є екземплярами Movie та Song за лаштунками. Однак, якщо ітерувати елементи цього масиву, на виході будемо отримувати елементи, типізовані як MediaItem, а не як Movie чи Song. Щоб працювати із їх реальним типом, слід перевірити їх тип, або привести їх до іншого типу, як описано нижче.

Перевірка типу

Оператор перевірки типу (is, або дослівно “є”) перевіряє, чи є екземпляр певного типу. Оператор перевірки типу повертає true, якщо екземпляр має вказаний тип, і false, якщо не має.

У прикладі нижче оголошено дві змінні, movieCount та songCount, котрі зберігатимуть кількість екземплярів Movie та Song у масиві library:

var movieCount = 0
var songCount = 0

for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    }
}

print("Медіа-бібліотека містить \(movieCount) фільми та \(songCount) пісні")
// Надрукує "Медіа-бібліотека містить 2 фільми та 3 пісні"

У цьому прикладі йде ітерування всіх елементів масиву library. Під час кожного проходу, цикл for-in присвоює константі itemнаступний нащадок MediaItem у масиві.
item is Movie повертає true якщо поточний MediaItem є екземпляром Movie, та false в іншому випадку. Аналогічно, item is Song перевіряє, чи є item екземпляром класу Song. Після виконання циклу for-in, значення movieCount та songCount містять кількості екземплярів кожного з нащадків MediaItem.

Приведення типів

Константа чи змінна певного типу може фактично посилатись на екземпляр класу-нащадку за лаштунками. Якщо є певна впевненість у цьому в конкретному випадку, можна привести тип до типу класу-нащадку, за допомогою оператора приведення типів (as? or as!, дослівно “як”).

Оскільки приведення типів може завершитись невдало, оператор приведення типів має дві різні форми. Умовна форма, as?, повертає опціональне значення типу, до якого приводиться екземпляр. Примусова форма, as!, пробує привести тип і після цього примусово розгортає опціонал – все це в ході єдиної монолітної операції.

Слід користуватись умовною формою оператора приведення типів (as?), коли немає впевненості в тому, що приведення типів спрацює. Ця форма оператора завжди повертає опціональне значення, і у випадках, коли приведення типів неможливе, буде повернуто значення nil. Це дозволяє перевірити, чи було приведення типів вдалим.

Примусовою формою оператору (as!) слід користуватись лише при повній впевненості, що приведення типів завжди буде успішним. Ця форма оператора призводить до помилки часу виконання при спробі приведення до некоректного типу.

У прикладі нижче відбувається ітерування по елементах MediaItem у масиві library, та друкується опис кожного елементу. Для цього потрібно звертатись до кожного з елементів як до екземпляра Movie чи Song, а не просто як до MediaItem. Це необхідно для того, щоб мати доступ до властивостей director чи artist екземплярів Movie чи Song відповідно, для використання їх в описі.

В цьому прикладі, кожен елемент у масиві може бути екземпляром Movie, або екземпляром Song. Ми не знаємо наперед, який клас використовувати для кожного елементу, і тому в ітераціях циклу доречно буде користуватись умовною формою оператора приведення типів (as?):

for item in library {
    if let movie = item as? Movie {
        print("Фільм: \(movie.name), режисер: \(movie.director)")
    } else if let song = item as? Song {
        print("Пісня: \(song.name), виконує: \(song.artist)")
    }
}

// Фільм: Дике поле, режисер: Сергій Жадан
// Пісня: Земля, виконує: Riffmaster
// Фільм: Захар Беркут, режисер: Ахтем Сейтаблаєв
// Пісня: 100% плагіат, виконує: Тартак
// Пісня: Аркан, виконує: Карна

Даний приклад починається зі спроби привести поточний item до типу Movie. Оскільки елемент є екземпляром MediaItem, можливо, що він має клас Movie; аналогічно, елемент також може мати клас Song, чи навіть лише базовий MediaItem. Через цю невизначеність, форма as? оператору приведення типів повертає опціональне значення при спробі приведення до типу класу-нащадка. Результатом item as? Movie є тип Movie?, або “опціональний Movie”.

Приведення до типу Movie буде невдалим при застосуванні до екземплярів Song у масиві library. Щоб з цим справитись, в даному прикладі використовується прив’язування опціоналу для перевірки, що опціональний Movie насправді містить значення (і таким чином перевірити результат приведення типу). Це прив’язування опціоналу записується як “if let movie = item as? Movie”, що можна прочитати, як:

“Спробувати звернутись до item як до Movie. Якщо це буде успішним, присвоїти результат новій тимчасовій константі на ім’я movie та зберегти в неї значення опціоналу Movie

Якщо приведення типу буде успішним, властивості movie можна буде використовувати для друку опису цього екземпляру Movie, включно з ім’ям режисера, що зберігається у властивості director. Аналогічний принцип використовується для перевірки екземплярів Song та друку відповідного опису (включно з іменем виконавця, що зберігається у властивості artist) щоразу, коли в масиві library знайдено екземпляр Song.

Примітка

Приведення типу фактично ніяк не змінює екземпляр чи його властивості. Екземпляр, на котрий посилається змінна, залишається тим же; просто до нього звертаються як до екземпляру того типу, до якого відбулось приведення.

Приведення типів для Any та AnyObject

У Swift є два спеціальні типи для робити з абстрактними типами:

  • Any може представляти екземпляри будь-якого типу взагалі, включаючи функціональні типи.
  • AnyObject може представляти екземпляри будь-якого типу-класу.

Слід використовувати Any та AnyObject тільки у випадках, коли явно потрібна поведінка та можливості, що вони дають. Завжди краще бути конкретним щодо типів, з якими повинен працювати ваш код.

Ось приклад використання Any для роботи із сумішшю різних типів, включаючи функціональні типи та не класи. У прикладі створюється масив на ім’я things, котрий може зберігати значення типу Any:

var things = [Any]()

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("привіт")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Привіт, \(name)" })

Масив things містить два значення Int, два значення Double, рядок, кортеж типу (Double, Double), фільм “Ghostbusters”, та замикання, котре приймає рядок, і повертає інший рядок.

Щоб з’ясувати фактичний тип константи чи змінної типу Any чи AnyObject, можна використовувати оператори is або as у шаблоні випадків інструкції switch. У прикладі нижче йде ітерування елементів масиву things та перебір можливих типів у інструкції switch. Кілька випадків switch прив’язують значення, що співпало, до константи відповідного типу, для того щоб надрукувати це значення:

for thing in things {
    switch thing {
    case 0 as Int:
        print("нуль як Int")
    case 0 as Double:
        print("нуль як Double")
    case let someInt as Int:
        print("цілочисельне значення \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("додатнє дійсне значення \(someDouble)")
    case is Double:
        print("інше дійсне значення яке ми не хочемо друкувати")
    case let someString as String:
        print("рядок \"\(someString)\"")
    case let (x, y) as (Double, Double):
        print("точка (x, y) з координатами \(x), \(y)")
    case let movie as Movie:
        print("фільм з назвою \(movie.name), режисер: \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Василина"))
    default:
        print("щось інше")
    }
}

// нуль як Int
// нуль як Double
// цілочисельне значення 42
// додатнє дійсне значення 3.14159
// рядок "привіт"
// точка (x, y) з координатами 3.0, 5.0
// фільм з назвою Ghostbusters, режисер: Ivan Reitman
// Привіт, Василина

Примітка

Тип Any представляє значення будь-якого типу, включно з опціональними типами. Swift дає попередження, якщо використати опціональне значення там, де очікується Any. Якщо дійсно потрібно використати опціональне значення як Any, можна використати оператор as для явного приведення опціоналу до Any, як показано нижче.

let optionalNumber: Int? = 3
things.append(optionalNumber)        // Попередження
things.append(optionalNumber as Any) // Нема попередження