궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [익스텐션 - Extensions]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님의 Swift 프로그래밍 3판을 참고하여 작성되었습니다.

 

익스텐션

  • 클래스, 구조체, 열거형, 프로토콜 타입 등에 새로운 기능을 추가하여 확장할 수 있는 기능
  • 기능을 추가하려는 타입을 구현한 소스 코드를 알지 못하거나 볼 수 없어도, 타입만 알면 기능 확장 가능
  • 익스텐션은 타입에 새로운 기능을 추가할 수 있지만, 기존에 존재하는 기능을 재정의(override) 불가
  • 소유하지 않은 유형 (예 : Apple 또는 다른 사람이 작성한 유형)을 수정하는 데 도움이 됨
  • 외부에서 가져온 프레임워크나 라이브러리에 내가 원하는 기능을 추가하고자 할 때 유용
  • 상속을 받지 않아도 구조체와 열거형에도 기능 추가 가능 하므로 매우 편리함
  • 익스텐션은 모든 타입에 적용할 수 있다.(구조체, 열거형, 클래스, 프로토콜, 제네릭 타입 등)

익스텐션이 타입에 추가할 수 있는 기능

  • 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
  • 타입 메서드 / 인스턴스 메서드
  • 이니셜라이저
  • 서브스크립트
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가

클래스의 상속과 익스텐션 차이

  • Class - 클래스의 상속은 클래스 타입에서만 가능. 특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가 기능 구현하는 수직 확장
  • Extension - 구조체, 클래스, 프로토콜 등에 적용 가능. 기존 타입에 기능을 추가하는 수평 확장
  상속  익스텐션
확장 수직 확장 수평 확장
사용 클래스 타입에서만 사용 클래스, 구조체, 프로토콜, 제네릭 등 모든 타입에서 사용
재정의 재정의 가능 재정의 불가

익스텐션 문법

이 기능을 사용하려면 extension 이라는 키워드를 사용해야 한다.

기본 형태

extension 확장할 타입 이름 {
     //기능 구현
}

익스텐션은 기존에 존재하는 타입이 추가로 다른 프로토콜을 채택할 수 있도록 확장 가능한데, 이경우 클래스나 구조체에서 사용하던 것처럼 같은 방법으로 프로토콜 이름을 나열하면 된다.

extension 확장할 타입 이름: 프로토콜 1, 프로토콜 2, 프로토콜 3 {
     // 프로토콜 요구사항
}

Swift 표준 라이브러리 타입(String, Double, Int 등)의 기능은 대부분 익스텐션으로 구현되어있다.

익스텐션으로 추가할 수 있는 기능

연산 프로퍼티

익스텐션을 통해 타입에 연산 프로퍼티를 추가할 수 있다.

// Int 타입 인스턴스가 짝수인지 홀수인지 판별하는 연산 프로퍼티
extension Int {
    var isEven: Bool {
       return self % 2 == 0
    }
    var isOdd: Bool {
       return self % 2 == 1
    }
}

print(3.isOdd) // true
print(1.isEven) //false
print(1.isOdd) // true

var number: Int = 5
print(number.isEven) // false
print(number.isOdd) // true

numver = 2
print(number.isOdd) // false
print(number.isEven) // true

Int 타입의 익스텐션에  짝수인지 홀수인지 판별하여 true / false를 반환하게 구현을 했다. 호출을 할 때는 print(Int.isOdd)처럼 사용하면 된다.

 

또, static 키워드를 사용하여 연산 타입 프로퍼티도 추가 가능하다.

TIP
 
 

익스텐션으로 연산 프로퍼티는 추가 가능하지만, 저장 프로퍼티는 추가 불가하다. 또, 타입에 정의되어 있는 기존의 프로퍼티에 프로퍼티 옵저버를추가할 수도 없다.

메서드

익스텐션을 통해 타입에 메서드를 추가할 수 있다. 다음 예제는 Int 타입에 repetitions라는 인스턴스 메서드를 추가한 예제이다.

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

repetitions(task:) 메서드는 () -> Void 타입의 하나의 인자를 받고 파라미터와 반환 값이 없는 함수이다.

3.repetitions {
    print("서근 개발노트!")
}
// 서근 개발노트!
// 서근 개발노트!
// 서근 개발노트!

위 함수를 실행하면 함수 안의 task를 숫자만큼 반복 실행하게 된다.

이니셜라이저

타입의 정의 부분에 이니셜라이저를 추가하지 않아도 익스텐션을 통해 이니셜라이저를 추가할 수 있다.

 

익스텐션에서 이니셜라이저를 추가할 때의 조건

  • 클래스 타입에 편의 이니셜 라이저는 추가할 수 있지만, 지정 이니셜라이저는 추가할 수 없다.
  • 값 타입에서는 편의 및 지정 이니셜 라이저를 추가할 수 있다.
  • 지정 이니셜라이저와 디이니셜라이저는 반드시 클래스 타입의 구현부에 위치해야 한다.(값 타입 상관없음)
extension String {
    init(intTypeNumber: Int) {
        self = "\(intTypeNumber)"
    }
    init(doubleTypeNumber: Double) {
        self = "\(doubleTypeNumber)"
    }
}

let stringFromInt: String = String(intTypeNumber: 20000) // "20000"
let stringFromDouble: String = String(doubleTypeNumber: 3.14159) // "3.14159"

