Swift - Type Casting

타입 캐스팅은 인스턴스의 타입을 확인하거나 해당 인스턴스를 클래스 계층 구조 내의 수퍼 클래스 또는 하위 클래스로 취급하는 방법이다. Swift의 타입 캐스팅은 isas 연산자로 구현되며, 두 연산자는 값의 유형을 확인하거나 값을 다른 유형으로 캐스팅하는데 사용된다.

Defining a Class Hierarchy for Type Casting

타입 캐스팅을 사용하면 특정 클래스 인스턴스의 타입을 확인하고 해당 인스턴스를 동일한 클래스 계층 구조 내의 다른 클래스로 캐스팅 할 수 있다. 예제 코드를 살펴보자. 기본 클래스인 MediaItem과 이를 상속한 Movie, Song 클래스와 이 두 클래스의 인스턴스를 모두 포함하는 library 배열을 정의했다.

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

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

let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
    Movie(name: "Citizen Kane", director: "Orson Welles"),
    Song(name: "The One And Only", artist: "Chesney Hawkes"),
    Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]

MovieSong 모두 MediaItem이라는 공통 수퍼 클래스가 있기 때문에 library의 타입은 [MediaItem]으로 swift의 타입 시스템이 추론한다. 실제로 library에 저장된 요소는 MovieSong의 인스턴스이지만 배열을 iterate 하는 경우 모든 항목은 Movie 또는 Song이 아닌 MediaItem으로 표현된다. 원래 타입으로 작업하려면 타입을 확인하거나 다른 타입으로 다운 캐스트해야한다.

Checking Type

인스턴스의 타입을 검사하기 위해서는 is 연산을 사용한다. is 연산자는 인스턴스의 타입을 검사하여 Bool 값을 반환한다.

var movieCount = 0
var songCount = 0

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

print("Media library contains \(movieCount) movies and \(songCount) songs")
// Prints "Media library contains 2 movies and 3 songs"

Downcasting

as?, as! 연산은 타입 캐스팅을 하는데에 사용되는 연산이다. 타입 캐스팅은 계층 관계를 가지는 타입 간에만 가능하다. 계층 관계가 없는 타입간에는 무조건 캐스팅이 실패하게 된다. 다운 캐스팅을 실패 할 수 있기 때문에 타입 캐스트 연산은 두 가지 다른 형태로 제공된다.

as?는 다운 캐스트하려는 타입의 옵셔널 값을 반환하며, 다운 캐스트가 불가능한 경우 값은 nil이 된다. as! 연산은 다운 캐스트를 시도하고 결과를 강제 언랩핑한다. 만약 잘못된 클래스 타입으로 다운 캐스트하려고 하면 런타임 오류가 발생한다. 따라서, 다운 캐스트가 성공할지 확실하지 않은 경우 as? 연산을 사용하고 다운 캐스트가 항상 성공할 것이라고 확신하는 경우에는 as!의 연산을 사용할 수 있다.

for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: \(song.name), by \(song.artist)")
    }
}

// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley

Type Casting for Any and AnyObject

Swift는 Any, AnyObject라는 두 가지 특수 타입을 제공한다. Any는 함수를 포함하여 모든 타입의 인스턴스를 나타낼 수 있고, AnyObject는 모든 클래스 타입의 인스턴스를 나타낼 수 있다. 이들이 제공하는 동작과 기능이 명시적으로 필요한 경우에만 AnyAnyObject를 사용해아 한다. 사용할때는 실제 타입으로의 다운캐스팅이 대부분 필요하기 때문에 런타임 성능에 좋지 못한 영향을 주고, 설계상으로도 바람직하지 않다.

var things: [Any] = []

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })

things의 원소들을 사용할때는 switch문과 is, as 구문을 조합하여 사용할 수 있다.

for thing in things {
    switch thing {
    case 0 as Int:
        print("zero as an Int")
    case 0 as Double:
        print("zero as a Double")
    case let someInt as Int:
        print("an integer value of \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("a positive double value of \(someDouble)")
    case is Double:
        print("some other double value that I don't want to print")
    case let someString as String:
        print("a string value of \"\(someString)\"")
    case let (x, y) as (Double, Double):
        print("an (x, y) point at \(x), \(y)")
    case let movie as Movie:
        print("a movie called \(movie.name), dir. \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Michael"))
    default:
        print("something else")
    }
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called Ghostbusters, dir. Ivan Reitman
// Hello, Michael

Reference

https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html