부모 클래스로부터 상속된 자식 클래스는 자기 자신의 타입이기도 하면서, 동시에 부모 클래스의 타입이기도 하다. 이때문에 자식 클래스는 본래의 타입 대신 부모클래스 타입으로 선언하여 사용할 수 있다.
class Vehicle {
var currentSpeed = 0.0
func accelerate() {
self.currentSpeed += 1
}
}
class Car: Vehicle {
var gear: Int {
return Int(self.currentSpeed / 20) + 1
}
func wiper() {
// 창을 닦는다.
}
}
let trans: Vehicle = Car()
마지막 구문에서 상수로 선언된 trans는 Car 클래스의 인스턴스를 할당 받지만 Vehicle 타입으로 선언되었다. Car 클래스는 Vehicle 클래스를 상속받은 자식 클래스이며, 따라서 Vehicle 클래스에 정의된 모든 프로퍼티와 메소드를 물려받았다. Vehicle 클래스 타입에 구현되어 있어야 하는 프로퍼티와 메소드가 상속을 통해 모두 구현되어 있으므로 Car 클래스는 형식상 Vehicle 타입으로 간주할 수 있다. 반대로 Vehicle 인스턴스를 Car 클래스 타입 변수나 상수에 할당할 수는 없다. Car 타입이 되기위해 가져야 할 요소들 중에서 gear 프로퍼티나 wiper() 메소드가 Vehicle 클래스에는 정의되어 있지 않기 때문이다.
let car: Car = Vehicle() // Error
이처럼 상속 관계에 있는 상위 클래스 타입으로 선언해서 사용하는 이유는 함수나 메소드의 인자값을 정의할 때 하위 클래스 타입으로 선언하는 것보다 상위 클래스 타입으로 선언하면 인자값으로 사용할 수 있는 객체의 범위가 훨씬 넓어진다.
func move(param: Car) {
param.accelerate()
}
여기서 acceletate() 메소드는 Vehicle에서 정의된 것으로 만약 param이 Vehicle 타입을 받는다면 훨씬 넓은 범위의 인자값을 객체로 받을 수 있다. 아래처럼 바꿀 수 있다.
func move(param: Vehicle) {
param.accelerate()
}
타입 비교 연산
스위프트는 타입 비교 연산자 is를 지원한다. 변수나 상수 또는 인스턴스에 이 연산자를 사용하면 할당된 값을 비교하는 것이 아니라 타입이 일치하는지 여부를 비교하고 그 결과를 Bool 형태로 반환한다.
인스턴스(또는 변수, 상수) is 비교대상 타입
이 연산자는 다음과 같은 연산 법칙을 사용한다.
- 연산자 왼쪽 인스턴스의 타입이 연산자 오른쪽 비교대상 타입과 일치할 경우 - true
- 연산자 왼쪽 인스턴스의 타입이 연산자 오른쪽 비교대상 타입의 하위 클래스일 경우 - true
- 그 외 - false
아래는 예시 코드이다.
class SUV : Car {
}
SUV() is SUV // true
SUV() is Car // true
SUV() is Vehicle // true
Car() is Vehicle // true
Car() is SUV // false
타입을 비교할 때 연산자 왼쪽에 인스턴스가 아니라 인스턴스가 할당된 변수가 사용될 때 주의해야 한다. 변수가 선언된 타입을 기준으로 비교하는 것이 아니라 변수에 할당된 실제 인스턴스를 기준으로 타입을 비교하기 때문이다.
let myCar: Vehicle = SUV()
if myCar is SUV {
print("myCar는 SUV 타입입니다.")
} else {
print("myCar는 SUV 타입이 아닙니다.")
}
// 실행 결과
// myCar는 SUV 타입입니다.
myCar에 대입된 인스턴스는 SUV 타입이지만 타입 어노테이션을 통해 Vehicle 타입의 인스턴스로 선언되었다. 연산의 결과는 false가 되어야 할 것 같지만 true이다. 이는 타입 비교 연산자가 비교할 때 선언 타입이 아니라 대입된 실제 값의 타입을 비교하기 때문이다. SUV() 인스턴스가 대입되었기 때문에 결과값은 true이다.
타입 캐스팅 연산
상위 클래스 타입으로 선언된 상수에 인스턴스를 할당하는 다음 구문을 볼 것이다.
let someCar: Vehicle = SUV()
주어진 구문에서 someCar 상수는 실제로는 SUV 클래스의 인스턴스가 할당되어 있지만, 컴파일러는 이 상수를 Vehicle타입으로 간주한다. 즉 타입 어노테이션이 실제 대입된 값보다 우선한다. 따라서 Vehicle 클래스에 선언되지 않은 프로퍼티나 매소드를 사용할 수 없다. SUV 클래스에 선언된 프로퍼티를 사용하거나 SUV 타입을 인자로 받는 함수를 사용하려먼 타입 캐스팅 이라는 것을 해야한다.
타입 캐스팅은 특정 타입으로 선언된 타입을 다른 타입으로 변환하는 것을 말한다.
타입 캐스팅은 업 캐스팅과 다운 캐스팅으로 나눠진다.
업 캐스팅(Up Casting)
- 하위 클래스 타입을 상위 클래스 타입으로 변환할 때
- 캐스팅하기 전 타입이 하위 클래스, 캐스팅한 후 타입이 상위 클래스 일 때
- 캐스팅한 결과, 캐스팅하기 전 타입보다 추상화될 때
- 일반적으로 캐스팅 과정에서 오류가 발생할 가능성이 없음
다운 캐스팅(Down Casting)
- 상위 클래스 타입을 하위 클래스 타입으로 캐스팅할 때
- 캐스팅하기 전 타입이 상위 클래스, 캐스팅한 후 타입이 하위 클래스일 때
- 캐스팅한 결과, 전 타입보다 구체화될 때
- 캐스팅 과정에서 오류가 발생할 가능성이 있음(오류 발생시 nil 반환)
- 오류에 대한 처리 방식에 따라 옵셔널 캐스팅과 강제 캐스팅으로 나누어짐
타입 캐스팅을 위한 연산자는 as이다. 업 캐스팅에는 as 만 사용하면 되지만, 다운 캐스팅에는 as? 또는 as!를 사용하면 된다. as?를 사용하면 옵셔널 타입으로 변환되고, as!를 사용하면 일반 타입으로 변환되고, 실패시 런타입 오류가 발생한다.
// 업 캐스팅
객체 as 변환할 타입
// 다운 캐스팅
객체 as? 변환할 타입(결과는 옵셔널 타입)
객체 as! 변환할 타입(결과는 일반 타입)
아래는 업 캐스팅의 예제이다.
let anyCar: Car = SUV()
let anyVehicle = anyCar as Vehicle
anyCar는 Car 타입인 상태이다. 이를 상위 타입인 Vehicle로 변환하여 anyVehicle 상수에 담고 있다. 오류는 발생하지 않지만 anyCar 에서 사용했던 gear 프로퍼티나 wiper() 메소드는 사용불가이다. 다음으로 다운 캐스팅 예제이다.
let anySUV = anyCar as? SUV
if anySUV != nil {
print("\(anySUV!) 캐스팅이 성공하였습니다")
}
// 실행 결과
SUV 캐스팅이 성공하였습니다
다운 캐스팅에는 오류 발생 가능성이 있어 as? 연산자를 사용하였고 anySUV는 옵셔널 타입이다. 다운 캐스팅이 잘 되었다면 nil이 아니므로 nil이 아닐 경우 문구를 출력하도록 하였다. 일반 타입으로 변환하고 싶을 때 옵셔널 타입 해제를 거치게 된다.
if let anySuv = anyCar as? SUV {
print("\(anySUV!) 캐스팅이 성공하였습니다")
}
하지만 만약 다운 캐스팅이 반드시 성공한다는 확신이 있으면 강제 캐스팅을 사용해도 된다.
let anySUV = anyCar as! SUV
print("\(anySUV) 캐스팅이 성공하였습니다.")
Any, AnyObject
우리는 상속 관계에 있는 클래스들 사이에서 캐스팅을 진행했다. 하지만 상속 관계에 있지 않아도 타입 캐스팅이 가능한 예외가 있는데 바로 Any, AnyObject 타입이다. 이 둘은 일종의 범용 타입이다. 그 중 AnyObject는 클래스의 일종으로 모든 종류의 클래스 타입을 저장할 수 있는 범용 타입 클래스이다.
var allCar: AnyObject = Car()
allCar = SUV()
또한 모든 클래스의 인스턴스는 AnyObject 타입으로 선언된 함수나 메소드의 인자값으로 사용될 수도 있으며, AnyObject 타입을 반환하는 함수나 메소드는 모든 종류의 클래스를 반환할 수 있다는 의미로 해석되기도 한다.
func move(_ param: AnyObject) -> AnyObject {
return param
}
move(Car())
move(Vehicle())
고정된 하나만의 타입을 저장하는 배열, 딕셔너리, 집합에서도 AnyObject 타입을 사용하면 모든 클래스를 저장할 수 있다.
var list = [AnyObject]()
list.append(Vehicle())
list.append(Car())
list.append(SUV())
위에서 말했듯 AnyObject는 클래스일 때만 AnyObject 타입으로 정의할 수 있다. 구조체이거나 열거형은 허용하지 않는다. Any 객체 역시 비슷하지만, 클래스에 국한되지 않고 스위프트에서 제공하는 모든 타입을 허용하는 특성이 있다. 원시 자료형, 구조체, 열거형, 심지어 함수까지 적용된다.
var value: Any = "Sample String"
value = 3
value = false
value = [1, 3, 5, 7, 9]
value = {
print("함수가 실행됩니다.")
}
따라서 함수의 인자값으로 종류에 상관없이 입력받거나 집단 자료형에서도 구분 없이 사용 가능하다.
func name(_ param: Any) {
print("\(param)")
}
name(3)
name(false)
name([1, 3, 5, 7, 9])
var rows = [Any]()
rows.append(3)
rows.append(false)
rows.append([1, 3, 5, 7, 9])'swift' 카테고리의 다른 글
| 스위프트 옵셔널 체인 (0) | 2024.04.16 |
|---|---|
| 스위프트 초기화 구문 (1) | 2024.04.16 |
| 스위프트 상속 (1) | 2024.04.16 |
| 스위프트 메소드 (0) | 2024.04.16 |
| 스위프트 프로퍼티 (0) | 2024.04.15 |