위 예제는 String 타입의 익스텐션을 정의하고 이니셜라이저로 Int 또는 double 타입을 String으로 변환하는 코드이다.

 

그리고 클래스 타입에서도 편의 이니셜라이저를 다음과 같이 추가 가능하다.

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

extension Person {
    // Person 클래스의 편의 이니셜라이저
    convenience init() {
        // Person의 지정 이니셜라이저를 호출하여 매개변수에 "Anonymous" 값 전달
        self.init(name: "Anonymous")
    }
}

let someOne: Person = Person()
print(someOne.name) // "Anonymous"

익스텐션으로 값 타입에 이니셜라이저를 추가했을때, 해당 값 타입이 아래 조건을 모두 성립하면 익스텐션으로 사용자정의 이니셜라이저를 추가한 이후에도 해당 타입의 기본 이니셜라이저와 멤버와이즈 이니셜라이저 호출 가능

  • 그 값 타입의 모든 저장 프로퍼티가 기본값을 가지고 있어야 함
  • 타입에 기본 이니셜라이저와 멤버와이즈 이니셜라이저 외에 추가 사용자 정의 이니셜라이저가 없어야 함

익스텐션을 통해 추가하는 이니셜라이저는 타입의 기존 이니셜라이저가 갖는 책임과 의무를 동일하게 수행해야 한다.

struct Rect {
    var x: CGFloat = 0
    var y: CGFloat = 0
}

extention Rect {
    init(width: CGFloat, height: CGFloat) {
     //구현
     self.init(x: 9, y: 8)
  }
}

좀 더 복잡한 예시를 들어보자면

 

아래 SizePoint 구조체는 기본값을 가졌고, 사용자 정의 이니셜라이저를 구현하지 않았기 때문에 기본 이니셜라이저와 멤버와이즈 이니셔랄이저를 사용할 수 있다. 때문에 익스텐션에서 추가해주는 새로운 이니셜라이저는 멤버와이즈 이니셜라이저에게 초기화를 위임할 수 있다.

struct Size {
    var width: Double = 0.0
    var height: Double = 0.0
}

struct Point {
    var x: Double = 0.0
    var y: Double = 0.0
}

struct Rect {
    var origin: Point = Point()
    var size: Size = Size()
}

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

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

let centerRect: Rect = Rect(center: Point(x: 3.0, y: 3.0),
                            size: Size(width: 100, height: 200))

잘못된 예시

extension Int {
	var isEven {
		return self % 2 == 0
	}
}
//isEven에 어노테이션 타입이 없다.
extension Bool {
	func toggled() -> Bool {
		if self = true {
			return false
		} else {
			return true
		}
	}
}
//if self == true 로 써야 한다.

서브스크립트

 

익스텐션을 통해 타입에 서브스크립트를 추가할 수 있다.

extension String {
    subscript(appendValue: String) -> String {
        return self + appendValue
    }
    
    subscript(repeatCount: UInt) -> String {
        var str: String = ""
        
        for _ in 0..<repeatCount {
            str += self
        }
        return str
    }
}

print("서근"["개발노트"]) // "서근개발노트"
print("서근개발노트"[2]) // "서근개발노트서근개발노트"

또 다른 예제를 보자면..

Swift의 built-in 타입에 integer 서브스크립트를 추가한 예제이다. 서브스크립트 [n]은 숫자의 오른쪽에서부터 n번째 위치하는 정수를 반환한다.

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
        // 10 * n번째 수로 현재 수를 나눈 것의 나머지
      // 1인 경우 746381295 % 10 -> 5가 나머지
      // 2인 경우 746381295 % 10 -> 9가 나머지
    }
}

746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7

만약 Int값에서 요청한 값이 처리할 수 있는 자릿수를 넘어가면 서브스크립트 구현에서 0을 반환한다.

746381295[9]
// 9로 처리할 수 있는 자릿 수를 넘어가면 0을 반환
0746381295[9]

중첩 데이터 타입

익스텐션을 통해 타입에 중첩 데이터 타입을 추가할 수 있다.

 

아래 예제에 대해 설명을 해보자면

  1. 익스텐션을 통해 Int 타입에 Kind 열거형 타입과 Kind 타입의 연산 프로퍼티를 추가
  2. kind 프로퍼티는 인스턴스가 양수인지 음수인지 Zero 인지를 판단해 Kind에 반환하는 연산 프로퍼티
  3. printIntegerKinds(number:) 함수는 Int 타입의 값의 배열을 전달받아서 각 값의 부호를 print() 함수를 통해 출력하는 함수
  4. print() 함수의 매개변수 중 terminator를 사용한 이유는 줄 바꿈을 하지 않기 위해 기본적으로 줄바꿈 문자로 지정되어 있는 terminator에 빈 문자열 "" 을 전달해준 것이다.
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
        }
    }
}

print(1.kind) //positive
print(0.kind) //zero
print((-1).kind) //negative

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(numbers: [3, 20, -2, -31, 0, 2])
//+ + - - 0 +

TIP
 
 

.number.kind가 이미 switch에서 Int.Kind 타입이라는 것을 알고 있기 때문에 안의 case에서 kind의 축약형인 .negative, .zeo, .positive로 사용할 수 있다.

 

 

읽어주셔서 감사합니다🤟

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.