서문
Swift에서는 "타입 캐스팅"이라고 표현하는 행위가 다른 언어들과 사뭇 차이가 있음에 유의하자
C언어에서 "타입 캐스팅"이라고 하면 String -> Int 처럼 원본을 유지하고 타입만 바꾸는걸 뜻하지만
Swift에서 String -> Int를 하면 Int 타입의 새 인스턴스를 생성함으로써 이루어지므로
이 행위를 타입 캐스팅이라고 부르지 않는다
여기선 "타입 캐스팅"이라고 하면 상속과 프로토콜 채택을 떠올려야 한다
[ Swift에서의 타입 캐스팅 ]
● is : 인스턴스의 타입을 확인 (예시: 해당 인스턴스가 Double 타입이 맞나?)
● as : 인스턴스의 타입을 superclass 혹은 subclass 타입으로 변경
● 특정 프로토콜을 따르는지 여부 확인
Defining a Class Hierarchy for Type Casting
타입 캐스팅이 사용되는 예시를 통해 이해해보자
상속 관계에 있는 3개의 class가 있고, Movie와 Song은 형제 관계이다
서로 다른 타입인 Movie와 Song의 인스턴스를 하나의 배열에 저장할 순 없을까?
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")
]
// the type of "library" is inferred to be [MediaItem]
놀랍게도 이런 구현이 가능하다
그럼 library 배열의 타입은 도대체 무엇일까. Moive? / Song?
-> 공동의 부모인 MediaItem 타입의 배열로 추론된다
Q. 공동의 부모로 추론한다면, 공동의 부모가 여러개라던가 상속관계가 복잡하면 어떻게 하느냐?
A. 다행(?)인지 Swift에선 1개 class만 상속할 수 있다
● library의 요소를 루프로 하나씩 꺼내면 각 요소들의 타입은 Movie/Song이 아닌 MediaItem이므로
1. 타입을 확인하고
2. 본래 타입으로 downcast 해야한다
Checking Type
● 상속 관계가 있는 타입의 instance는 부모 타입 중 하나로 upcast될 수 있다.
is 연산자를 사용하면 instance 본질 타입에 대해 알 수 있다
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"
● 본질이 Movie 타입인 인스턴스가 MediaItem으로 upcast된 상태이지만
is 연산자를 통해 본질이 Movie임을 알 수 있었다
● 그리고, is 연산자는 본질 타입과의 "일치"여부가 아닌 "포함"여부를 확인하는 것이다
만약 본질 타입이 Movie의 자식이어도 Movie를 포함하므로 true를 반환한다
Downcasting
위에서 언급한 것 처럼 상속관계에 있는 타입의 인스턴스는 부모 타입 중 하나로 upcast될 수 있는데
이를 원래 타입으로 되돌리거나 또 다른 부모 타입 중 하나로 바꾸는 방법이 있다
● as 연산자를 사용하면 된다.
본질 타입이 바꾸려는 타입의 자식이 아니면, 캐스팅이 실패할 수 있기 때문에
Optional을 반환하는 as? / as! 를 사용한다
(as? 와 as! 의 용도차이는 nil이 아님이 보장될 때만 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
● upcasting이든 downcasting이든 인스턴스에 대한 access를 바꾸는 것이지
인스턴스 본질을 수정하거나 바꾸지 않는다
Type Casting for Any and AnyObject
Swift에는 Any라는 타입이 있다
이름 그대로 아무거나 저장할 수 있다
● 클래스 인스턴스면 AnyObject / 그 외에는 전부 Any
뭔가 Swift스럽지 않고 과하게 편하다는 느낌이 나지 않는가?
맞다. 적절한 타입을 지정하는게 무조건 더 좋고, 꼭 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를 사용하면 다양한 타입을 하나의 배열로 만들 수도 있다
(심지어 클로저도)
● Any타입 배열의 요소를 꺼내려면 어떻게 해야 할까?
-> 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
● 이런 구현이 가능한 이유는 Any/AnyObject는 모든 타입/객체타입의 부모 타입이기 때문이다
"case 0 as Int" : 요소가 Int로 downcast 후 값이 0인지
"case let someInt as Int" : Int로 downcast 되는지
● 반대로, Any/AnyObject로 upcast도 가능하다
심지어 Optional도 가능하다.
let optionalNumber: Int? = 3
things.append(optionalNumber) // Warning
things.append(optionalNumber as Any) // No warning
warning이 뜨는 이유는 아마도...
Any에 Optional을 넣는게 불가능한건 아니지만,
구현자가 생각하기에 Any의 본질적인 의도와는 다르다고 판단한듯
'Swift > Language Guide' 카테고리의 다른 글
Control Flow - #1/2 (0) | 2021.09.12 |
---|---|
Functions - #1/1 (0) | 2021.09.12 |
Collection Types - #2/2 : Set, Dictionary (0) | 2021.09.10 |
Collection Types - #1/2 : Array (0) | 2021.09.09 |
Strings and Characters - #2/2 (0) | 2021.09.06 |
댓글