본문은 야곰 아카데미 커리어 스타터 캠프를 통해 학습한 내용을 회고한 글입니다.
타입 캐스팅 Type Casting
타입 확인하기 Checking Type
어떤 인스턴스가 특성 서브클래스 타입에 속하는지 확인하기 위해서 타입 체크 오퍼레이터(is)를 사용한다. 만약에 인스턴스가 그 서브클래스 타입에 속하면 true를 반환하고, 그렇지 않으면 false를 반환한다.
class Music {
var name: String
init(name: String) {
self.name = name
}
}
class NothingButThieves: Music {
var year: Int
init(name: String, year: Int) {
self.year = year
super.init(name: name)
}
}
class HisaishiJoe: Music {
var movie: String
init(name: String, movie: String) {
self.movie = movie
super.init(name: name)
}
}
let playList = [
NothingButThieves(name: "Soda", year: 2017),
HisaishiJoe(name: "The Legend of Ashitaka", movie: "Princess Mononoke"),
NothingButThieves(name: "You know me too well", year: 2018),
NothingButThieves(name: "Moral Panic", year: 2020),
HisaishiJoe(name: "Merry-Go-Round of Life", movie: "Howl's Moving Castle")
]
//playList의 타입은 [Music]으로 추론 됨.
if playList is [Music] {
print("playList의 타입은 [Music]")
} //playList의 타입은 [Music]
var NBTCount = 0
var JoeCount = 0
for item in playList {
if item is NothingButThieves {
NBTCount += 1
} else if item is HisaishiJoe {
JoeCount += 1
}
}
print("playList에는 \(NBTCount)개의 낫벗띠 노래가 있고 \(JoeCount)개의 히사이시조 노래가 있다.") //playList에는 3개의 낫벗띠 노래가 있고 2개의 히사이시조 노래가 있다.
업캐스팅 Upcasting(as)
부모클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 타입정보를 전환해주는 과정이다.
class Music {
var name: String
init(name: String) {
self.name = name
}
}
class NothingButThieves: Music {
var year: Int
init(name: String, year: Int) {
self.year = year
super.init(name: name)
}
}
class HisaishiJoe: Music {
var movie: String
init(name: String, movie: String) {
self.movie = movie
super.init(name: name)
}
}
let playList: [Music] = [
NothingButThieves(name: "Soda", year: 2017),
HisaishiJoe(name: "The Legend of Ashitaka", movie: "Princess Mononoke"),
NothingButThieves(name: "You know me too well", year: 2018),
NothingButThieves(name: "Moral Panic", year: 2020),
HisaishiJoe(name: "Merry-Go-Round of Life", movie: "Howl's Moving Castle")
]
playList를 Music이라는 타입의 인스턴스만 들어갈 수 있다고 명시해줘봤다. 그런데, 문제가 없이 잘 작동한다. NothingButThieves, HisaishiJoe라는 타입의 인스턴스가 어떻게 들어간 것일까? NothingButThieves와 HisaishiJoe는 명백히 서로 다른 타입의 클래스이다. 하지만 부모 클래스가 같다는 공통점이 있다. 둘의 슈퍼클래스가 Music으로 동일하기 때문에 이들을 Music이라는 클래스로 업캐스팅해서 묶어버린 것이다.
as를 사용해서 타입 정보를 전환해 줄 수도 있다.
업캐스팅은 항상 성공한다고 볼 수 있다.
다운캐스팅 Downcasting(as?, as!)
특정 클래스 타입의 변수나 상수가 실질적으로 서브클래스의 인스턴스를 따르고 있을 수도 있다. 이런 경우에, 타입 캐스트 오퍼레이터(as?나 as!)를 사용해서 서브클래스로의 다운캐스팅을 시도해볼 수 있다. 타입캐스팅은 실패할 수 있기 때문에 as?와 as!의 두가지 형태로 나뉜다.
as?를 사용하면 다운캐스팅하려는 타입에 대해서 옵셔널한 값을 반환한다. 다운캐스팅이 성공할지 모르는 상황에서 사용한다. 이 오퍼레이터는 항상 옵셔널한 값을 반환하고, 다운캐스팅이 되지 않으면 nil을 반환한다. as?를 통해서 다운캐스팅이 될지 안 될지를 확인할 수 있다.
as!를 사용하면 다운캐스팅을 하고 그에 대해 언래핑된 값을 반환할 수 있다. 그래서 다운캐스팅이 성공할거라는 것을 확신하는 상황에서 사용해야한다. 만약에 as!를 사용해서 적절하지 않은 클래스 타입으로 다운캐스팅을 하려고 하면 런타임에러가 발생한다.
for music in playList {
if let nbt = music as? NothingButThieves {
print("Nothing But Thieves의 노래 \(nbt.name)는 \(nbt.year)년도에 릴리즈 되었다.")
} else if let joe = music as? HisaishiJoe {
print("Hisaishi Joe의 노래 \(joe.name)는 영화 \(joe.movie)에 삽입 되었다.")
}
}
//Nothing But Thieves의 노래 Soda는 2017년도에 릴리즈 되었다.
//Hisaishi Joe의 노래 The Legend of Ashitaka는 영화 Princess Mononoke에 삽입 되었다.
//Nothing But Thieves의 노래 You know me too well는 2018년도에 릴리즈 되었다.
//Nothing But Thieves의 노래 Moral Panic는 2020년도에 릴리즈 되었다.
//Hisaishi Joe의 노래 Merry-Go-Round of Life는 영화 Howl's Moving Castle에 삽입 되었다.
아까 playList에 각각의 인스턴스들을 넣으면서 타입이 Music으로 업캐스팅 된 상태이다. 이 상태에서 각각의 인스턴스들이 NothingButThieves 클래스로 다운캐스팅이 되면 nbt에 각각의 인스턴스가 들어가주고, HisaishiJoe 클래스로 다운캐스팅이 되면 joe에 각각의 인스턴스가 들어가주는 그런 형태이다. as?를 사용하면 옵셔널한 값이 반환되기 때문에 위처럼 if let을 통하여 옵셔널 바인딩을 해야한다.
싱글톤 Singleton
싱글톤 클래스는 어플리케이션에서 얼마나 많이 인스턴스를 요구하든지간에 같은 인스턴스를 반환해준다. 전형적인 클래스들은 호출할 때마다 인스턴스를 생성해주는 반면에, 싱글톤 클래스에서는 하나의 전역 인스턴스만을 생성한다. 그리고 그 이후로 다른 인스턴스가 생성되지 못하도록 한다.
몇몇의 Cocoa 프레임워크 클래스들은 싱글톤을 사용한 것이다. NSFileManager, NSWorkspace등이 포함되고, UIKit에서는 UIApplication and UIAccelerometer 등이 포함된다. 싱글톤 이스턴스를 반환하는 메서드의 이름은 전형적으로는 sharedClassType의 형태를 가진다. 예를 들면 sharedFileManager, sharedColorPanel, and sharedWorkspace처럼 말이다.
싱글톤 구현 Singleton implement
인스턴스를 저장할 프로퍼티를 생성하고 인스턴스가 또 생성되는 것을 막아주면 싱글톤 구현이 완성된다.
- static을 이용해 인스턴스를 저장할 프로퍼티 생성: 첫 번째로 인스턴스를 저장할 프로퍼티를 생성하기 위해서 static을 사용해서 프로퍼티를 만들어준다. 전역으로 저장될 것이기 때문에 static이라는 키워드를 사용하는 것이다.
- init 함수 접근제어자를 private로 지정: 혹시라도 init함수를 호출해 인스턴스를 또 생성하는 것을 막기 위해서 init() 함수의 접근 제어자를 private로 지정해주면 된다.
class FruitStore {
var fruitStock = [Fruit.딸기: 10, .바나나: 10, .파인애플: 10, .키위: 10, .망고: 10]
static let shared = FruitStore()
private init() {}
...
구현한 싱글톤을 접근하는 방법은 간단하다. 앞서 static을 이용해 생성한 프로퍼티를 이용해주면 된다.
struct JuiceMaker {
let fruitStore = FruitStore.shared
...
반환타입
무언가에 대한걸 전달할 때는 반환타입이 중요하다.
func useFruit(juice: Juice) -> [Fruit: Int]? {
let fruitRemainder = isStocked(juice: juice)
switch fruitRemainder {
case .success(let remainder):
for fruit in remainder {
fruitStock[fruit.0] = fruit.1
}
return fruitStock
case .failure(let error):
print(error.rawValue)
return nil
}
}
위 코드에서 fruit.0, fruit.1과 같이 숫자 인덱스를 사용해서 값에 접근하는게 싫었다. 튜플을 사용하면 라벨을 달아줄 수 있어서 그 부분을 해결하고자 했다.
func isStocked(juice: Juice) -> Result<[(Fruit, Int)], JuiceMakerError> {
let juice = juice.selectRecipe
var fruitList: [(fruit: Fruit, stock: Int)] = []
for (fruit, amount) in juice {
guard let stock = fruitStock[fruit] else {
return .failure(JuiceMakerError.invalidFruit)
}
let newStock = stock - amount
fruitList.append((fruit: fruit, stock: newStock))
if newStock < 0 {
return .failure(JuiceMakerError.outOfStock)
}
}
return .success(fruitList)
}
그래서 위와 같이 코드를 고쳤는데 전혀 반영이 안 되는 것이다..? 그러다가 중요한 것은 반환타입이라는 것을 깨달았다.
func isStocked(juice: Juice) -> Result<[(fruit: Fruit, stock: Int)], JuiceMakerError> {
let juice = juice.selectRecipe
var fruitList: [(Fruit, Int)] = []
for (fruit, amount) in juice {
guard let stock = fruitStock[fruit] else {
return .failure(JuiceMakerError.invalidFruit)
}
let newStock = stock - amount
fruitList.append((fruit, newStock))
if newStock < 0 {
return .failure(JuiceMakerError.outOfStock)
}
}
return .success(fruitList)
}
이렇게 Result안의 배열 안의 튜플에 라벨을 달아줘야 했던 것이다.
for fruit in remainder {
fruitStock[fruit.fruit] = fruit.stock
}
그 결과 이렇게 fruit과 stock이라는 라벨을 사용해서 값에 접근할 수 있게 되었다.
잊지 말자.
중요한 것은 반환타입이다.
블록 내부에서 아무리 쇼를 해도 반환타입에 그것을 반영하지 않으면 무소용!
Nil 반환
useFruit와 makeJuice에서는 상황에 따라 nil을 반환하고 있다. 이것이 뭔가 보기 좋지 않다고 생각했다.
func useFruit(juice: Juice) -> [Fruit: Int]? {
let fruitRemainder = isStocked(juice: juice)
switch fruitRemainder {
case .success(let remainder):
for fruit in remainder {
fruitStock[fruit.fruit] = fruit.stock
}
return fruitStock
case .failure(let error):
print(error.rawValue)
return nil
}
func makeJuice(juiceName: Juice) -> Juice? {
if let fruitstocks = fruitStore.useFruit(juice: juiceName) {
return juiceName
}
return nil
}
nil 반환이 나쁜 것일까? 꼭 그렇지 만은 않다고 그루트께서 말씀하셨다. 반환할 값이 없으면 nil을 반환해도 된다며.. 그렇지만, 사용자에게 경고?해야할 것이 있을 경우 error를 뱉어내주는 것이 훨씬 좋다고 하셨다. 무언가 유의미한 error가 있으면 그것을 반환해야하는 것이다. 그러므로 makeJuice같은 경우에는 nil을 반환하는 것도 좋지만, juice가 만들어지지 않았다는 error를 뱉어내주는게 좋겠다.
참조
https://babbab2.tistory.com/127
https://babbab2.tistory.com/66
'YAGOM CAREER STARTER' 카테고리의 다른 글
[TIL] 20230106: Modality/Navigation (0) | 2023.01.06 |
---|---|
[자료구조스터디] 20230104: Linked List (0) | 2023.01.06 |
[TIL] 20230104: KVO (0) | 2023.01.04 |
[TIL] 20230103: CaseIterable, 반복문을 통해 딕셔너리 key와 value 가져오기 (0) | 2023.01.03 |
[TIL] 20230102: MVC, Initialization, 쥬스 메이커, 연결 리스트 (0) | 2023.01.03 |