스위프트는 데이터 타입 안전을 위하여 각기 다른 타입끼리의 값 교환을 엄격히 제한한다. 또, 다른 프로그래밍 언어에서 대부분 지원하는 암시적 데이터 타입 변환은 지원하지 않는다.
19.1 기존 언어의 타입 변환과 스위프트의 타입 변환
먼저 [코드 19-1]에서 C언어와 스위프트의 데이터 타입 변환을 살펴보자.
/*
코드 19-1. C언어와 스위프트의 데이터 타입 변환 비교
*/
// C 언어
double value = 3.3
int convertedValue = (int)value
convertedValue = 5.5 // double -> int 암시적 데이터 타입 변환
// 스위프트
var value: Double = 3.3
var convertedValue: Int = Int(value)
convertedValue = 5.5 // 오류!!
C언어 코드를 살펴보면 부동소수 타입인 double에서 정수 타입인 int로 데이터 타입을 변경했다. 그러나 스위프트 코드를 보면 Int(value)라는 형태로 데이터 타입의 형태를 변경해주는데, 이는 이니셜라이저에 해당한다. 즉, 기존 값을 전달인자로 받는 이니셜라이저를 통해 새로운 Int 구조체의 인스턴스를 생성한다.
Int의 이니셜라이저는 대부분 실패하지 않는 이니셜라이저로 정의되어 있다. 하지만 StringPorotocol 타입을 매개 변수로 받는 실패 가능한 이니셜라이저가 있다. StringProtocol 타입의 데이터 text를 Int 타입으로 변경할 때, 적절하지 못한 매개변수가 전달된다면 새로운 인스턴스가 생성되지 않을 수 있다.
/*
코드 19-3. 실패 가능한 Int 이니셜라이저
*/
var stringValue: String = "123"
var integerValue: Int? = Int(stringValue)
print(integerValue) // Optional(123)
stringValue = "A123"
integerValue = Int(stringValue)
print(integerValue) // nil == Optional.none
19.2 스위프트 타입캐스팅
스위프트에서는 다른 언어틔 타입 변환 혹은 타입캐스팅을 이니셜라이저로 단순화했다. 하지만 스위프트에도 타입캐스팅은 있으며 대신 조금 다른 의미로 사용한다. 스위프트의 타입캐스팅은 인스턴스의 타입을 확인하거나 자신을 다른 타입의 인스턴스인양 행세할 수 있는 방법으로 사용할 수 있다. is와 as 연산자로 구현되어있다. 이 두 연산자로 값의 타입을 확인하거나 다른 타입으로 전환(Cast)할 수 있다. 또한 타입캐스팅을 통해 프로토콜을 준수하는지도 확인해볼 수 있다.
스위프트의 타입캐스팅은 실제로 참조 타입에서 주로 사용된다.
/*
코드 19-4. Coffee 클래스와 Coffee 클래스를 상속받은 Latte와 Americano 클래스
*/
class Coffee {
let name: String
let shot: Int
var description: String {
return "\(shot) shot(s) \(name)"
}
init(shot: Int) {
self.shot = shot
self.name = "coffee"
}
}
class Latte: Coffee {
var flavor: String
override var description: String {
return "\(shot) shot(s) \(flavor) latte"
}
init(flavor: String, shot: Int) {
self.flavor = flavor
super.init(shot: shot)
}
}
class Americano: Coffee {
let iced: Bool
override var description: String {
return "\(shot) shot(s) \(iced ? "iced" : "hot") americano"
}
init(shot: Int, iced: Bool) {
self.iced = iced
super.init(shot: shot)
}
}
위의 코드에서 Coffee는 Latte나 Americano인 척할 수 없지만, Latte나 Americano는 Coffee인 척할 수 있다. 왜냐하면 Latte나 Americano는 Coffee가 갖는 특성을 모두 갖기 때문이다. 이를 이해하는 것이 스위프트의 타입캐스팅을 이해하고 활용하는 시작점이다.
19.3 데이터 타입 확인
타입 확인 연산자인 is를 사용하여 인스턴스가 어떤 클래스(혹은 어떤 클래스의 자식클래스)의 인스턴스인지 타입을 확인해볼 수 있다. 인스턴스가 해당 클래스의 인스턴스거나 그 자식클래스의 인스턴스라면 true를, 그렇지 않다면 false를 반환한다. is 연산자는 클래스의 인스턴스뿐만 아니라 모든 데이터 타입에서 사용할 수 있다.
/*
코드 19-5. 데이터 타입 확인
*/
let coffee: Coffee = Coffee(shot: 1)
print(coffee.description) // 1 shot(s) coffee
let myCoffee: Americano = Americano(shot: 2, iced: false)
print(myCoffee.description) // 2 shot(s) hot americano
let yourCoffee: Latte = Latte(flavor: "green tea", shot: 3)
print(yourCoffee.description) // 3 shot(s) green tea latte
print(coffee is Coffee) // true
print(coffee is Americano) // false
print(coffee is Latte) // false
print(myCoffee is Coffee) // true
print(yourCoffee is Coffee) // true
print(myCoffee is Latte) // false
print(yourCoffee is Latte) // true
[코드 19-5]에서 보는 coffee 인스턴스는 Coffee 타입이다. 따라서 coffee는 Latte 타입이나 Americano 타입이 될 수 없다. 그러나 myCoffee는 Americano 타입이며, yourCoffee는 Latte 타입이므로 myCoffee와 yourCoffee는 Coffee 타입인지 확인했을 때 true를 반환받는다. 하지만 myCoffee와 yourCoffee는 서로 특성이 다르며 부모와 자식 클래스의 관계가 아니기 때문에 서로 다른 타입이다.
is 연산자 외에도 타입을 확인해볼 수 있는 방법이 있다. 메타 타입(Meta Type)타입을 이용하는 것이다.
메타 타입 타입은 타입의 타입을 뜻한다. 클래스 타입, 구조체 타입, 열거형 타입, 프로토콜 타입등의 타입의 타입이다. 즉, 타입 자체가 하나의 타입으로 또 표현할 수 있다는 것이다.
클래스, 구조체, 열거형의 이름은 타입의 이름이다. 그 타입의 이름 뒤에 .Type을 붙이면 이는 메타 타입을 나타낸다. 프로토콜 타입의 메타 타입은 .Protocol이라고 붙여주면 된다. 예를 들어 SomeClass라는 클래스의 메타 타입은 SomeClass.Type이라고 표현하며, SomeProtocol.Protocol이라고 표현한다.
또, self를 사용해서 타입을 값처럼 표현할 수 있다. 예를 들어 SomeClass.self라고 표현하면 SomeClass의 인스턴스가 아니라 SomeClass 타입을 값으로 표현한 값을 반환한다. 그리고 SomeProtocol.self라고 표현하면 SomeProtocol을 준수하는 타입의 인스턴스가 아니라 SomeProtocol 프로포콜을 값으로 표현한 값을 반환한다.
/*
19-6. 메타 타입
*/
protocol SomeProtocol { }
class SomeClass: SomeProtocol { }
let intType: Int.Type = Int.self
let stringType: String.Type = String.self
let classType: SomeClass.Type = SomeClass.self
let protocolProtocol: SomeProtocol.Protocol = SomeProtocol.self
var someType: Any.Type
someType = intType
print(someType) // Int
someType = stringType
print(someType) // String
someType = classType
print(someType) // SomeClass
someType = protocolProtocol
print(someType) // SomeProtocol
[코드 19-6]에 정의된 SomeProtocol, SomeClass 등의 메타 타입이 하나의 값으로 취급되어 someType 변수에 할당될 수 있음을 확인할 수 있다. 또 Int, String도 스위프트에서 구조체로 구현한 타입이므로 메타 타입을 값으로 취급해 할당될 수 있음을 확인할 수 있다.
인스턴스의 타입을 표현한 값을 알아보고자 한다면 type(of:) 함수를 사용한다. 그래서 type(of: someInstance).self라고 표현하면 someInstance의 타입을 값으로 표현한 값을 반환한다.
NOTE_인스턴스 self와 타입 self의 의미
.self 표현은 값 뒤에 써주면 그 값 자신을, 타입 이름 뒤에 써주면 타입을 표현하는 값을 반환한다. "stringValue".self는 "stringValue" 그 자체를, String.self는 String 타입을 나타내는 값이다.
/*
코드 19-7. type(of:) 함수와 .self 사용
*/
print(type(of: coffee) == Coffee.self) // true
print(type(of: coffee) == Americano.self) // false
print(type(of: coffee) == Latte.self) // false
print(type(of: coffee) == Americano.self) // false
print(type(of: myCoffee) == Americano.self) // true
print(type(of: yourCoffee) == Americano.self) // false
print(type(of: coffee) == Latte.self) // false
print(type(of: myCoffee) == Latte.self) // false
print(type(of: yourCoffee) == Latte.self) // true
19.4 다운캐스팅
어떤 클래스 타입의 변수 또는 상수가 정말로 해당 클래스의 인스턴스를 참조하지 않을 수도 있다. 예를 들어 Latte 클래스의 인스턴스가 Coffee 클래스의 인스턴스인양 Coffee 행세를 할 수 있다.
/*
코드 19-8. Latte 타입의 인스턴스를 참조하는 Coffee 타입 actingConstant 상수
*/
let actingConstant: Coffee = Latte(flavor: "vanilla", shot: 2)
print(actingConstant.description) // 2 shot(s) vanilla latte
[코드 19-8]의 actingConstant 상수는 Coffee 인스턴스를 참조하도록 선언했지만, 실제로는 Coffee 타입인 척 하는 Latte 타입의 인스턴스를 참조하고 있다. 이런 상황에서 actingConstant가 참조하는 인스턴스를 진짜 타입인 Latte 타입으로 사용해야 할 때가 있다. 가령 Latte 타입에 정의되어 있는 메서드를 사용하거나 프로퍼티에 접근해야 할 때 Latte 타입으로 변수의 타입을 변환해주어야 하는데 이를 다운캐스팅이라고 한다. 클래스의 상속 모식도에서 자식클래스보다 더 상위에 있는 부모클래스의 타입을 자식클래스의 타입으로 캐스팅한다고 해서 다운캐스팅이라고 부르는 것이다.
타입캐스트 연산자에는 as?와 as! 두 가지가 있다. 타입캐스트 연산자를 사용하여 자식클래스 타입으로 다운캐스팅할 수 있다.
다운캐스팅을 시도해보는 조건부 연산자인 as? 연산자는 다운캐스팅이 실패했을 경우 nil을 반환하고 다운캐스팅을 강제하는 as! 연산자는 다운캐스팅에 실패할 경우 런타임 오류가 발생한다. 따라서 as? 연산자는 반환 타입이 옵셔널이며, as! 연산자의 반환 타입은 옵셔널이 아니다.
as?를 사용하면 다운캐스팅에 성공할 경우 옵셔널 타입으로 인스턴스를 반환하며, 실패할 경우 nil을 반환한다.
as!를 사용하면 다운캐스팅에 성공할 경우 옵셔널이 아닌 인스턴스가 반환되며, 실패할 경우 런타임 오류가 발생한다.
/*
코드 19-9. 다운캐스팅
*/
if let actingOne: Americano = coffee as? Americano {
print("This is Americano")
} else {
print(coffee.description)
}
// 1 shot(s) coffee
if let actingOne: Latte = coffee as? Latte {
print("This is Latte")
} else {
print(coffee.description)
}
// 1 shot(s) coffee
if let actingOne: Coffee = coffee as? Coffee {
print("This is Just Coffee")
} else {
print(coffee.description)
}
// This is Just Coffee
if let actingOne: Americano = myCoffee as? Americano {
print("This is Americano")
} else {
print(coffee.description)
}
// This is Americano
if let actingOne: Latte = myCoffee as? Latte {
print("This is Latte")
} else {
print(coffee.description)
}
// 1 shot(s) coffee
if let actingOne: Coffee = myCoffee as? Coffee {
print("This is Just Coffee")
} else {
print(coffee.description)
}
// This is Just Coffee
// Success
let castedCoffee: Coffee = yourCoffee as! Coffee
// 런타임 오류!!! 강제 다운캐스팅 실패!
let castedAmericano: Americano = coffee as! Americano
컴파일러가 다운캐스팅을 확신할 수 있는 경우에는 as를 사용할 수도 있다. 항상 성공하는 것을 아는 경우는 캐스팅하려는 타입이 같은 타입이거나 부모클래스 타입이라는 것을 알 때이다.
/*
코드 19-10. 항상 성공하는 다운캐스팅
*/
let castedCoffee: Coffee = yourCoffee as Coffee
NOTE_타입캐스팅의 의미
캐스팅은 실제로 인스턴스를 수정하거나 값을 변경하는 작업이 아니다. 인스턴스는 메모리에는 똑같이 남아있을 뿐이다. 다만 인스턴스를 사용할 때 어떤 타입으로 다루고 어떤 타입으로 접근해야 할지 판단할 수 있도록 컴퓨터에 힌트를 주는 것뿐이다.
19.5 Any, AnyObject의 타입캐스팅
Any는 함수 타입을 포함한 모든 타입을 뜻하고, AnyObject는 클래스 타입만을 뜻한다.
Any 또는 AnyObject가 사용되는 프레임워크의 API를 보면 어떤 타입의 데이터라도 전달할 수 있다는 의미로 해석할 수 있다. 그런데 문제는 반환되는 타입도 Any나 AnyObject라면 전달받은 데이터가 어떤 타입인지 확인하고 사용해야 한다. 스위프트는 암시적 타입 변환을 허용하지 않기 때문이다.
/*
코드 19-11. AnyObject의 타입 확인
*/
func checkType(of item: AnyObject) {
if item is Latte {
print("item is Latte")
} else if item is Americano {
print("item is Americano")
} else if item is Coffee {
print("item is Coffee")
} else {
print("Unknown Type")
}
}
checkType(of: coffee) // item is Coffee
checkType(of: myCoffee) // item is Americano
checkType(of: yourCoffee) // item is Latte
[코드 19-11]에서는 AnyObject를 타입으로 명시한 item 매개변수가 있는 checkType(of:) 함수에 인스턴스를 전달인자로 호출해보았다. 함수 안에서 is 연산자를 사용하여 해당 인스턴스가 어떤 인스턴스인지만 확인해 볼 수 있었다.
[코드 19-12]의 방법을 사용하면 item이 어떤 타입인지 판단하는 동시에 실질적으로 해당 타입의 인스턴스로 사용할 수 있도록 캐스팅할 수 있다.
/*
코드 19-12. AnyObject의 타입캐스팅
*/
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("Unknown Type")
}
}
castTypeToAppropriate(item: coffee) // 1 shot(s) coffee
castTypeToAppropriate(item: myCoffee) // 2 shot(s) hot americano
castTypeToAppropriate(item: yourCoffee) // 3 shot(s) green tea latte
클래스의 인스턴스만 취할 수 있었던 AnyObject에 비해 Any는 모든 타입의 인스턴스를 취할 수 있다. Any는 함수, 구조체, 클래스, 열거형 등 모든 타입의 인스턴스를 의미할 수 있다.
TIP_ 옵셔널과 Any
Any 타입은 모든 값 타입을 표현한다. 더불어 옵셔널 타입도 표현할 수 있다. 하지만 Any 타입의 값에 옵셔 널 타입의 값이 들어온다면 스위프트 컴파일러는 경고를 한다. 의도적으로 옵셔널 값을 Any 타입의 값으로 사용하고자 한다면 as 연산자를 사용하여 명시적 타입 캐스팅을 해주면 경고 메시지를 받지 않는다.
흔한 예로는 print() 함수를 생각해볼 수 있다.
let optionalValue: Int? = 100
print(optionalValue) // 컴파일러 경고 발생
print(optionalValue as Any) // 경고 없음
'iOS > SWIFT' 카테고리의 다른 글
[Swift] Chapter 21. 익스텐션 (0) | 2021.10.11 |
---|---|
[Swift] Chapter 20. 프로토콜 (0) | 2021.10.11 |
[Swift] Chapter 18. 상속 (0) | 2021.10.10 |
[Swift] Chapter 17. 서브스크립트 (0) | 2021.10.10 |
[Swift] Chapter 16. 모나드 (0) | 2021.10.10 |
댓글