19. Swift 문법 - Inheritance and Polymorphism
1. Inheritance
상속에 대해 알아보겠습니다.
상속 관계에 있는 클래스들은 상속 계층을 구성합니다. 클래스 계층에서 가장 위에 있는 클래스를 Base Class 또는 Root Class 라고 합니다. 바로 아래 계층의 클래스는 Base Class 를 상속합니다. 상속 관계에서 위에 있는 클래스를 Super Class 라고 하거나 Parent Class 라고 합니다. 그리고 아래쪽에 있는 클래스는 Sub Class 또는 Child Class 라고 합니다.
Objective-C 에서는 모든 클래스가 NSObject 라는 클래스를 상속해야 하지만 Swift 에서는 이런 제약이 없습니다.
여러 Sub Class 가 하나의 Super Class 를 상속하는 것은 문제가 없지만, 하나의 Sub Class 가 두 개 이상의 Super Class 를 상속하는 것은 문제가 됩니다. 이것을 다중 상속이라고 하는데 Swift 에서는 지원되지 않습니다. 다중 상속과 유사한 패턴은 Protocol 을 통해 구현됩니다.
다른 클래스를 상속하는 것을 Subclassing 이라고 합니다. Sub Class 는 Super Class 에 선언되어 있는 멤버들을 상속합니다. 마치 Sub Class 에 선언한 것처럼 자유롭게 사용이 가능합니다.
Super Class 에서 상속 받은 멤버가 Sub Class 에 맞지 않다면 구현을 수정하는 것도 가능합니다. 이것을 Overriding(재정의) 라고 합니다.
Super Class 에서 상속은 하면서 재정의를 금지하는 것도 가능합니다.
// 문법
// class ClassName: SuperClassName {
// }
class Figure {
var name = "Unknown"
init(name: String) {
self.name = name
}
func draw() {
print("draw \(name)")
}
}
class Circle: Figure {
var radius = 0.0
}
// Circle 클래스에는 생성자를 구현하지 않았지만
// Circle 클래스가 Figure 클래스를 상속 받았기 때문에
// name 파라미터를 가지는 생성자를 사용할 수 있습니다.
let c = Circle(name: "Circle")
c.radius
// Super Class 의 멤버에도 자유롭게 접근할 수 있습니다.
c.name
c.draw()
final 키워드를 통해 상속을 금지할 수 있습니다.
// 문법
// final class ClassName: SuperClassName {
// }
// 다른 클래스를 상속하는 것은 가능하지만,
// 다른 클래스가 final class 를 상속 받는 것은 금지됩니다.
final class Rectangle: Figure {
var width = 0.0
var height = 0.0
}
// final class 는 상속이 금지된 클래스로 아래 코드는 에러가 납니다.
//class Square: Rectangle {
//
//}
2. Overriding
상속 받은 멤버를 Sub Class 에 맞게 변경하는 것으로 Sub Class 에서 Super Class 와 동일한 멤버를 구현하는 것입니다. Overriding 이 가능한 멤버는 method, 속성, subscripts, 생성자입니다.
Overriding 은 두 가지 방식으로 구현됩니다.
1. Super Class 구현을 기반으로 새로운 코드을 추가하거나
2. Super Class 구현을 무시하고 완전히 새롭게 구현합니다.
class Figure {
var name: String
init(name: String) {
self.name = name
}
func draw() {
print("draw \(name)")
}
}
class Circle: Figure {
var radius = 0.0
var diameter: Double {
return radius * 2
}
override func draw() {
super.draw() // 먼저 상위 구현을 호출
print("🔵") // 새롭게 구현
}
}
let c = Circle(name: "Circle")
c.draw()
속성을 overriding 하는 경우에는 메소드와 조금 다른 방식을 사용합니다. 속성을 overriding 할 때는 계산 속성을 구현하거나 프로퍼티 옵저버를 사용합니다.
먼저 계산 속성을 구현 하는 방식을 알아보겠습니다.
class Oval: Circle {
override var radius: Double {
get {
return super.radius
}
set {
super.radius = newValue
}
}
override var diameter: Double {
get {
return super.diameter
}
set {
super.radius = newValue / 2
}
}
}
계산 속성으로 구현할 때 주의할 점이 있습니다.
- 읽기와 쓰기가 가능한 속성은 읽기 전용으로 오버라이딩 하는 것은 허용되지 않습니다. 반드시 읽기와 쓰기가 모두 가능한 계산 속성으로 overriding 하여야 합니다.
- 읽기 전용 속성을 overriding 할 때 get block, set block 모두 구현 가능하지만 읽기 전용 속성의 값을 변경할 수는 없고 다른 속성의 값을 변경하는 것은 가능합니다.
다음으로 Property Observer 를 사용하여 구현하는 방법을 살펴보겠습니다.
class Oval: Circle {
// 프로퍼티 옵저버로 구현하기
// super 의 속성이 변수 저장 속성으로 되어 있는 경우 가능합니다.
// 읽기 전용 속성의 경우에는 값이 변하지 않는 속성이므로 프로퍼티 옵저버로 오버라이딩 할 수 없습니다.
override var radius: Double {
willSet {
print(newValue)
}
didSet {
print(oldValue)
}
}
}
overriding 또한 final 키워드를 통해 금지할 수 있는데 이것이 상속 자체의 금지를 의미하는 것은 아닙니다.
3. Upcasting and Downcasting
먼저 몇가지 class 들을 선언하겠습니다.
class Figure {
var name: String
init(name: String) {
self.name = name
}
func draw() {
print("draw \(name)")
}
}
class Rectangle: Figure {
var width = 0.0
var height = 0.0
override func draw() {
super.draw()
print("◼️ \(width) x \(height)")
}
}
class Square: Rectangle {
}
let f = Figure(name: "Unknown")
f.name
let r = Rectangle(name: "Rect")
r.width
r.height
r.name
Upcasting 이란, 서브클래스 인스턴스를 슈퍼클래스 형식으로 저장하는 것을 말합니다.
let s: Figure = Square(name: "Square")
s.name
Downcasting 이란, Upcasting 된 인스턴스를 원래 형식으로 처리하기 위해 필요합니다. Upcasting 과 달리 에러가 발생할 수 있고 항상 성공하는 것도 아닙니다. Downcasting 에는 Typecasting 연산자를 사용합니다.
// Figure 타입인 s 를 Rectangle 타입으로 Downcasting 했습니다.
let downcastedS = s as! Rectangle
downcastedS.width
downcastedS.height
downcastedS.name
class Rhombus: Square {
var angle = 45.0
}
// 원본 클래스보다 아래쪽에 있는 클래스로 Downcasting 하는 것은 허용되지 않습니다.
// let dr = s as! Rhombus -> 에러
4. Type casting
인스턴스 형식을 확인하거나 다른 형식의 인스턴스를 처리할 때 사용합니다. Type casting 의 연산자는 두가지가 있는데 먼저 Type check operator 를 알아보겠습니다.
(1) Type Check Operator
문법은 다음과 같습니다.
expression is Type
is 연산자라고 부르기도 합니다. Type Check 연산자의 경우 런타임에 타입을 확인합니다.
두 피연산자의 형식이 동일한 경우 true 를 리턴하며, 왼쪽 피연산자의 형식이 오른쪽 피연산자와 동일한 상속 계층에 있고 오른쪽 피연산자가 슈퍼클래스일 때도 true 가 리턴됩니다. 나머지 경우에는 false 가 리턴됩니다.
let num = 123
num is Int // true
num is Double // false
num is String // false
class Figure {
var name: String
init(name: String) {
self.name = name
}
func draw() {
print("draw \(name)")
}
}
class Triangle: Figure {
override func draw() {
super.draw()
print("🔺")
}
}
class Rectangle: Figure {
var width = 0.0
var height = 0.0
override func draw() {
super.draw()
print("◼️ \(width) x \(height)")
}
}
class Square: Rectangle {
}
class Circle: Figure {
var radius = 0.0
override func draw() {
super.draw()
print("🔴")
}
}
let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rectangle")
let s = Square(name: "Square")
let c = Circle(name: "Circle")
r is Rectangle // true
r is Figure // true
r is Square // false
(2) Type Casting Operator
expression as Type
위와 같이 작성하면 Compile Time Cast 로 컴파일 타임에 결과가 확정됩니다. 따라서 캐스팅에 성공하면 에러가 발생하지 않고 캐스팅에 실패하면 컴파일 에러가 발생합니다.
expression as? Type
expression as! Type
위 두가지 경우는 Run Time Cast 방식으로 런타임에 결정되기 때문에 컴파일 타임에는 결과를 알 수 없습니다. as? 의 경우 캐스팅에 성공하면 캐스팅된 인스턴스를 리턴하고 실패하면 nil 을 리턴합니다. as! 의 경우 강제 추출 연산자와 마찬가지로 캐스팅에 실패할 경우 crash 가 발생합니다. 따라서 forced cast 는 가능하면 사용하지 않는 것이 좋습니다.
주로 다운캐스팅을 할 때나 값 형식을 다른 형식으로 전환할 때 사용합니다. 왼쪽 피연산자의 형식이 오른쪽 형식과 호환된다면 오른쪽 형식으로 캐스팅 된 인스턴스를 리턴합니다. 새로운 인스턴스가 리턴되는 것은 아닙니다. 이미 존재하는 인스턴스에서 오른쪽 피연산자 형식에 있는 멤버만 접근할 수 있는 임시 인스턴스를 리턴합니다.
// 구조체로 구현된 String 자료형은 클래스로 구현된 NSString 자료형으로 호환됩니다.
let nsstr = "str" as NSString
// "str" as Int - 컴파일 에러 발생
t as? Triangle // true
t as! Triangle // true
var upcasted: Figure = s
// Upcasting 의 경우 항상 성공하기 때문에 Compile cast 를 사용할 수 있습니다.
// upcasted = s as Figure
// 하지만 Downcasting 에는 Compile cast 를 사용할 수 없습니다.
upcasted as? Square // true
upcasted as! Square // true
upcasted as? Rectangle // true
upcasted as! Rectangle // true
upcasted as? Circle // nil
// upcasted as! Circle - 에러
// 실제로 Downcasting 을 할 경우 아래와 같이 사용하는 것이 좋습니다.
if let c = upcasted as? Circle {
}
// 배열의 경우 동일한 형식의 값들만 저장할 수 있는데,
// 모든 요소가 동일한 상속 계층에 있다면 가장 인접한 슈퍼 클래스로 Upcasting 되어 저장이 됩니다.
let list = [t, r, s, c] // list 의 타입은 [Figure]
// item 은 Figure 형식이지만 각 인스턴스에서 overriding 한 메소드가 호출됩니다.
// 이것을 다형성(Polymorphism)이라고 합니다.
// Upcasting 되어있는 인스턴스를 통해 메소드를 호출하더라도 실제 형식에서 overriding 된 메소드가 호출된다는 것입니다.
for item in list {
item.draw()
if let c = item as? Circle {
c.radius
}
}
// 결과
draw Triangle
🔺
draw Rectangle
◼️ 0.0 x 0.0
draw Square
◼️ 0.0 x 0.0
draw Circle
🔴
5. Any & AnyObject
범용 자료형으로 Any 는 모든 형식을 저장할 수 있고, AnyObject 는 모든 클래스 형식을 저장할 수 있습니다.
// Any 로 지정하면 형식에 관계 없이 모든 데이터를 저장할 수 있습니다.
var data: Any = 1
data = 2.3
data = "String"
data = [1, 2, 3]
data = NSString() // 값 형식, 참조 형식 상관 없이 모두 저장 가능합니다.
// AnyObject 에는 참조 형식만 저장할 수 있습니다.
var obj: AnyObject = NSString()
// obj = 1 -> 값 형식을 저장하는 것은 에러가 발생합니다.
// 이 외에도 Any 로 시작하는 다양한 형식들이 있는데
// Swift 에서는 이를 Type-Erasing Wrapper or Type-Erased Wrapper 라고 합니다.
// Any 와 AnyObject 는 형식에 대한 정보를 가지고 있지 않습니다.
// 형식에 대한 정보가 없기 때문에 속성이나 메소드에 접근할 수 없습니다.
// 그래서 인스턴스를 사용하기 위해서는 타입 캐스팅이 필요합니다.
if let str = data as? String {
print(str.count)
} else if let list = data as? [Int] {
}
// Type Casting Pattern
// switch 문과 type casting 연산을 함께 수행하는 패턴을 말합니다.
// 범용 형식으로 저장되었거나 Upcasting 된 인스턴스를 매칭 시킬 때 주로 사용합니다.
switch data {
case let str as String:
print(str.count)
case let list as [Int]:
print(list.count)
case is Double:
print("Dounle Value")
default:
break
}
6. Overloading
하나의 형식에서 동일한 이름을 가진 다수의 멤버를 구현할 때 사용합니다. 함수, 메소드, subscript, 생성자에서 오버로딩을 지원합니다. overloading 규칙을 살펴보면 다음과 같습니다.
- 규칙1: 함수 이름이 동일하면 파라미터 수로 식별
- 규칙2: 함수 이름, 파라미터 수가 동일하면 파라미터 자료형으로 식별
- 규칙3: 함수 이름, 파라미터가 동일하면 Argument Label 로 식별
- 규칙4: 함수 이름, 파라미터, Argument Label 이 동일하면 리턴형으로 식별
// 1번 함수
func process(value: Int) {
print("process Int")
}
// 2번 함수: 1번 함수와 규칙 2에 의해 식별됩니다.
func process(value: String) {
print("process String")
}
// 3번 함수: 2번 함수와 규칙 1에 의해 식별됩니다.
func process(value: String, anotherValue: String) {
}
// 4번 함수: 2번 함수와 규칙 3에 의해 식별됩니다.
func process(_ value: String) {
}
process(value: 0)
process(value: "str")
process("str")
보통의 경우 규칙 3번까지만 사용하고 리턴형으로 식별하는 것은 피하는 것이 좋습니다.
func process(value: Double) -> Int {
return Int(value)
}
func process(value: Double) -> String {
return String(value)
}
let result: Int = process(value: 12.34)
// 타입 추론으로 형식을 지정하고 싶은 경우, let result = process(value: 12.34) as Int 를 사용합니다.
위에서는 함수에 대해서만 알아보았는데 메소드에서도 살펴보도록 하겠습니다.
struct Rectangle {
func area() -> Double {
return 0.0
}
static func area() -> Double {
return 0.0
}
}
let r = Rectangle()
r.area()
Rectangle.area()
인스턴스 메소드와 타입 메소드는 함수 이름, 파라미터, Argument Label, 리턴형이 모두 동일하더라도 호출 방식에 차이가 있어 완전히 구분되기 때문에 문제없이 구현 가능합니다.