타입 캐스팅
타입캐스팅이란 인스턴스 타입을 확인하거나, 인스턴스의 타입을 슈퍼클래스 또는 서브클래스 타입처럼 다루기 위해 사용한다.
- Swift에서 타입 캐스팅은
is
와as
연산자를 사용하여 구현함 - 이 두 연산자는 값의 타입을 확인하거나, 값을 다른 타입으로 전환(
Cast
)하는 간단하고 표현적인 방법을 제공함 - 타입 캐스팅을 통해 특정 클래스 인스턴스의 타입을 확인 가능
- 타입 캐스팅을 통해 프로토콜을 준수하는지도 확인 가능
먼저 알아볼것은 '타입 캐스팅을 사용하여 특정 클래스 인스턴스의 타입을 확인 가능' 부분이다.
인스턴스의 타입은 is
키워드를 사용해서 확인이 가능하다.
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var seogun = Person(name: "서근")
if seogun is Person {
print(true) //true
}
Person
이라는 클래스를 정의하고 초깃값이 없기 때문에 초기화를 해줬다. 그리고 is
키워드를 통해 'segoun
'이 'Person
' 타입이 맞는지 확인한다.
이런 식으로 인스턴스 자체를 확인할 수 도 있지만, 인스턴스의 프로퍼티도 확인 가능하다.
var seogun = Person(name: "서근")
if seogun.name is String {
print(true)
}
is
타입 확인 키워드는 학상 true
이여야만 한다는 걸 기억해야 한다!
Swift 타입 캐스팅은 실제로 참조 타입에 주로 사용된다. 아래 예제를 통해 확인해 보자면
class Coffee {
let name: String
let shot: Int
var description: String {
return "\(shot)샷 \(name)"
}
init(shot: Int) {
self.shot = shot
self.name = "커피"
}
}
//Coffee 클래스를 상속받은 Lattee 클래스
class Latte: Coffee {
var flavor: String
//부모클래스의 description 재정의
override var description: String {
return "\(shot)샷 \(flavor) 라떼"
}
init(flavor: String, shot: Int) {
self.flavor = flavor
super.init(shot: shot)
}
}
//슈퍼클래스인 Coffee를 상속받은 Americano 클래스
class Americano: Coffee {
let iced: Bool
//마찬가지로 desctiption 재정의.
override var description: String {
//삼항연산자
return "\(shot)샷 \(iced ? "아이스" : "핫") 아메리카노"
}
init(shot: Int, iced: Bool) {
self.iced = iced
super.init(shot: shot)
}
}
Latte
와 Americano
클래스는 Coffee
클래스를 상속받았고, 슈퍼클래스의 특성들을 모두 포함하여 받은 것을 확인할 수 있다.
즉, Latte
와 Americano
클래스는 Coffee
인척 할 수 있지만, Coffee
는 그렇게 하지 못한다.
이때 타입 캐스팅을 활용할 수 있다.
데이터 타입 확인 - is
타입 확인 연산자 is
를 사용하여 타입을 확인해보자!
let coffee: Coffee = Coffee(shot: 3)
print(coffee.description) //3 샷 커피
let myLatte: Latte = Latte(flavor: "바닐라", shot: 2)
print(myLatte.description) //2샷 바닐라 라떼
let yourAmericano: Americano = Americano(shot: 2, iced: true)
print(yourAmericano.description) //2샷 아이스 아메리카노
print(coffee is Coffee) //true
print(coffee is Latte) //false
print(coffee is Americano) //false
print(myLatte is Coffee) //true
print(yourAmericano is Coffee) //true
print(myLatte is Americano) //false
print(yourAmericano is Americano) //true
코드를 보면 coffee
타입이 Coffee
타입이죠? print(coffee is Coffee)
는 true
지만, print(coffee is Lattee)
는 false
이다. 즉 Coffee
클래스는 Latte
나 Americano
클래스 타입이 될 수 없다는 뜻이다.
반대로 myLatte
나 yourAmericano
는 각각 Coffee
클래스를 상속받은 클래스들 이기 때문에 Coffee
타입 인지 확인했을 때 true
를 반환받았다!
메타 타입 타입
is
연산자 이외에도 타입을 확인해 볼 수 있는 방법은 메타 타입(Meta Type
) 타입을 사용하는 것이다.
메타 타입 타입 이란 타입의 타입이라는 뜻!
클래스 / 구조체 / 열거형 / 프로토콜 등의 타입의 타입! 즉, 타입 자체가 하나의 타입으로 또 표현할 수 있다는 의미
무슨 소리인가 하면....
클래스 / 구조체 / 열거형의 이름은 타입의 이름인데, 그래서 클래스는 참조 타입, 구조체/열거형은 값 타입이라고 하는 것! 그 타입의 이름 뒤에 .Type
을 붙이면 이것은 메타 타입을 나타냄!
프로토콜 타입 메타 타입은 .Protocol
키워드를 사용한다.
예를 들어보자면, SomeClass
클래스의 메타 타입은 SomeClass.Type
이다. SomeProtoco
l의 메타 타입은 SomeProtocol.Protocol
또, self
를 사용하여 타입을 값처럼 표현도 가능하다.
예를 들어 SomeClass.self
는 SomeClass
의 인스턴스가 아닌 SomeClass
타입을 값으로 표현한 값을 반환한다. SomeProtocol.self
이면 SomeProtocol
프로토콜을 값으로 표현한 값을 반환한다는 의미!
protocol SomeProtocol { }
class SomeClass: SomeProtocol { }
let stringType: String.Type = String.self //String.Type
let intType: Int.Type = Int.self //Int.Type
let classType: SomeClass.Type = SomeClass.self //SomeClass.Type
let protocolType: SomeProtocol.Protocol = SomeProtocol.self //SomeProtocol.Protocol
var someType: Any.Type
someType = stringType //String.Type
print(someType) //String
someType = classType
print(someType) //someClass
인스턴스 self와 타입 self의 의미
.self
표현은 값 뒤에 사용하면 그 값 자신을 의미, 타입 이름 뒤에 쓰면 타입을 표현하는 값을 반환한다."StringValue".self
는 "StringValue"
그 자체를 String.self
는 String
타입을 나타내는 값이다.
프로그램 실행 중에 인스턴스 타입을 표현한 값을 알아보려면 type(of:)
함수를 하면 된다.
만약, type(of: someInstance).self
라고 정의하면 someInstance
의 타입을 값으로 표현한 값을 반환하게 된다.
print(type(of: coffee) == Coffee.self) //true
print(type(of: coffee) == Latte.self) //false
print(type(of: coffee) == Americano.self) //false
print(type(of: myLatte) == Latte.self) //true
다운캐스팅으로 넘어가기 전에 타입캐스팅 Apple 예제를 보고 넘어가 보자!
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
// MediaItem(슈퍼)클래스를 상속받은 Movie
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
// MediaItem(슈퍼)클래스를 상속받은 Song
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
let library = [
Movie(name: "도둑들", director: "최동훈"),
Song(name: "light Switch", artist: "찰리 푸스"),
Movie(name: "이터널스", director: "클로이 자오"),
Song(name: "다이너마이트", artist: "방탄소년단"),
Song(name: "Rumor", artist: "Lizzo")
]
배열을 하나 만들었는데 이 안에는 MediaItem
을 상속받는 Movie
와 Song
클래스가 섞여 있다. 이렇게 library
배열은 MediaItem
타입의 배열을 갖게 된다.
이제 is
타입 캐스팅을 사용해보자
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는 \(movieCount)개의 영화가 있고, \(songCount)개의 노래가 있습니다")
// Media library는 2개의 영화가 있고, 3개의 노래가 있습니다s"
library
배열을 돌며 해당 인스턴스가 Movie
타입 인지, Song
타입 인지 is
키워드를 통해 확인하고 있다!
다운 캐스팅
특정 클래스 타입의 상수 또는 변수는 하위 클래스의 인스턴스를 참조할 수 있다. 이 경우, 타입 캐스트 연산자 as?
또는 as!
를 사용하여 서브 클래스 타입으로 downcasting
을 시도할 수 있다.
다운 캐스팅은 실패할 여지가 충분히 있기 때문에 타입 캐스트 연산자는 두 가지 형태로 제공된다.
as?
- 조건부 형식인
as?
는 다운 캐스팅하려는 타입의 옵셔널 값을 반환한다. - 캐스팅에 실패할 경우
nil
을 반환한다. - 캐스팅에 성공하면 옵셔널 타입으로 인스턴스를 반환한다.
- 실패할 가능성이 있으면 조건부 연산자인
as?
를 사용 - 런타임에 실행된다.
as!
- 강제 형식인
as!
는 강제 언래핑을 하여 값을 반환한다. - 캐스팅에 실패할 경우 런타임 오류가 발생한다.
- 캐스팅에 성공할 경우 옵셔널이 아닌 인스턴스를 반환한다.
- 항상 성공할 것이라는 확신이 있으면 강제 형식인
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)")
}
}
/*
영화: 도둑들, 감독: 최동훈
노래: light Switch, 가수: 찰리 푸스
영화: 이터널스, 감독: 클로이 자오
노래: 다이너마이트, 가수: 방탄소년단
노래: Rumor, 가수: Lizzo
*/
if let
구문을 활용해 as?
조건부 형식 다운 캐스팅을 했다. library
는 Mediaitem
타입 배열이기 때문에, 그 안에 있는 인스턴스는 MediaItem
의 서브클래스인 Movie
와 Song
이기 때문이다.
즉! MediaItem
은 Super
클래스 / Movie
와 Song
은 MediaItem
의 자식클래스 이기 때문에 다운 캐스팅이 당연함(as
)!!
for
문에서 item
은 MediaItem
인스턴스 이니까 Movie
가 될 수도 Song
이 될 수 도 있다.
또, 조건부 형식 as?
는 옵셔널을 반환하니까, if let
구문으로 꺼내왔다.
위에서 했던 Coffee
예제를 다시 사용해서 다운 캐스팅을 해보자!
if let actingOne: Americano = coffee as? Americano {
print("이것은 아메라카노 입니다.")
} else {
print(coffee.description)
}
//coffee는 Americano가 아니기 때문에 print(coffee.description) 구문 출력
//3샷 커피
if let actingOne: Latte = myLatte as? Latte {
print("이것은 라떼 입니다.")
} else {
print(coffee.description)
}
//이것은 라떼 입니다.
if let actingOne: Americano = yourAmericano as? Americano {
print("이것은 아메리카노 입니다")
} else {
print(coffee.description)
}
//이것은 아메리카노 입니다.
if let actingOne: Latte = yourAmericano as? Latte {
print("이것은 라떼 입니다.")
} else {
print(yourAmericano.description)
}
//2샷 아이스 아메리카노
한 가지만 놓고 보면 if let actingOne: Americano = cofee as? Americano
는 "만약 coffee
가 참조하는 인스턴스가 Americano
타입의 인스턴스 라면 actingOne
이라는 임시 상수에 할당하라"이다.
하지만 coffee
는 Coffee
클래스 이기 때문에 Coffee
클래스는 Americano
클래스를 포함하지 않는다. 그렇기에 다운 캐스팅에 실패하여 else
구문의 print(coffee.description)
이 출력된다!
as
- 컴파일 시점에 타입 캐스팅(
upcasting
)을 하며, 실패할 경우 에러가 발생한다. - 팬턴 매칭(
switch
)에서도 사용한다. - 컴파일러가 다운캐스팅을 확신할 경우
as?
/as!
대신as
를 사용한다. - 캐스팅하려는 타입이 같은 타입 이거나 부모클래스 타입이라는 것을 알 때
as
연사자를 사용
//항상 성공한다는 것을 컴파일러도 알고 있다.
let castedCoffee: Coffee = myAmericano as Coffee
Any, AnyObject 타입 캐스팅
Swift에는 특정 타입을 지정하지 않고 여러 타입의 값을 할당할 수 있는 Any
와 AnyObject
타입이 존재한다.
Any - 함수(function
) 타입을 포함한 모든 타입의 인스턴스
AnyObject - 모든 클래스 타입의 인스턴스
Any / AnyObject 사용 주의사항
Any
와 AnyObject
를 사용하면 예상하지 못한 오류가 발생할 확률이 높아지므로 되도록 사용을 지양하는 것을 권장한다. 코드에서 처리할 타입은 항상 구체적으로 기술하는 것이 좋다.
//클래스의 인스턴스만 수용할 수 있는 AnyObject 타입
func checkType(of item: AnyObject) {
if item is Latte {
print("라떼 입니다.")
} else if item is Americano {
print("아메리카노 입니다.")
} else if item is Coffee {
print("커피 입니다.")
} else {
print("Unknown 타입")
}
}
checkType(of: coffee) //커피 입니다.
checkType(of: myLatte) //라떼 입니다.
checkType(of: yourAmericano) //아메리카노 입니다.
위 코드에서는 is
연산자를 사용하여 해당 인스턴스가 어떤 타입의 인스턴스 인지만 확인해 보았다.
이제 item
이 어떤 타입인지 판단하는 동시에 해당 타입의 인스턴스로 사용할 수 있도록 캐스팅할 수 있다.
func castTypeToAppropriate(item: AnyObject) {
if let castedItem: Latte = item as? Latte {
print(castedItem.description)
} else if let castedItem: Americano = item as? Americano {
print(castedItem.description)
} else if let castedItem: Coffee = item as? Coffee {
print(castedItem.description)
} else {
print("Unknwon 타입")
}
}
castTypeToAppropriate(item: coffee) //3샷 커피
castTypeToAppropriate(item: myLatte) //2샷 바닐라 라떼
castTypeToAppropriate(item: yourAmericano) //2샷 아이스 아메리카노
AnyObject
는 클래스의 인스턴스만 취할 수 있었지만, Any
는 모든 타입의 인스턴스를 취할 수 있다.
Any
는 함수, 구조체, 클래스, 열거형 모든 타입의 인스턴스를 의미할 수 있다.
func checkAnyType(of item: Any) {
switch item {
case 0 as Int:
print("zero as an Int")
case 0 as Double:
print("zero as an Double")
case let someInt as Int:
print("an integer value of \(someInt)")
case let (x, y) as (Double, Double):
print("an (x, y) point at \(x), \(y)")
case let stringConverter as (String) -> String:
print(stringConverter("서근"))
default:
print("something else : \(type(of: item))")
}
}
checkAnyType(of: 0) //zero as an Int
checkAnyType(of: 0.0) //zero as an Double
checkAnyType(of: 30) //an integer value of 30
checkAnyType(of: (2.0, 5.1)) //an (x, y) point at 2.0, 5.1
//클로저 사용
checkAnyType(of: { (name: String) -> String in "\(name) 개발 블로그 입니다!"} )
//서근 개발 블로그 입니다!
as와 as? as! 차이점
as
는 컴파일 타임에, as?
와 as!
는 런타임에 실행된다. as
는 업캐스팅(항상 성공하는)과 패턴 매칭(switch
)에서만 사용 가능하다.
옵셔널과 Any
Any
타입은 모든 값 타입을 표현하는데, 옵셔널 타입도 표현 가능하다. 하지만 Any
타입의 값이 들어와야 할 자리에 옵셔널 타입의 값이 위치한다면 Swift는 컴파일러 경고를 한다. 의도적으로 옵셔널 값을 Any
타입으로 사용하려면 as
업캐스팅을 사용하여 명시적 타입 캐스팅을 해주면 경고 메시지를 받지 않는다.
let optionalValue: Int? = 100
print(optionalValue) // 컴파일러 경고
print(optionalValue as Any) // 경고 없음
읽어주셔서 감사합니다 🤟
'SWIFT > Grammar' 카테고리의 다른 글
Swift : 기초문법 [프로토콜#2 - 메서드 요구사항] (0) | 2022.01.27 |
---|---|
Swift : 기초문법 [프로토콜#1 - 프로퍼티 요구사항] (1) | 2022.01.25 |
Swift : 기초문법 [상속#3 - 클래스의 이니셜라이저 convenience, required] (0) | 2022.01.24 |
Swift : 기초문법 [상속#2 재정의 override] (0) | 2022.01.23 |
Swift : 기초문법 [상속#1 - 클래스 상속(자식, 부모, 기반 클래스)] (0) | 2022.01.22 |