공식 문서로 공부하는 Swift (17) - 타입 캐스팅

ci·2020년 5월 31일
1

Type Casting

타입 캐스팅(type casting)은 인스턴스의 타입을 확인하거나 클래스 계층의 다른 부모 클래스/자식 클래스로 취급하는 방법이다.

Swift의 타입 캐스팅은 isas 연산자로 구현된다. 이 두 연산자는 값의 타입을 확인하거나 다른 타입으로 바꾸는 간단하고 표현적인 방법을 제공한다.

타입이 프로토콜을 따르는지 확인하는 데에도 타입 캐스팅을 사용할 수 있다.



타입 캐스팅을 위한 클래스 계층 선언

특정 클래스 인스턴스의 타입을 확인하거나 같은 계층 내 다른 클래스로 인스턴스를 변환하기 위해 클래스의 계층에서 타입 캐스팅을 사용할 수 있다. 아래 세 가지 코드는 클래스 계층과 이 클래스 인스턴스를 포함하는 배열을 정의한다.

첫 번째로 MediaItem 기반 클래스를 정의한다. String 타입의 name 프로퍼티와 init(name:) 이니셜라이저를 선언한다.

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

다음으로 MediaItem의 두 가지 자식 클래스를 정의한다. Movie는 영화에 대한 추가적인 정보를 캡슐화한다. director 프로퍼티와 이니셜라이저를 추가한다. 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)
    }
}

마지막으로 Movie 인스턴스와 Song 인스턴스를 저장하는 library 상수 배열을 만든다. library 배열의 타입은 배열 리터럴의 내용을 초기화하면서 추론된다. Swift의 타입 체커는 MovieSong이 같은 부모 클래스 MediaItem을 갖는다고 추론할 수 있다. 따라서 library 배열은 [MediaItem] 타입을 갖게 된다.

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")
]
// the type of "library" is inferred to be [MediaItem]

library 배열은 MovieSong 인스턴스를 저장한다. 하지만 배열의 내용을 순회할 때 반환되는 아이템의 타입은 MediaItem이 된다. 때문에 타입을 확인하고 서로 다른 타입으로 다운캐스트(downcast) 해야만 한다.



타입 확인하기

타입 확인 연산자 is를 사용해 인스턴스가 특정 자식 클래스 타입인지 확인한다. 타입 확인 연산자는 그 자식 클래스의 인스턴스면 true를, 아니면 false를 반환한다.

아래 예시는 Movie 인스턴스와 Song 인스턴스의 개수를 세는 movieCountsongCount 두 변수를 정의한다.

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?는 다운캐스팅을 시도한 타입의 옵셔널 값을 반환한다. 강제적 형태 as!는 다운캐스트를 시도하고 결과를 강제 언래핑 한다.

다운캐스트가 성공한다는 보장이 없을 때 as?를 사용한다. 이 형태의 연산자는 항상 옵셔널 값을 반환하고, 다운캐스트가 불가능하면 값은 nil이 된다. 이는 다운캐스트가 성공했는지 확인할 수 있게 해 준다.

다운캐스트가 항상 성공한다는 확신이 있을 때 as!를 사용한다. 이 형태의 연산자는 다운 캐스트가 실패하면 런타임 에러를 발생시킨다.

아래 예시는 library의 각 MediaItem을 순회하고 적절한 문자열로 출력한다. 이를 위해 MovieSong으로 접근해야만 한다. directorartist 프로퍼티에 접근할 필요성이 생긴다.

배열의 각 아이템이 Movie인지 Song인지 확신할 수 없기 때문에 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

캐스팅은 인스턴스를 수정하거나 값을 바꿀 수 없다. 기저의 인스턴스는 동일하게 유지된다. 변환을 시도한 타입의 인스턴스로써 간단히 취급하고 접근하는 것이다.



Any와 AnyObject를 위한 타입 캐스팅

Swift는 두 가지의 특별한 비지정 타입을 제공한다.

  • Any는 함수 타입을 포함하여 모든 타입의 인스턴스를 대표한다.
  • AnyObject는 모든 클래스 타입을 대표한다.

꼭 필요할 때만 AnyAnyObject를 사용한다. 코드에서 작동할 것이라 예상하는 타입을 지정하는 것이 항상 좋다.

다음은 여러 타입이 섞여 있는 작업을 하기 위해 Any를 사용하는 예시를 보여 준다.

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

Any 또는 AnyObject로만 알려진 상수나 변수의 타입을 확인하기 위해 switch문의 케이스에서 isas 패턴을 사용할 수 있다.

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

Any는 옵셔널 타입을 포함한 모든 타입의 값을 대표한다. Swift는 Any가 예상되는 값에 옵셔널 값을 사용할 경우 경고할 것이다. 만약 Any 값에 정말 옵셔널을 사용하길 원한다면 옵셔널을 Any로 명시적으로 변환하기 위해 as 연산자를 사용할 수 있다.

let optionalNumber: Int? = 3
things.append(optionalNumber)        // Warning
things.append(optionalNumber as Any) // No warning

0개의 댓글