iOS/Swift

[Swift] extension과 상속의 차이

HarryJeonn 2022. 11. 9. 18:07

차이점을 알아보기 전에 공식문서를 살펴보면서 extension은 뭔지 상속은 뭔지 먼저 알아보자.

Extensions

Extensions - The Swift Programming Language (Swift 5.7)

Extension은 기존 클래스, 구조, 열거형 또는 프로토콜에 새 기능을 추가할 수 있다.

원본 소스코드에 액세스할 수 없는 유형을 확장하는 기능이 포함된다.

기능

계산 인스턴스 속성 및 계산 유형 속성 추가

인스턴스 메서드 및 유형 메서드 정의

새 이니셜라이저 제공

첨자 정의

새 중첩 유형 정의 및 사용

기존 형식을 프로토콜에 맞게 만들기

프로토콜을 확장할 수 있다.

extension은 새 기능을 추가할 수 있지만 기존 기능을 재정의할 수는 없다.

예제

extension 선언

extension SomeType {
	// new functionality to add to SomeType goes here
}

protocol 사용

extension SomeType: SomeProtocol, AnotherProtocol {
    // implementation of protocol requirements goes here
}

extension은 하나 이상의 프로토콜을 채택하도록 기존 유형을 확장할 수 있다.

extension을 활용한 거리 계산

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \\(oneInch) meters")
// Prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \\(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"

1.0을 1m로 기준으로 잡고 그 외 단위는 계산을 한다.

예제에서 25.4.mm는 몇 미터인지, 3.ft는 몇 미터 인지 출력했다.

let aMarathon = 42.km + 195.m
print("A marathon is \\(aMarathon) meters long")
// Prints "A marathon is 42195.0 meters long"

42.km과 195.m를 더하면 m기준으로 계산 후 출력한다.

Extensions can add new computed properties, but they can’t add stored properties, or add property observers to existing properties.

새로운 계산 프로퍼티를 추가할 수 있지만, 저장 프로퍼티나 프로퍼티 옵저버를 추가할 수 는 없다고 한다.

Initializers

extention은 기존 유형에 새로 initializers를 추가할 수 있다.

하지만 class에 지정된 새 initializers 혹은 deinitializers를 추가할 수 없다.

class에 지정된 initializers, deinitializers는 항상 원래 class에서 구현해야 한다.

extension에서 initializer를 사용하는 경우 class에서 initializer를 호출하기 전 까지 사용할 수 없다.

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
}

Rect 구조체와 Rect 구조체가 사용하는 Size, Point 구조체를 정의했다.

모든 속성에 기본값을 정의했다.

let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
   size: Size(width: 5.0, height: 5.0))

기본값을 정의했기 때문에 기본 Rect와 값을 주는 Rect를 만들 수 있다.

extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

initializer를 새로 만들어서 값을 주는 Rect를 생성할 때 제공된 Point와 Size 값으로 계산을 한다.

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

계산된 Point와 생성할 때 입력한 Size 값을 저장한다.

If you provide a new initializer with an extension, you are still responsible for making sure that each instance is fully initialized once the initializer completes.

extension에서 새 initializer를 사용하는 경우 initializer가 완료되면 각 인스턴스가 완전히 초기화되었는지 확인해야 한다고 한다.

Methods

extension은 새 메서드를 추가할 수 있다.

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

위 코드는 입력된 Int값 만큼 반복하는 함수이다.

