타입 캐스팅
타입 캐스팅
값의 런타임 타입을 확인하고, 그 값에 더 구체적인 타입 정보를 붙여 주는 방법을 다뤄요.
*타입 캐스팅(Type Casting)*은 인스턴스의 타입을 확인하거나, 그 인스턴스를 자신의 클래스 계층 안에 있는 다른 슈퍼클래스나 서브클래스로 취급하는 방법이에요.
Swift에서 타입 캐스팅은 is와 as 연산자로 구현돼요. 이 두 연산자는 값의 타입을 확인하거나 값을 다른 타입으로 캐스팅하는 간단하고 표현력 좋은 방법을 제공하죠.
타입 캐스팅을 이용해 어떤 타입이 프로토콜을 준수하는지 확인할 수도 있는데, 이건 doc:Protocols#Checking-for-Protocol-Conformance에서 다뤄요.
본문
타입 캐스팅을 위한 클래스 계층 정의하기
클래스와 서브클래스의 계층 구조에서 타입 캐스팅을 쓰면, 특정 클래스 인스턴스의 타입을 확인하고 그 인스턴스를 같은 계층 안의 다른 클래스로 캐스팅할 수 있어요. 아래 세 코드 조각이 클래스 계층과 그 인스턴스들을 담은 배열을 정의하는데, 이걸 타입 캐스팅 예제에 사용할 거예요.
첫 번째 조각은 MediaItem이라는 새 기반 클래스를 정의해요. 이 클래스는 디지털 미디어 라이브러리에 등장하는 어떤 항목이든 기본 기능을 제공하죠. 구체적으로 String 타입의 name 프로퍼티와 init(name:) 이니셜라이저를 선언해요. (모든 미디어 항목, 즉 영화와 노래를 포함해서, 이름을 가질 거라고 가정해요.)
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
다음 조각은 MediaItem의 서브클래스 두 개를 정의해요. 첫 번째 서브클래스인 Movie는 영화나 필름에 대한 추가 정보를 담아요. 기반 MediaItem 클래스 위에 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)
}
}
마지막 조각은 library라는 상수 배열을 만들어요. 여기엔 Movie 인스턴스 두 개와 Song 인스턴스 세 개가 들어 있죠. library 배열의 타입은 배열 리터럴의 내용으로 초기화하면서 추론돼요. Swift의 타입 검사기가 Movie와 Song에 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에 저장된 항목들은 뒤에서 여전히 Movie와 Song 인스턴스예요. 하지만 이 배열의 내용을 순회하면 받아오는 항목들의 타입은 Movie나 Song이 아니라 MediaItem이에요. 이 항목들을 원래 타입으로 다루려면 아래에서 설명하는 것처럼 타입을 확인하거나 다른 타입으로 다운캐스팅해야 해요.
타입 확인하기
타입 확인 연산자(is)를 사용해 인스턴스가 특정 서브클래스 타입인지 확인할 수 있어요. 타입 확인 연산자는 인스턴스가 그 서브클래스 타입이면 true를, 아니면 false를 반환해요.
아래 예제는 movieCount와 songCount라는 변수 두 개를 정의하는데, library 배열 안의 Movie와 Song 인스턴스 개수를 세는 역할을 해요.
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".
이 예제는 library 배열의 모든 항목을 순회해요. 각 반복마다 for-in 루프가 item 상수에 배열의 다음 MediaItem을 할당하죠.
item is Movie는 현재 MediaItem이 Movie 인스턴스면 true를, 아니면 false를 반환해요. 마찬가지로 item is Song은 그 항목이 Song 인스턴스인지 확인하죠. for-in 루프가 끝나면 movieCount와 songCount에는 각 타입으로 발견된 MediaItem 인스턴스의 개수가 담겨요.
다운캐스팅
특정 클래스 타입의 상수나 변수가 뒤에서는 실제로 서브클래스의 인스턴스를 가리킬 수 있어요. 이 경우라고 생각된다면, 타입 캐스트 연산자(as? 또는 as!)로 서브클래스 타입으로 다운캐스트를 시도할 수 있어요.
다운캐스팅은 실패할 수 있기 때문에 타입 캐스트 연산자는 두 가지 형태가 있어요. 조건부 형태인 as?는 다운캐스트하려는 타입의 옵셔널 값을 반환해요. 강제 형태인 as!는 다운캐스트를 시도하고 결과를 한 번에 강제로 언래핑하죠.
다운캐스트가 성공할지 확실하지 않을 때는 조건부 형태의 타입 캐스트 연산자(as?)를 사용해요. 이 형태의 연산자는 항상 옵셔널 값을 반환하고, 다운캐스트가 불가능하면 그 값은 nil이 돼요. 이 덕분에 다운캐스트가 성공했는지 확인할 수 있죠.
강제 형태의 타입 캐스트 연산자(as!)는 다운캐스트가 항상 성공할 거라고 확신할 때만 사용해요. 이 형태의 연산자는 잘못된 클래스 타입으로 다운캐스트하려 하면 런타임 오류를 일으키죠.
아래 예제는 library의 각 MediaItem을 순회하면서, 각 항목에 알맞은 설명을 출력해요. 그렇게 하려면 각 항목을 단순한 MediaItem이 아니라 진짜 Movie나 Song으로 접근해야 해요. 그래야 설명에 쓸 Movie나 Song의 director나 artist 프로퍼티에 접근할 수 있거든요.
이 예제에서 배열의 각 항목은 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
이 예제는 현재 item을 Movie로 다운캐스트하려고 시도하면서 시작해요. item은 MediaItem 인스턴스이므로 Movie일 수도 있고, 마찬가지로 Song일 수도 있으며, 그냥 기반 MediaItem일 수도 있어요. 이 불확실성 때문에 as? 형태의 타입 캐스트 연산자는 서브클래스 타입으로 다운캐스트를 시도할 때 옵셔널 값을 반환해요. item as? Movie의 결과 타입은 Movie?, 즉 "옵셔널 Movie"예요.
Movie로의 다운캐스트는 library 배열의 Song 인스턴스에 적용하면 실패해요. 이에 대처하기 위해 위 예제는 옵셔널 바인딩을 사용해 옵셔널 Movie가 실제로 값을 담고 있는지 확인해요 (즉, 다운캐스트가 성공했는지 알아내죠). 이 옵셔널 바인딩은 "if let movie = item as? Movie"라고 쓰는데, 이렇게 읽을 수 있어요.
"item을 Movie로 접근해 봐요. 성공하면 반환된 옵셔널 Movie에 저장된 값을 movie라는 새 임시 상수에 할당해요."
다운캐스트가 성공하면 movie의 프로퍼티를 사용해 그 Movie 인스턴스의 설명을 출력해요. director의 이름을 포함해서요. 비슷한 원리로 Song 인스턴스를 확인하고, library에서 Song을 찾았을 때마다 artist 이름을 포함한 적절한 설명을 출력해요.
Note: 캐스팅은 실제로 인스턴스를 수정하거나 값을 바꾸지 않아요. 실제 인스턴스는 그대로이고, 캐스팅된 타입의 인스턴스로 단지 취급되고 접근될 뿐이에요.
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("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })
things 배열에는 Int 값 두 개, Double 값 두 개, String 값 하나, (Double, Double) 타입의 튜플, 영화 "Ghostbusters", 그리고 String 값을 받아 다른 String 값을 반환하는 클로저 표현식이 들어 있어요.
Any나 AnyObject 타입으로만 알려진 상수나 변수의 구체적인 타입을 알아내려면, switch 문의 case에서 is나 as 패턴을 사용할 수 있어요. 아래 예제는 things 배열의 항목들을 순회하면서 switch 문으로 각 항목의 타입을 조회해요. switch 문의 몇몇 case는 매칭된 값을 지정된 타입의 상수에 바인딩해 값을 출력할 수 있게 해요.
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
Note:
Any타입은 옵셔널 타입을 포함해 모든 타입의 값을 나타내요.Any타입이 기대되는 곳에 옵셔널 값을 사용하면 Swift가 경고를 줘요. 정말로 옵셔널 값을Any값으로 사용해야 한다면, 아래처럼as연산자로 옵셔널을Any로 명시적으로 캐스팅할 수 있어요.let optionalNumber: Int? = 3 things.append(optionalNumber) // Warning things.append(optionalNumber as Any) // No warning
더 알아보기
is연산자와as연산자는 이 문서에서 다루었지만, 프로토콜 준수 여부 확인은 doc:Protocols#Checking-for-Protocol-Conformance에서 자세히 설명해요.AnyObject는 클래스 전용 타입이라서, 클래스가 아닌 타입을 담을 땐Any를 사용해요.