형 변환 (Type Casting)
Swift에서 타입 캐스팅은 is
및 as
연산자로 나눠줄 수 있습니다.
is
연산자를 통하여 인스턴스에 대한 타입을 확인을 할 수가 있고
as
연산자는 클래스의 계층구조(Superclass나 Subclass)에서 변환할 타입의 인스턴스로 리턴합니다.
또한, 타입 캐스팅을 이용하여 특정 프로토콜을 따르는지 확인할 수도 있습니다.
is 연산자 - Type Checking
간단히 영어 표현식으로 생각해 보면 "is"는 "~은 ~이다"라고 사용이 되죠?
영어를 읽는다 생각하고 아래 예제를 읽어보면 사용방법이 확 이해가 가실 거예요.
var score: Int = 100
score is Int
"score" 는 "Int" 타입이다.
// true
score is Double
"score" 는 "Double" 타입이다.
// false
is
연산자는 Bool
타입으로 리턴합니다.
만약 인스턴스가 동일한 타입이거나 동일한 타입에 대하여 Subclass 유형이면
리턴 값으로 true
를 리턴하고 틀리면 false
를 리턴합니다.
음... 동일한 타입에 대하여 Subclass?? 이게 무슨 말이냐 하면... 예제를 보면서 알아봅시다.
아래와 같이 Developer
에 Person
을 상속받아 Developer
타입으로 danny
라는 객체(인스턴스)를 생성해 줬습니다.
class Person {}
class Developer: Person {}
let danny = Developer()
danny is Developer
danny is Person
// true
// true
is
로 타입 검사를 해보면 danny
는 Developer
타입이므로 당현히 true
로 리턴하겠죠?
그러면 Person
은 어떻게 나오는지 볼까요?
Person
에 대한 타입 검사를 해보면 true
로 리턴하게 되네요.
이유는 Developer
는 Person
의 자식 클래스(Subclass)이고,
부모 클래스인 Person
의 모든 특성과 기능을 상속받아 만들어진 타입입니다.
이미 자식 클래스는 부모 클래스의 모든 기능을 갖고있어 타입이 같다고 취급해주는 거죠!!!
간단한 예제를 통해 어떤 식으로 사용할 수 있는지 알아봅시다.
class Person {
let name: String
init(name: String) {
self.name = name
}
}
class Developer: Person {}
class Doctor: Person {}
let people: [Person] = [
Developer(name: "Danny"),
Developer(name: "Jobs"),
Doctor(name: "김사부")
]
Person
을 상속받아서 "개발자 Danny
, Jobs
"와 "의사 김사부
"를 만들어 줬습니다.
if문
이나 switch문
으로 분기처리를 통해 이와 같이 사용할 수 있습니다.
var developerCount = 0
for person in people {
if person is Developer {
print("개발자 : \(person.name)")
developerCount += 1
} else if person is Doctor {
print("닥터 : \(person.name)")
}
}
print("개발자의 수 : \(developerCount)명")
// 개발자 : Danny
// 개발자 : Jobs
// 닥터 : 김사부
// 개발자의 수 : 2명
for문
으로 각각의 타입이 true
일 경우 그 해당하는 이름을 프린트하게 하고
또한 Developer
가 true
일 경우 developerCount
의 숫자를 1씩 증가시키도록 만들었습니다.
var developerCount = 0
for person in people {
switch person {
case is Developer:
print("개발자 : \(person.name)")
developerCount += 1
case is Doctor:
print("닥터 : \(person.name)")
default:
break
}
}
print("개발자의 수 : \(developerCount)명")
// 개발자 : Danny
// 개발자 : Jobs
// 닥터 : 김사부
// 개발자의 수 : 2명
as 연산자 (UpCasting / DownCasting)
as
연산자도 그림으로 이해해 보면 외우기가 편해집니다.
상속 관계에서
'자식 클래스' --> '부모 클래스'로 형 변환 시켜 줄 때는 Upcating (위로 올라가니까 up)
'부모 클래스' --> '자식 클래스'로 형 변환을 할 때는 Downcasting (아래로 내려가니까 down)
Casting은 메모리의 값을 수정하여 타입을 변경하는 게 아니라 단순히 캐스팅된 타입의 인스턴스로 사용한다고 하네요.
(Swift는 타입 취급에 대해 관대하지 않기 때문에 단순히 해당 타입인 것처럼 취급하려는 목적인 것 같습니다.)
Upcasting (as)
일단 Upcasting은 무조건 성공합니다.
예제를 보면서 알아봅시다.
class Person {
let name = "Danny"
let age = 20
}
class Developer: Person {
let skill = "Swift"
}
메모리 구조에 대해서 조금 이해하고 넘어가면 좋은데,
한 개의 저장 속성마다 하나의 메모리 공간을 갖는다고 가정해 볼게요.
그러면 Person
의 인스턴스에 대해서는 name
, age
이렇게 2개의 메모리 공간이 생성되겠죠?
let person = Person()
person.name // "Danny"
person.age // 20
그리고, Developer
의 인스턴스는 Person
상속을 받았으므로
name
, age
및 skill
이렇게 3개의 메모리 공간이 생기게 되죠.
let danny = Developer()
danny.name // "Danny"
danny.age // 20
danny.skill // "Swift"
위에서 Upcasting은 '자식 클래스' --> '부모 클래스'로 형 변환을 시켜 준다고 말씀드렸어요.
자 간단히 생각해 봅시다. 3개의 메모리 공간에서 2개의 메모리 공간으로 변경되는 것은 크게 문제가 되지 않아요.
상속 관계에서 '자식 클래스'는' 부모 클래스'의 멤버를 당연히 포함하고 있고
또한, 메모리 공간이 없던 게 생기는 게 아니고 '자식 클래스'의 skill
속성만 빼주면
Person
타입으로 사용이 가능하므로 Upcasting이 되는 거죠!
바로 Developer
를 Person
으로 Upcasting을 해봅시다.
let castingDanny = danny as Person
castingDanny.name // "Danny"
castingDanny.age // 20
castingDanny.skill // Error: Value of type 'Person' has no member 'skill'
참고. castingDanny
는 Person
타입이므로 Developer
의 속성 skill
은 접근이 불가능합니다.
아래와 같이 직접적으로 as
형변환 없이 타입을 선언만 해줘도 자동으로 캐스팅이 됩니다.
let castingDanny: Person = danny
castingDanny.name // "Danny"
castingDanny.age // 20
바로 Downcasting으로 넘어가겠습니다. 위의 메모리 개수를 생각하고 읽어주세요.
Downcasting (as? / as!)
Downcasting은 '부모 클래스' --> '자식 클래스'로 형 변환 해주는 겁니다.
Downcasting은 메모리 공간을 생각하면서 보면 '부모 클래스' 2개에서 '자식 클래스' 3개로 변경을 시켜줘야 되죠.
메모리 공간을 2개에서 3개로 변경을 시켜 줘야 하는데
이렇게 변환할 때는 새로운 메모리가 있을 수도 있고 없을 수도 있으므로 (실패가 가능하므로)
옵셔널을 "as" 뒤에다 붙여 주는 거예요. (as?
)
위랑 같은 예제를 갖고 왔습니다.
class Person {
let name = "Danny"
let age = 20
}
class Developer: Person {
let skill = "Swift"
}
let danny = Developer()
let castingDanny: Person = danny
castingDanny.name // "Danny"
castingDanny.age // 20
castingDanny.skill // Error: Value of type 'Person' has no member 'skill'
이와 같이 Person
타입의 castingDanny
가 있습니다.
Person
타입이므로 Developer
의 속성인 skill
로 접근이 불가능하죠.
이걸 사용하기 위해선 Person
(부모) -> Developer
(자식) 타입으로 Downcasting을 해줘야 됩니다.
let castingDeveloper = castingDanny as? Developer
as?
가 옵셔널이므로 현재 타입도 Developer?
가 됩니다.
사용 시 옵셔널 바인딩을 통해 풀어서 사용해 주면 됩니다.
if let castingDeveloper = castingDanny as? Developer {
castingDeveloper.name // "Danny"
castingDeveloper.age // 20
castingDeveloper.skill // "Swift"
}
만약, 부모 인스턴스가 자식 인스턴스를 갖고 있을 거라는 확신이 있을 때만
as!
로 강제 옵셔널 언레핑을 사용하도록 하자.
let castingDeveloper = castingDanny as! Developer
castingDeveloper.name // "Danny"
castingDeveloper.age // 20
castingDeveloper.skill // "Swift"
될 수 있으면 옵셔널 바인딩으로 풀어주는 게 안전합니다.
as? = 안전한 방법. 실패 시 nil을 리턴함
as! = 확신이 있을 때만 사용. 실패 시 에러를 발생
참고. 스위프트에서는 내부적으로 여전히 Objective-C의 프레임워크를 사용하는 것이 많아 서로 상호 호환이 가능하도록 설계해 둠
ex) Sting
<--> NSString
Any와 AnyObject를 위한 타입 캐스팅
Any 타입
- 모든 타입을 표현할 수 있는 타입
- 기본 타입(
Int
,String
,Bool
, ...) 등 포함, 클래스, 구조체, 열거형, 함수타입 까지도 포함 (옵셔널 타입도 포함)
장점: 모든 타입을 표현이 가능하다.
var closure: (String) -> String = { name in
name
}
let array: [Any] = [5, "MacBook", 1.5, Developer(), closure]
단점: 저장된 타입의 메모리 구조를 알 수 없기 때문에
let array: [Any] = ["MacBook", 5, Developer(), closure]
let index0: Any = array[0]
let index1: Any = array[1]
let index2: Any = array[2]
let index3: Any = array[3]
사용하려면 항상 다운 캐스팅을 해줘야 된다.
let index0: String = (array[0] as! String)
let index1: Int = (array[1] as! Int)
let index2: Developer = (array[2] as! Developer)
let index3: (String) -> String = (array[3] as! (String) -> String)
AnyObject 타입
- 모든 클래스 인스턴스도 표현할 수 있는 타입
let objcArray: [AnyObject] = [Person(), Developer()]
let objcIndex0: Person = (objcArray[0] as! Person)
let objcIndex1: Developer = (objcArray[1] as! Developer)
AnyObject 마찬가지로 다운 캐스팅을 해줘야 사용이 가능합니다.
let objcIndex0: Person = (objcArray[0] as! Person)
let objcIndex1: Developer = (objcArray[1] as! Developer)
참고. 옵셔널값의 Any 타입
- Upcasting으로 컴퍼일러 경고를 없앨 수 있습니다.
let optionalNum: Int? = 10
print(optionalNum) // 경고
print(optionalNum as Any) // 경고 없음
지금까지 배운 것들을 응용하여 swich문으로 분기 처리를 해보고 마무리하겠습니다.
let array: [Any] = ["MacBook", 5, Developer(), {(str: String) in str}]
for (index, value) in array.enumerated() {
switch value {
case is String:
print("Index \(index) - String 타입 입니다.")
case let num as Int:
print("Index \(index) - \(num), Int 타입입니다")
case let danny as Developer:
print("Index \(index) - 개발자 : \(danny.name), 나이 : \(danny.age), 스킬 : \(danny.skill)")
case let closure as (String) -> String:
print("Index \(index) - \(closure) 클로저 타입 입니다.")
default: break
}
}
// Index 0 - String 타입 입니다.
// Index 1 - 5, Int 타입입니다
// Index 2 - 개발자 : Danny, 나이 : 20, 스킬 : Swift
// Index 3 - (Function) 클로저 타입 입니다.
부족한 설명이지만, 조금은 이해 가셨나요?
틀린 내용이 있다면 언제든지 지적해 주시면 감사히 받겠습니다. 🫠
읽어주셔서 감사합니다 😃
참고
'Xcode > Swift 문법' 카테고리의 다른 글
[iOS/Swift] 이니셜라이저(init)와 고차함수 (0) | 2023.02.08 |
---|---|
[iOS/Swift] 동시성(Concurrency) 프로그래밍 (0) | 2023.02.04 |
[iOS/Swift] AsyncStream / AsyncThrowingStream (0) | 2023.01.26 |
[iOS/Swift] 프로토콜 AsyncSequence (비동기 시퀀스) (0) | 2023.01.20 |
[iOS/Swift] 프로토콜 Sequence (0) | 2023.01.19 |