3.repetitions {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!

Mutating Instance Methods

extension으로 인스턴스 자체를 수정할 수 있다.

extension Int {
    mutating func square() {
        self = self * self
    }
}
var someInt = 3
someInt.square()
// someInt is now 9

원래 값을 제곱하는 함수를 extension으로 구현했다.

나 자신을 변경하기 위해 self키워드를와 mutating을 사용했다.

Subscript

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7

Subscript는 배열에서 index값으로 값을 찾는 것, Dictionary에서 key값으로 값을 찾는 것 등을 의미한다.

그런데 Int 타입에서 index로 값을 찾고싶다면?

그 코드가 위에 구현되어있다.

Int 타입 뿐만 아니라 String타입도 가능하다.

extension String {
    subscript(idx: Int) -> String? {
        guard (0..<count).contains(idx) else {
            return nil
        }
        let target = index(startIndex, offsetBy: idx)
        return String(self[target])
    }
}

let harry = "Hello, Harry!"
sodeul[0]           // Optional("H")
sodeul[100]         // nil

Nested Types

기존 Class, Struct, enum 타입에도 새 nested type을 구현할 수 있다.

Nested Types은 중첩 타입으로 예를 들어 Struct안에 enum을 구현할 수 있는 것이다.

extension Int {
    enum Kind {
        case negative, zero, positive
    }
    var kind: Kind {
        switch self {
        case 0:
            return .zero
        case let x where x > 0:
            return .positive
        default:
            return .negative
        }
    }
}

음수, 0, 양수를 구별하는 extension 예제이다.

func printIntegerKinds(_ numbers: [Int]) {
    for number in numbers {
        switch number.kind {
        case .negative:
            print("- ", terminator: "")
        case .zero:
            print("0 ", terminator: "")
        case .positive:
            print("+ ", terminator: "")
        }
    }
    print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// Prints "+ + - 0 - 0 + "

위에서 extension을 활용하여 구현한 것을 기반으로 함수를 구현했다.

Int 배열을 받아서 요소별로 number.kind를 사용하여 음수, 0, 양수를 구분하여 출력했다.

Ingeritance(상속)

Inheritance - The Swift Programming Language (Swift 5.7)

클래스는 다른 클래스의 methods, properties, 기타 특성을 상속할 수 있다.

상속하는 클래스를 하위 클래스, 상속받은 클래슬르 상위 클래스라고 한다.

class Vehicle {
    var currentSpeed = 0.0
    var description: String {
        return "traveling at \\(currentSpeed) miles per hour"
    }
    func makeNoise() {
        // do nothing - an arbitrary vehicle doesn't necessarily make a noise
    }
}

Vehicle이라는 기본 클래스를 정의한다.

currentSpeed의 기본 값은 0.0, 차량 설명을 하기위한 description, makeNoise 메소드가 있다.

makeNoise 메소드는 아무 동작도 없지만 하위 클래스에서 새로 정의하여 사용한다.

let someVehicle = Vehicle()

print("Vehicle: \\(someVehicle.description)")
// Vehicle: traveling at 0.0 miles per hour

새 Vehicle 클래스를 정의한 후 그 클래스의 description을 출력했다.

기본 값인 0.0을 출력한다.

Subclassing

class SomeSubclass: SomeSuperclass {
    // subclass definition goes here
}

subclassing은 기존 클래스를 기반으로 새 클래스를 만드는 행위이다.

하위 클래스는 기존 클래스의 특성을 상속하므로 이를 구체화할 수 있다.

콜론으로 구분하여 상위 클래스 이름 앞에 하위 클래스 이름을 작성한다.

class Bicycle: Vehicle {
    var hasBasket = false
}

새 클래스인 Bicycle은 Vehicle에 대한 특성을 모두 얻는다.

// 속성 변경
let bicycle = Bicycle()
bicycle.hasBasket = true

// 상위 클래스에서 구현된 내용 사용
bicycle.currentSpeed = 15.0
print("Bicycle: \\(bicycle.description)")
// Bicycle: traveling at 15.0 miles per hour

클래스를 생성한 후 클래스에 대한 속성을 변경할 수 있다.

Vehicle을 상속 받았기 때문에 Vehicle에서 구현한 내용을 사용할 수 있다.

class Tandem: Bicycle {
    var currentNumberOfPassengers = 0
}

Vehicle을 상속한 Bicycle 클래스를 다른 클래스가 상속 받을 수 있다.

let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Tandem: \\(tandem.description)")
// Tandem: traveling at 22.0 miles per hour

Bicycle을 상속한 Tandem 클래스는 Bicycle과 Vehicle 모두의 특성을 물려 받는다.

Overriding

하위 클래스는 상위 클래스에서 정의된 내용을 재정의(Override)할 수 있다.

이때 override키워드를 붙여야 한다. 그렇지 않으면 컴파일 에러가 발생한다.

상위 클래스의 메소드, 속성, 첨자 사용

override func someMethod() {
	super.someMethod()
}

하위 클래스에서 상위 클래스의 무언가를 재정의 할 때 상위 클래스에서 이루어지는 동작이 유용할 때가 있다.

그럴 때 상위 클래스의 구현된 동작을 사용하려면 super를 사용한다.

Overriding Method

class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo")
    }
}

