티스토리 뷰

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, 리턴형이 모두 동일하더라도 호출 방식에 차이가 있어 완전히 구분되기 때문에 문제없이 구현 가능합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함