Type Casting
Type Casting이란?
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/typecasting/
Documentation
docs.swift.org
Type Casting
Determine a value`s runtime type and give it more specific tpye information.
공식문서에 따르면 Type Casting을 다음과 같이 설명하고 있습니다. 'Value의 런타임 타입을 결정하고 보다 구체적인 타입 정보를 제공한다' 라고 되어 있습니다.
Type Casting은 인스턴스의 타입을 체크하는 방법이고 is 또는 as 연산로 실행됩니다.
Type Casting의 종류
- is - 타입 체크 연산자
- as - 지정된 타입을 변경하는 연산자
- as: 업캐스팅(Upcasting)
- as!, as?: 다운캐스팅(Downcasting)
Type Casting 예시
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")
]
간단하게 예시 코드를 설명하자면 MediaItem 이라는 Root Class를 만들었고 각각의 Movie, Song class에서 ModiaItem 클래스를 상속받고 있는 구조입니다. library라는 상수 배열을 만들어 Movie와 Song의 인스턴스를 몇 개 넣어놓은 코드입니다.
is 연산자
is 연산자는 해당 인스턴스가 특정 하위 클래스 타입인지 확인하는 연산자이다. 인스턴스가 해당 하위 클래스 타입인 경우 true를 반환하고 아닐 경우 false를 반환합니다.
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
}
}
print(movieCount) // 2
print(songCount) // 3
for문을 통해 library 상수 배열에 들어있는 인스턴스를 Movie Class와 Song Class의 인스턴스와 같은지 검사하는 분기처리를 통해 true값을 +1 씩 올려주는 코드입니다. 결과를확인해보니 예상한대로 결과가 나왔습니다.
그렇다면 다음 코드를 살펴보겠습니다.
let movie1 = Movie(name: "Casablanca", director: "Michael Curtiz")
movie1 is Movie // true
movie1 is MediaItem // true
Movie 클래스와 비교했을 때는 당연히 Movie Class로 인스턴스를 만들어냈으니 true입니다. 그럼 상속을 받았던 부모클래스(MediaItem Class)와 비교했을 때 결과는? 네 바로 true입니다. 조금 생각해보면 당연한 결과입니다. 부모 클래스에서 모든것을 물려받고 Movie Class에서 필요했던 프로퍼티만 추가한 결과니깐 더 큰 범위에 있는 부모클래스와 비교하니 같다고 하는 것입니다.
as 연산자
as연산자는 2가지로 나뉜다고 했습니다.
- as: 업캐스팅(Upcasting)
- as?, as!: 다운캐스팅(Downcasting)
is연산자와는 조금 다른 예시로 살펴보겠습니다.
class Media {
var name: String
init(name: String) {
self.name = name
}
}
class Movie: Media {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
class Song: Movie {
var artist: String
init(name: String, director: String, artist: String) {
self.artist = artist
super.init(name: name, director: director)
}
}
이번에는 Movie클래스와 Song클래스가 각각 Media클래스를 상속받는 것이 아닌 순서대로 Song클래스도 Media클래스를 상속받은 Movie클래스를 상속받은 형태로 코드를 작성해보았습니다. 예시가 썩 마음에 들진 않지만 애플 공식문서에 나온 예시를 조금 변형해서 활용해보도록 하겠습니다.
Media Class -> Movie Class -> Song Class
Downcasting(다운캐스팅)
let media: Media = Song(name: "Super Shy", director: "감독", artist: "뉴진스")
media 상수에 Media클래스 타입으로 지정하고 하위 클래스인 Song클래스로 인스턴스를 만들고 프로퍼티를 사용해보겠습니다.
media.name
media.director
media.artist
Value of type 'Media' has no member 'director'
Value of type 'Media' has no member 'artist'
Song클래스로 만들었으니 부모클래스의 프로퍼티는 물론 인스턴스를 찍어내었던 클래스의 프로퍼티까지 사용할 수 있을 줄 알았으나 컴파일 에러가 발생한다. 에러 내용은 'Media는 멤버(프로퍼티)로 director, artist를 가지고 있지 않다.'라고 합니다. 그렇습니다. 정리하자면 다음과 같습니다.
💡 하위클래스로 인스턴스를 찍어낸다 하더라도 상위클래스로 타입을 지정한다면 메모리 구조에 우선 하위클래스가 가진 속성을 전부 생성은 하지만 타입을 지정 클래스가 가진 속성에만 접근을 할 수 있습니다.
위 내용은 타입캐스팅을 이해하는데 정말 중요한 개념이라고 생각합니다. 계속 클래스의 구조와 상속관계에 있을 때 메모리 구조가 어떻게 될지 보이진 않지만 항상 생각하는 습관이 중요할 것 같습니다.
as? vs as!
as?와 as!의 차이점을 잠깐 짚고 넘어가겠습니다.
as 키워드에는 ?,! 2가지 키워드를 나타낼 수 있는데 as? 키워드를 사용하면 다운캐스팅을 했을 때 실패할 가능성이 있기 때문에 다운캐스팅이 실패하면 nil을 반환합니다. 그래서 if let 바인딩으로 값을 추출하여 사용하면 되겠습니다. 하지만 as! 키워드는 실패하더라도 강제추출하기 때문에 다운캐스팅에 실패한다면 컴파일 에러를 반환하게 됩니다.
as? -> nil 반환
as! -> 컴파일 에러
여기서 잠깐 실패할 가능성에 대해 얘기하자면 처음부터 상위클래스로 인스턴스를 생성하고 타입을 지정할 때 발생합니다. 그렇다면 언제 다운캐스팅에 실패하게 될까요? 간단한 예시를 통해 살펴보겠습니다.
let media: Media = Media(name: "미디어")
let downCasting = media as? Song
downMedia의 값을 확인해보면 nil입니다. 네 상속관계에 있는 Song클래라 하더라도 이미 메모리에는 Media 타입으로 Media클래스가 가진 멤버 속성만 올라있을 것입니다. 그런데 하위클래스로 인스턴스를 만들지도 않았는데 타입캐스팅을 사용하려고하면 당연히 하위클래스의 멤버에 접근할 수도 없고(메모리에 없으니깐) 타입캐스팅에 실패하게되는 것입니다. 그래서 nil이 나온 것이고 이렇게 불확실한 상황에서 as? 연산자를 통해 타입캐스팅을 할 수 있을 것 같습니다. 불확실한 상황인데 강제 언래핑을 통해 타입캐스팅을 시도하면 컴파일 에러가 발생하게 될 것입니다.
as?
그럼 이번엔 지정했던 Root 클래스 타입인 Media를 다운캐스팅을 통해 하위클래스로 만들어보겠습니다.
let media: Media = Song(name: "Super Shy", director: "감독", artist: "뉴진스")
let downCasting = media as? Song
downMedia 상수의 타입을 살펴보면 옵셔널 Song클래스입니다. 네 이해되셨을겁니다. 부모클래스의 타입을 지정했더라도 일단 하위 클래스로 인스턴스를 만들어냈으니 메모리에는 올라가 있지만 접근이 불가능한 것 뿐이었습니다. 그걸 타입캐스팅 정확히 하면 다운캐스팅을 통해 Song클래스로 만들었습니다. 물론 as? 연산자를 사용했기때문에 반환값도 옵셔널인 것 뿐입니다.
as!
지금은 Song클래스로 다운캐스팅해도 아무문제가 없다고 확실한 상황입니다. 그럼 as! 연산자를 통해 다운캐스팅 해보겠습니다.
let media: Media = Song(name: "Super Shy", director: "감독", artist: "뉴진스")
let downCasting = media as! Song
반환값을 확인해보면 옵셔널이 아닌 그냥 Song클래스입니다.
결론적으로 상황에 맞게 사용해주면 될 것 같습니다.
앱 만들 때 타입 캐스팅 예시
그런데 이런 타입 캐스팅을 앱에서 어디서 활용할까요? 이미 관습적으로 사용하고 있었을수도 있고 정확히 이해하고 사용하고 있을 수도 있습니다.
많은 예시가 있겠지만 대표적으로 TableView를 만들 때 Cell을 만들어주는 메서드에서 Cell을 직접만든 Cell 파일로 다운 캐스팅해주는 코드를 많이 보셨을겁니다.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath)
return cell
}
우선 cellForRowAt 메서드의 return 타입이 UITableViewCell 이니 dequeueReusableCell 속성으로 해당 셀의 identifier를 통해 cell을 만들어 줍니다. 여기까지만 해도 기본 cell은 구현되고 나타낼 수 있습니다. 하지만 cell이 복잡해지고 해당 cell class에 속성과 메서드를 만들어야해서 파일을 만들었다고 생각하면 해당 cell에 접근해서 그 속성과 메서드를 사용할 수 있어야겠죠? 그래서 여기서 다운캐스팅을 통해 직접 만든 UITableViewCell 로 다운캐스팅해서 해당 클래스의 속성과 메서드를 활용하는 것입니다.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier) as? CustomTableViewCell else {
return UITableViewCell()
}
return cell
}
CustomTableViewCell 클래스로 다운캐스팅하면 이제 타입이 CustomTableViewCell이 된 것을 확인할 수 있습니다.
Upcasting(업캐스팅)
업캐스팅 단어만으로 보면 다운캐스팅의 반대말이니 자식클래스 타입에서 상속받고 있는 부모클래스로 타입을 형변환하는 것을 말합니다.
> 💡 업캐스팅이란 하위클래스로 인스턴스를 생성하고 타입을 지정하고 나중에 as 연산자로 상위클래스로 타입을 변경한다면 실패할 가능성이 없습니다. 그래서 업캐스팅 상황에서는 ?,! 키워드 없이 as 연산자만으로 타입을 변경할 수 있습니다.
여기서 실패할 가능성이 없는 이유는 처음 변수나 상수에 인스턴스를 할당할 때 생성되는 메모리 구조를 생각해보면 당연하다고 볼 수 있습니다. 하위클래스로 먼저 메모리에 할당 했기 때문에 상속된 모든 속성을 가지고 있는데 상위 클래스로 타입을 변경한다 하더라도 당연히 상위클래스의 상속된 속성에 접근이 가능하기 때문에 실패할 가능성이 없다고 말하는 것입니다.
let song: Song = Song(name: "Super Shy", director: "감독", artist: "뉴진스")
song.name // Super Shy
song.director // 감독
song.artist // 뉴진스
let upCasting = song as Media
upCasting 상수의 타입은 Media입니다. 이미 부모클래스의 모든 속성을 가지고 메모리에 할당되었기 때문에 부모클래스로 업캐스팅을 실패없이 할 수 있는 모습입니다.
Any & AnyObject
Any
타입캐스팅과 관련 있고 활용해볼 수 있는 Any 타입에 대해서도 같이 알아보겠습니다. swift에서는 기본적으로 타입을 강제하고 있습니다
var anyType = "Any Type"
anyType = 10
String 타입을 가진 변수에 Int 타입을 할당하려고 하면 에러가 납니다.
Cannot assign value of type 'Int' to type 'String'
하지만 이것을 Any타입으로 해결할 수 있습니다.
var anyType: Any = "Any Type"
anyType = 10
print(anyType) // 10
Any 타입으로 지정하면 어떠한 자료형도 다룰 수 있기 때문에 정수형을 다시 저장하더라도 에러가 나지 않고 저장할 수 있게 됩니다. 하지만 여기서 생각해볼 수 있는 문제가 이러면 굳이 자료형 타입을 명시하지 않고 모두 Any로 지정하고 편하게 쓰면 되지 않을까? 라는 의문이 생기지만 Any 타입을 사용하게 되면 항상 타입 캐스팅해서 사용해야서 불편한 점이 존재합니다.
var anyType: Any = "Any Type"
anyType.count
원래 사용하던 문자열의 개수를 셀 수 있는 메서드인 count를 사용하면 에러가 발생합니다.
Value of type 'Any' has no member 'count'
Any타입은 count멤버가 없다고 합니다. 그렇습니다. count멤버는 Sting타입에 구현되어 있던 메서드를 가져다 쓴 것이었습니다. 그런데 Any타입으로 아무 타입이나 다룰 땐 편했는데 막상 해당 데이터타입의 기능들을 쓰려니 못쓰게 되었습니다. 불편하네요😭
그렇다면 count를 어떻게 사용할 수 있을까요?
var anyType: Any = "Any Type"
(anyType as! String).count
Any 타입을 String으로 타입캐스팅하여서 사용해야합니다. 귀찮습니다. ㅎ
Any타입을 배열에도 활용할 수 있습니다.
class Person {
var name: String
var age: Int
init(_ name: String, _ age: Int) {
self.name = name
self.age = age
}
}
let person = Person("철수", 15)
var anyTypeArray: [Any] = [
"Any Type",
100,
3.5,
true,
person
]
AnyObject
AnyObject 란 클래스의 인스턴스만 담을 수 있는 타입이다. 구조체의 인스턴스는 담을 수 없다.
class Person {
var name: String = ""
var age: Int = 0
}
class Human {
var name: String = ""
var age: Int = 0
}
var anyObjectArry: [AnyObject] = [Person(), Human()]
구조체 타입을 담으면 컴파일 에러가 발생합니다.
class Person {
var name: String = ""
var age: Int = 0
}
class Human {
var name: String = ""
var age: Int = 0
}
struct Animal {
var name: String = ""
var age: Int = 0
}
var anyObjectArry: [AnyObject] = [Person(), Human(), Animal()]
Cannot convert value of type 'Animal' to expected element type 'AnyObject'
여기까지 타입캐스팅과 Any, AnyObject의 개념까지 살펴봤습니다.
타입캐스팅은 클래스 타입이 상속관계에 있을 때 또 인스턴스를 생성했을 때 상황별로 메모리에 어떻게 올라가지는 고민해보면서 이해한다면 그렇게 어렵지 않을 것 같습니다. 그렇기 때문에 귀찮아도 항상 메모리 구조와 매칭시켜 생각하는 습관을 가지면 좋을 것 같습니다.
'iOS > Swift' 카테고리의 다른 글
Swift - Swift에서는 왜 문자열을 Subscript로 접근할 수 없을까? (1) | 2023.12.12 |
---|---|
Swift - Method Swizzling (1) | 2023.12.01 |
ARC(Automatic Reference Counting) (0) | 2023.09.04 |
Swift - Protocol (0) | 2023.08.07 |
Swift - @Property Wrapper (1) | 2023.07.27 |