타입 캐스팅 (Type Casting)
- 타입 캐스팅(Type Casting) 은 인스턴스의 타입을 확인하거나, 해당 인스턴스를 다른 타입으로 변환하는 방법이다
- Swift에서 타입 캐스팅은 클래스 상속 관계에서 주로 사용되며, 구조체나 열거형에서는 사용되지 않는다
- 타입 캐스팅을 통해 인스턴스가 특정 클래스 타입인지 확인하고, 필요에 따라 타입을 변환할 수 있다
타입 캐스팅의 종류
- 타입 검사 (
is 연산자)
- 인스턴스가 특정 클래스 타입의 인스턴스인지 확인한다
- 상속 관계에서 부모 클래스, 자식 클래스 여부를 확인하는 데 사용한다
- 반환값은
Bool 타입 (true 또는 false)
- 타입 변환 (
as 연산자)
- 인스턴스를 특정 클래스 타입으로 변환한다
as?: 안전한 타입 변환 (Optional 반환)
as!: 강제 타입 변환 (강제 언래핑, 변환이 실패하면 런타임 에러 발생)
- 업캐스팅 (Upcasting)
- 하위 클래스의 인스턴스를 상위 클래스 타입으로 변환하는 것
- 항상 성공하며,
as 키워드를 사용한다
- 컴파일러가 항상 성공하는 것을 보장하기 때문에 에러가 발생하지 않는다
- 다운캐스팅 (Downcasting)
- 상위 클래스의 인스턴스를 하위 클래스 타입으로 변환하는 것
- 실패할 가능성이 있기 때문에
as? 또는 as! 를 사용한다
as?: 안전한 타입 변환 (성공하면 옵셔널 타입으로 반환, 실패하면 nil 반환)
as!: 강제 타입 변환 (성공하면 일반 타입으로 반환, 실패하면 런타임 에러 발생)
- 브릿징 (Bridging)
- Swift와 Objective-C 타입 간의 변환을 의미한다
- 예를 들어,
String ↔️ NSString, Array ↔️ NSArray 와 같은 변환이 자동으로 이루어진다
- 타입 캐스팅 (
as) 를 사용하여 서로 변환할 수 있다 (강제 캐스팅 필요 없음)
클래스 상속 구조 정의
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Mammal: Animal {
var isWarmBlooded: Bool = true
}
class Bird: Animal {
var canFly: Bool
init(name: String, canFly: Bool) {
self.canFly = canFly
super.init(name: name)
}
}
Animal, Mammal, Bird 클래스는 상속 관계로 정의되었다
Animal 클래스는 모든 동물의 공통 속성인 name을 저장한다
Mammal 클래스는 Animal을 상속받아 포유류의 특징을 추가한다 (isWarmBlooded)
Bird 클래스는 Animal을 상속받아 새의 특징을 추가한다 (canFly)
- 각 클래스의 인스턴스를 초기화하여 생성한다
타입 검사 (is 연산자)
is 연산자는 인스턴스가 특정 타입에 속하는지 검사하는 연산자이다
- 상속 관계의 클래스에서 타입을 확인하는 데 주로 사용된다
- 형식: 인스턴스
is 타입 ➔ 반환 값: true 또는 false
타입 검사 (is 연산자) 사용
let animal = Animal(name: "동물")
let mammal = Mammal(name: "사자")
let bird = Bird(name: "독수리", canFly: true)
print(animal is Animal)
print(animal is Mammal)
print(animal is Bird)
print(mammal is Animal)
print(mammal is Mammal)
print(mammal is Bird)
print(bird is Animal)
print(bird is Mammal)
print(bird is Bird)
is 연산자를 사용하여 인스턴스가 특정 타입인지 확인하고 있다
Animal 타입은 모든 인스턴스에서 참(true)으로 반환된다 (상속 관계)
Mammal 타입은 Mammal 인스턴스에서만 참(true)으로 반환된다
Bird 타입은 Bird 인스턴스에서만 참(true)으로 반환된다
- 이 예제는 타입 검사 기능을 간단히 확인하는 코드이다
타입 검사 활용 예제
let animals: [Animal] = [animal, mammal, bird, Mammal(name: "코끼리"), Bird(name: "펭귄", canFly: false)]
var mammalCount = 0
for someAnimal in animals {
if someAnimal is Mammal {
mammalCount += 1
}
}
print("Mammal 타입의 인스턴스 개수: \(mammalCount)")
animals 배열은 다양한 인스턴스를 포함하고 있다 (Animal, Mammal, Bird)
is 연산자를 사용하여 배열의 인스턴스가 Mammal 타입인지 검사한다
- 검사 결과가
true인 경우에만 mammalCount 값을 증가시킨다
- 결과적으로, 배열 내의
Mammal 타입 인스턴스는 2개이다
요약
is 연산자는 인스턴스가 특정 타입에 속하는지 확인하는 연산자이다
Animal, Mammal, Bird 클래스는 상속 관계로 정의되었다
is 연산자는 상속 관계에서 부모 타입 또는 자식 타입 여부를 확인하는 데 사용된다
- 타입 검사 결과는
true 또는 false로 반환된다
as 연산자 / 다운 캐스팅의 정확한 의미에 대한 이해
클래스 상속 구조 정의
class Employee {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Manager: Employee {
var department: String
init(name: String, age: Int, department: String) {
self.department = department
super.init(name: name, age: age)
}
}
class Developer: Employee {
var programmingLanguage: String
init(name: String, age: Int, programmingLanguage: String) {
self.programmingLanguage = programmingLanguage
super.init(name: name, age: age)
}
}
Employee, Manager, Developer 클래스는 상속 관계로 정의되었다
Employee 클래스는 name과 age라는 저장 속성을 가진다
Manager 클래스는 Employee를 상속받아 department 속성을 추가한다
Developer 클래스는 Employee를 상속받아 programmingLanguage 속성을 추가한다
- 각 클래스의 인스턴스를 초기화하여 생성한다
업캐스팅(Upcasting) (as 연산자)
- 하위 클래스의 인스턴스를 상위 클래스 타입으로 변환하는 것을 의미한다
- 업캐스팅은 항상 성공하며, 타입 변환 후 상위 클래스에서 정의된 속성과 메서드만 사용할 수 있다
as 키워드를 사용하여 변환한다
- 업캐스팅은 컴파일러가 항상 성공하는 것을 보장한다
업캐스팅 (as 연산자) 사용 예제
let developer = Developer(name: "Ethan", age: 29, programmingLanguage: "Swift")
let employee: Employee = developer as Employee
print("이름: \(employee.name), 나이: \(employee.age)")
developer 인스턴스를 Employee 타입으로 변환한다
- 업캐스팅은 항상 성공하며, 상위 클래스의 속성 (
name, age) 에 접근할 수 있다
programmingLanguage 속성에는 접근할 수 없다 (하위 클래스의 고유 속성이기 때문)
브릿징 (Bridging)
- 브릿징(Bridging) 은 Swift와 Objective-C 간의 호환성을 위해 사용된다
- Swift의 기본 타입(
String, Array, Dictionary)은 Objective-C의 타입과 상호 변환될 수 있다
- 타입 캐스팅(
as)을 사용하여 변환하며, 컴파일러가 자동으로 변환을 허용한다
- 강제 타입 캐스팅 (
as!) 이나 안전한 타입 변환 (as?) 이 필요하지 않다
- 브릿징은 타입 변환을 강제로 시도하지 않고 자연스럽게 연결되는 것이 특징이다
브릿징 (Bridging) 사용 예제
import Foundation
let swiftString: String = "Hello, Swift"
let nsString: NSString = swiftString as NSString
print("NSString 타입: \(nsString)")
let newSwiftString: String = nsString as String
print("String 타입: \(newSwiftString)")
let swiftArray: [String] = ["Swift", "Objective-C", "Python"]
let nsArray: NSArray = swiftArray as NSArray
print("NSArray 타입: \(nsArray)")
let newSwiftArray: [String] = nsArray as! [String]
print("Array 타입: \(newSwiftArray)")
- Swift의
String 타입이 NSString으로 자동 변환되고, 다시 String으로 변환되는 과정이다
- Swift의
Array 타입이 NSArray로 자동 변환되고, 다시 Array로 변환되는 과정이다
- Swift와 Objective-C의 타입은 자연스럽게 변환되므로 타입 캐스팅을 강제로 시도할 필요가 없다
다운캐스팅(Downcasting) (as? / as! 연산자)
- 상위 클래스의 인스턴스를 하위 클래스 타입으로 변환하는 것을 의미한다
- 다운캐스팅은 실패할 가능성이 있기 때문에 두 가지 방식으로 구분된다
안전한 타입 변환 (as?)
- 타입 변환이 성공하면 옵셔널 타입으로 반환된다
- 타입 변환이 실패하면
nil을 반환한다
- 왜 실패할 수 있는가?
• 실제로 해당 인스턴스가 하위 클래스의 인스턴스가 아닌 경우
• 예를 들어, Employee 타입이지만 실제로는 Manager가 아닌 Developer라면 nil을 반환한다
- 주로 옵셔널 바인딩 (
if let / guard let)과 함께 사용한다
강제 타입 변환 (as!)
- 타입 변환이 성공하면 일반 타입으로 반환된다
- 타입 변환이 실패하면 런타임 오류가 발생하여 프로그램이 종료된다
- 사용 시 매우 주의해야 한다 (프로그램이 비정상 종료될 수 있기 때문)
다운캐스팅 (as?, as! 연산자) 사용 예제
let employee: Employee = Developer(name: "Charlie", age: 30, programmingLanguage: "Python")
if let developer = employee as? Developer {
print("개발자 이름: \(developer.name), 언어: \(developer.programmingLanguage)")
} else {
print("다운캐스팅 실패 - Developer 타입이 아닙니다.")
}
let manager = employee as! Manager
print("매니저 이름: \(manager.name), 부서: \(manager.department)")
employee는 Employee 타입으로 선언되었지만 실제로는 Developer 인스턴스이다
- 안전한 타입 변환 (
as?)은 Developer로 변환이 성공하여 옵셔널 타입으로 반환한다
- 강제 타입 변환 (
as!)은 실패할 경우 프로그램이 강제로 종료된다
- 다운캐스팅 실패 시, 안전한 변환은
nil을 반환한다 (타입이 맞지 않기 때문)
요약
as 연산자는 항상 성공하는 업캐스팅을 수행한다
as? 연산자는 안전한 다운캐스팅을 수행하여, 실패 시 nil을 반환한다
as! 연산자는 강제 다운캐스팅을 수행하며, 실패 시 런타임 오류를 발생시킨다
- 브릿징은 Swift와 Objective-C 타입 간 변환을 자연스럽게 연결한다
상속과 다형성(Polymorphism)
다형성(Polymorphism)
- 다형성(Polymorphism) 은 하나의 객체(인스턴스)가 여러 가지 타입의 형태로 표현될 수 있는 것을 의미한다
- 주로 클래스의 상속을 통해 구현되며, 같은 메서드 호출이 다른 방식으로 동작할 수 있게 한다
- 다형성은 상위 클래스 타입으로 선언된 변수나 배열을 사용해, 하위 클래스의 인스턴스를 다룰 수 있는 특징을 가진다
- 메서드를 재정의하여 각 클래스의 인스턴스마다 다른 동작을 수행할 수 있다
클래스 상속 구조 정의
class Animal {
var name: String
init(name: String) {
self.name = name
}
func makeSound() {
print("동물이 소리를 낸다")
}
}
Animal 클래스는 모든 동물의 공통 속성인 name을 가진다
init(name:) 초기화 메서드를 통해 name 값을 설정한다
makeSound() 메서드는 동물이 소리를 내는 기본 동작을 정의하며, 상속받은 하위 클래스에서 재정의될 수 있다
- 이 메서드를 재정의하지 않으면 기본적으로
“동물이 소리를 낸다”라는 메시지가 출력된다
하위 클래스 정의
class Dog: Animal {
var breed: String
init(name: String, breed: String) {
self.breed = breed
super.init(name: name)
}
override func makeSound() {
print("\(name)(\(breed))가 멍멍! 하고 짖는다")
}
func fetch() {
print("\(name)가 공을 물어온다")
}
}
class Cat: Animal {
var color: String
init(name: String, color: String) {
self.color = color
super.init(name: name)
}
override func makeSound() {
print("\(name)(\(color))가 야옹! 하고 운다")
}
func climb() {
print("\(name)가 나무를 오른다")
}
}
Dog 클래스는 Animal을 상속받아 breed 속성을 추가한다
Cat 클래스는 Animal을 상속받아 color 속성을 추가한다
- 각 클래스는
makeSound() 메서드를 재정의하여 다형성을 구현한다
Dog 클래스는 fetch() 라는 고유 메서드를 추가로 정의한다
Cat 클래스는 climb() 라는 고유 메서드를 추가로 정의한다
- 상위 클래스의 메서드와 속성은 상속받아 사용할 수 있으며, 하위 클래스에서 재정의된 메서드는 다형성을 발현한다
다형성의 구현(Polymorphism)
let animal: Animal = Animal(name: "동물")
let dog: Animal = Dog(name: "바둑이", breed: "진돗개")
let cat: Animal = Cat(name: "나비", color: "하얀색")
let animals: [Animal] = [animal, dog, cat]
print("동물들의 소리:")
for creature in animals {
creature.makeSound()
}
동물들의 소리:
동물이 소리를 낸다
바둑이(진돗개)가 멍멍! 하고 짖는다
나비(하얀색)가 야옹! 하고 운다
dog 와 cat 인스턴스는 각각 Animal 타입으로 선언되어 업캐스팅된 상태이다
- 배열
animals 는 Animal 타입으로 선언되었지만, 실제로는 다양한 하위 클래스의 인스턴스를 포함하고 있다
- 배열의 요소가
Animal 타입으로 선언되었더라도, 실제 인스턴스의 타입에 따라 재정의된 makeSound() 메서드가 호출된다
- 이는 다형성의 중요한 특징으로, 상위 타입으로 선언되어도 실제 인스턴스의 타입이 가지고 있는 메서드가 호출되는 것을 의미한다
다형성의 메서드 호출 원리와 메서드 테이블
- 다형성의 구현은 메서드 테이블(Method Table) 이라는 개념을 통해 이루어진다
- 상위 클래스 타입으로 선언된 객체라도, 실제 메모리에 저장된 인스턴스의 메서드 테이블을 참조하여 메서드를 호출한다
- 만약
makeSound() 메서드를 재정의했다면, 해당 메서드 테이블의 주소가 업데이트되어 올바르게 호출된다
- 따라서, 코드에서는
Animal 타입으로 선언되었어도 실제로는 Dog 또는 Cat 클래스의 메서드를 호출할 수 있는 것이다
요약
- 다형성은 상위 클래스 타입으로 선언된 객체가 하위 클래스의 메서드를 호출할 수 있는 특성이다
- 상속과 메서드 재정의를 통해 구현되며, 메서드 테이블을 이용해 다형성을 실현한다
- 배열을 사용하여 다양한 인스턴스를 한 번에 처리할 수 있다
- 타입 검사와 다운캐스팅을 사용하여 필요한 기능을 활용할 수 있다
Any와 AnyObject를 위한 타입 캐스팅
- 스위프트는 불특정한 타입을 다룰 수 있는 타입을 제공한다
- 특정 타입이 아닌 다양한 타입을 저장하거나 사용할 때 유용하게 활용할 수 있다
Any 타입
Any 타입 은 모든 타입의 인스턴스를 표현할 수 있는 타입이다
- 기본 타입(
Int, String, Bool 등) 뿐만 아니라, 커스텀 클래스, 구조체, 열거형, 함수 타입까지도 포함한다
- 옵셔널 타입 또한 포함할 수 있다
Any 타입으로 저장된 값은 메모리 구조를 알 수 없기 때문에, 항상 타입캐스팅을 통해 사용해야 한다
Any 타입 사용 예제
var unknownValue: Any = "Hello, Swift"
unknownValue = 42
unknownValue = 99.99
if let stringValue = unknownValue as? String {
print("문자열: \(stringValue)")
} else if let intValue = unknownValue as? Int {
print("정수: \(intValue)")
} else if let doubleValue = unknownValue as? Double {
print("소수: \(doubleValue)")
}
소수: 99.99
unknownValue 는 Any 타입으로 선언되었기 때문에 모든 타입의 값을 저장할 수 있다
Any 타입은 저장된 값의 실제 타입을 알 수 없으므로, 사용할 때는 항상 타입캐스팅을 해야 한다
as? 연산자를 사용하여 안전하게 타입을 변환할 수 있다 (변환에 실패하면 nil 반환)
- 변환이 성공하면
if let 구문으로 옵셔널 바인딩하여 해당 타입으로 사용할 수 있다
AnyObject 타입
AnyObject 타입 은 어떤 클래스 타입의 인스턴스도 표현할 수 있는 타입이다
- 클래스 인스턴스만을 다루기 때문에 구조체, 열거형 등의 값 타입은 포함되지 않는다
- 주로 클래스 타입의 객체들을 담는 배열 등에 사용된다
AnyObject 타입 사용 예제
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Vehicle {
var model: String
init(model: String) {
self.model = model
}
}
let animal = Animal(name: "강아지")
let vehicle = Vehicle(model: "자동차")
let objectArray: [AnyObject] = [animal, vehicle]
for object in objectArray {
if let animal = object as? Animal {
print("동물의 이름: \(animal.name)")
} else if let vehicle = object as? Vehicle {
print("탈것의 모델: \(vehicle.model)")
}
}
동물의 이름: 강아지
탈것의 모델: 자동차
objectArray 는 AnyObject 타입의 배열로, 클래스 인스턴스만 저장할 수 있다
- 배열의 요소를 사용하기 위해서는 타입캐스팅이 필요하다 (
as? 사용)
Animal 과 Vehicle 클래스 인스턴스만 저장할 수 있기 때문에, 구조체나 열거형은 포함할 수 없다
- 타입캐스팅을 성공하면 해당 타입으로 사용 가능하며, 실패하면
nil 을 반환한다
타입캐스팅 + 분기 처리
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let mixedArray: [Any] = [
42,
"Hello",
3.14,
Person(name: "Alice", age: 25),
{ (x: Int) -> Int in return x * 2 }
]
for (index, item) in mixedArray.enumerated() {
switch item {
case is Int:
print("Index \(index): 정수입니다")
case let value as String:
print("Index \(index): 문자열 - \(value) 입니다")
case let value as Double:
print("Index \(index): 실수 - \(value) 입니다")
case let person as Person:
print("Index \(index): 사람 - 이름: \(person.name), 나이: \(person.age)")
case is (Int) -> Int:
print("Index \(index): 클로저 타입입니다")
default:
print("Index \(index): 알 수 없는 타입입니다")
}
}
mixedArray 는 다양한 타입의 값을 포함한 배열이다
for 문과 switch 문을 이용하여 배열의 각 요소를 검사한다
is 와 as 를 사용하여 타입을 확인하고 안전하게 변환한다
- 변환된 타입에 따라 각기 다른 처리를 수행한다
옵셔널 값의 Any 변환
let optionalValue: Int? = 42
print(optionalValue)
print(optionalValue as Any)
- 옵셔널 값은 직접 사용하면 경고가 발생한다 (
Optional(…) 형태로 출력)
- 옵셔널 값은 임시적인 값으로 간주되므로, 안전하게 사용하려면 옵셔널 바인딩을 해야 한다
- 하지만
as Any 로 변환하면 경고 없이 출력할 수 있다
Any 타입은 옵셔널 값도 포함할 수 있다
요약
Any 는 모든 타입을 포함할 수 있는 타입이다 (옵셔널 포함)
AnyObject 는 모든 클래스 타입을 포함할 수 있는 타입이다 (구조체와 열거형은 제외)
- 타입캐스팅을 통해 요소를 구분하고 사용할 수 있다 (
as?, is 활용)
[Any] 타입을 사용하면 배열에 다양한 타입의 값을 저장할 수 있다