let train = Train()
train.makeNoise()
// Prints "Choo Choo"

Vehicle 클래스에서 구현된 makeNoise()를 재정의하여 사용하는 예제이다.

Vehicle 클래스에서는 아무런 동작이 없는 메소드였지만 Train 클래스에서 재정의 한 것을 볼 수 있다.

Overriding Getter, Setter

override 할 때 동일한 이름 및 타입을 가진 상위 클래스의 속성과 일치하는지 컴파일러가 확인할 수 있도록 이름과 타입을 모두 명시해야한다.

class Car: Vehicle {
    var gear = 1
    override var description: String {
        return super.description + " in gear \\(gear)"
    }
}

let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Car: \\(car.description)")
// Car: traveling at 25.0 miles per hour in gear 3

Vehicle 클래스에서 속도만 설명해주던 description을 기어까지 넣어서 반환한다.

여기서 super 키워드를 사용하여 상위 클래스에서 사용된 내용 + 기어를 추가한 것을 볼 수 있다.

Overriding Property Observers

property 재정의를 통하여 property observer를 추가할 수 있다.

이렇게 하면 상속된 property값이 변경될 때 알림을 받을 수 있다.

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10.0) + 1
        }
    }
}

let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("AutomaticCar: \\(automatic.description)")
// AutomaticCar: traveling at 35.0 miles per hour in gear 4

Car 클래스를 상속받아서 currentSpeed를 override하며 값이 바뀔 때 마다 동작하는 didSet을 구현했다.

속도가 바뀔 때 마다 기어 값도 변경된다.

Preventing Overrides

final을 사용하여 override를 방지할 수 있다.

하위 클래스의 최종 메소드, 프로퍼티, 서브스크립트를 override 하려는 모든 시도는 컴파일 에러가 난다.

그래서 상속과 extension의 차이점은?

상속은 상위 클래스의 모든 특성을 받아들여 하나의 새로운 클래스를 만들어서 재정의(Override)도 할 수있고, 상위 클래스의 자식 클래스가 된다.

extension은 해당 클래스에서 내가 필요한 부분만 새로 정의하거나 기능을 추가하여 사용하는 것이다.

정리하자면 가장 큰 차이점은 아래와 같이 생각한다.

  1. 상속은 수직적으로 확장하고 extension은 수평적으로 확장한다.
  2. 새로운 클래스를 정의하는 것(상속)과 기존 타입에 기능을 추가하는 것(extension)의 차이가 있다.
  3. 상속은 클래스에서 사용이 가능하고, extension은 모든 타입에서 사용 가능하다.
  4. 상속은 override 가능, extension은 불가능
  상속 extension
확장 수직적 수평적
정의 새로운 클래스를 정의 기존 타입에 추가
사용 클래스에서 사용 가능 클래스, 구조체, 프로토콜, 열거형 등
모든 타입에서 사용 가능
재정의(override) 가능 불가

🧐

셀 수도없이 사용했던 상속과 extension이다.

대학교에서도 수업을 들으면 Swift를 가르치지는 않지만 상속은 빠지지 않는 중요한 개념이였다.

하지만 직접 공식문서를 까본적은 없었다. 이미 질리도록 들었다고 생각했기 때문이다.

까보니 알고있었던 것들도 당연히 있었지만 몰랐던 것, 아 이게 그래서? 생각했던 부분이 있었다.

역시.. 알고 써야한다.

공식문서 짱짱