형식을 확장하는 Extension에 대해 알아보겠습니다. 이 확장의 대상으로는 클래스, 구조체, 열거형, 프로토콜이 있습니다. 별도의 코드 블록을 붙이는 형식으로 구성합니다.
확장으로 멤버를 추가 하는 것은 가능하지만, 기존 멤버를 오버라이딩 하는 것은 불가능 합니다.
// 문법
// 저장 속성이나 프로퍼티 옵저버는 추가가 불가하다.
// 간편 생상자는 구현이 가능 나머지 생성자는 x
// 생성자 위임이 정상적으로 실행되도록 구현하는 것이 중요
extension Type {
computedProperty
computedTypeProperty
...
}
extension Type: Protocol, ... {
requirements
}
// Size 구조체에 비교 연산을 추가해보겠습니다.
extension Size: Equatable {
public static func == (lhs: Size, rhs: Size) -> Bool {
return lhs.width == rhs.width && lhs.height == rhs.height
}
}
저장 속성과 프로퍼티 옵저버, 기존 형식에 존재하는 속성을 오버라이딩은 불가능하지만, 계산속성은 확장이 가능하다.
// Date 형식에 년도를 리턴하는 속성을 추가해보겠습니다
extension Date {
var year: Int {
let cal = Calendar.current
// Date 형식 내부에서 날짜 속성에 접근시에는 self로 접근한다
return cal.component(.year, from: self)
}
}
// Double 형식에 섭씨/화씨 온도 변환 메소드 추가
extension Double {
func toFahrenheit() -> Double {
return self * 9 / 5 + 32
}
func toCelsius() -> Double {
return (self - 32) * 5 / 9
}
// 유지보수 시에 구현한 인스턴스 메소드만 변경하면 되므로, 더 효율적인 코드이다.
static func converToFaherenheit(from: celsius: Double) -> Double {
return celsius.toFahrenheit()
}
}
// UIColor 클래스에 RGB 파라미터를 받는 생성자 추가
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
}
}
// 파라미터들의 범위를 검증 하는 코드 추가 버전
// assert는 else라면 종료시키고 메시지를 띄워준다. 프로그램에 영향을 끼치지 않는 장점 !
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
assert(red >= 0 && red <= 255, "Invalid red component")
assert(green >= 0 && green <= 255, "Invalid green component")
assert(blue >= 0 && blue <= 255, "Invalid blue component")
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
}
}
// 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])
}
}
이는 단순하게 설명하자면, 형식에서 공통적으로 제공하는 멤버 목록 입니다. 프로토콜을 채용한 형식은 프로토콜의 필수 멤버들을 구현해야만 한다. 이것들을 요구사항(Requirements)이라고 부르기도 한다.
// 문법
protocol ProtocolName {
propertyRequirements
methodRequirements
initializerRequirments
subscriptRequirements
}
protocol ProtocolName: Protocol, ... {}
protocol Something() {
// 이 프로토콜을 채용하면 아래 함수를 구현해야만 하고, 프로토콜을 따른다고도 명시해야 한다
func doSomething()
}
// 클래스에만 프로토콜을 적용하는 방법
// 해당 프로토콜은 클래스에서만 채용할 수 있다.
protocol ProtoclName: AnyObject {
}
// 프로퍼티 선언 문법
protocol ProtocolName {
// var로만 선언한다
// set이 없다면 이 프로토콜을 채용한 곳에서는 읽기전용 속성을 선언하거나,
// 읽기 쓰기 모두 가능한 속성으로 선언해도 문제가 없다.
var name: Type {get set}
static var name: Type {get set}
}
// 예시 코드
protocol Figure {
// 읽기와 쓰기가 가능해야한다 따라서 let = name = "hi" 는 에러
var name: String {get set}
// 형식 속성을 따라야 하는 경우
// 이 프로토콜을 채용한 곳에서는 해당 변수에 static을 선언해야 한다
// class인 경우 class 키워드로 선언하여도 문제는 없다.
static var name: String {get set}
}
struct Circle: Figure {
var name: String {
get {
return "Circle"
}
set {
...
}
}
}
// 문제 x
class Circle: Figure {
class var name: String {
get {
}
set {
}
}
}
프로토콜에서 메소드를 선언할 때는 메소드의 헤드부분만 선언하고 바디는 필요없다.
// 문법
protocol ProtocolName {
func name(param) -> ReturnType
// 타입 메소드
static func name(param) -> ReturnType
// 값 형식에서 채용할 수 있고, 메소드 안에서 속성값을 변경해야 한다면 mutating 키워드 사용
mutating func name(param) -> ReturnType
}
// 문법
protocol ProtocolName {
init(param)
init?(param)
init!(param)
}
class Circle: Figure {
var name: String
// 클래스는 상속을 고려해야하고, 모든 서브클래스에서
// 프로토콜의 요구 사항을 충족시켜야 하기 때문에 required 키워드를 추가 하여야한다.
// 또는 final 클래스로 선언하면 상속을 하지 않으므로 error가 없다
required init(n: String) {
name = n
}
}
프로토콜에도 서브스크립트 구문을 추가할 수 있다.
// 문법
protocol ProtocolName {
subscript(param) -> ReturnType {get set}
}
프로토콜도 1급 객체 입니다. 따라서 독립적인 형식이므로 파라미터, 리턴형 등으로 사용이 가능합니다.
protocol Resettable {
func reset()
}
class Size: Resettable {
var width = 0.0
var height = 0.0
func reset() {
width = 0.0
height = 0.0
}
}
// 업캐스팅과 유사하고, 프로토콜에서 선언된 것들만 사용이 가능하다.ㅋ₩
let r: Resettable = Size()
프로토콜 적합성은 타입 캐스팅 연산자를 사용해서 확인합니다.
// 문법
instance is ProtocolName
instance as ProtocolName
instance as? ProtocolName
instance as! ProtocolNam
let r = Size() as Resettable // Size() 인스턴스이지만 r의 타입은 프로토콜 형식을 따른다
프로토콜을 사용하면 상속과 비슷한 패턴도 구현이 가능하다.
protocol Figure {
...
}
// 위 프로토콜을 채택하는 것들은 아래와 같이 하나에 다 저장할 수 있다.
// Figure 형식으로 캐스팅하여 저장한다
// 하지만 Figure 프로토콜의 멤버들에만 접근이 가능하다
let list: [Figure] = [t, r, c]
// 위의 Protocol 멤버 말고 접근을 하려면
// 원래 형식으로 캐스팅 해서 접근해야 한다
for item in list {
if let c = item as? Circle {
c.radius
}
}
여러 프로토콜을 병합하는 것에 대해서 알아보겠습니다.
// 문법
protocol & protocol
class & protocol
// 두 프로토콜을 모두 충족시키는 임시 형식으로 선언된다
// Size()는 해당 프로토콜 전부 충족해야한다.
var rpp: Resettable & Printable = Size()
// 모든 서브클래스를 저장할 수 있게 해준다.
var rp: Circle & Resttable = Circle()
선택적 요구사항에 대해서 알아보겠습니다. 여기서 옵셔널은 선택적이라는 의미입니다.
// 문법
// @objc를 붙인 멤버들은 옵셔널 형식이다. 따라서 호출하려면 옵셔널 체이닝을 활용해야한다
@objc protocol ProtocolName {
@objc optional requirements
}
// 이는 AnyObject를 상속하므로 Class에서만 사용이 가능하다.
프로토콜에 멤버들을 필수로 구현하지 않아도 되게끔 만들어준다
프로토콜의 확장은, 이 프로토콜을 채용한 형식에 구현을 추가하는것과 같다. 코드의 양을 대폭 줄일 수 있다.
protocol Figure {
var name: String { get }
func draw()
}
extension Figure {
func draw() {
print("draw figure")
}
}
// 확장을 하였으므로, draw가 구현되어 있다고 본다
// 형식에서 똑같은 이름 파라미터로 새로 구현하면 더 높은 우선순위를 가지므로,
// 프로토콜 확장에서 구현한 메소드는 무시가 된다.
struct Rectangle: Figure {
var name = ""
}
// 조건을 부여해서 확장하는 방법
// 여기서 Self 는 프로토콜을 채용하는 형식을 의미한다
extension Figure where Self: Equatable {
func draw {
print("draw figure")
}
}
// 이는, Equatable을 만족하는 형식에 이 프로토콜 확장을 적용시키므로 위의 구조체는 해당이 안된다
// 따라서 draw를 호출할 수 없다.
매우 중요한 Equatable 프로토콜에 대해서 알아보겠습니다.
값의 동일성을 비교할 수 있는 타입이라면 반드시 구현해야 한다
연관 값이 선언되지 않은 열거형은 Equatable 구현이 자동으로 추가되고, 모든 연관 값의 형식이 Equatable을 구현한 형식인 경우에도 자동으로 추가한다. 연관 값이 선언되어 있다면 이 프로토콜을 채용하는것 만으로 컴파일러가 나머지를 알아서 구현 해준다.
구조체에 선언된 속성들의 형식이 Equtable을 따르는(Int, String...)형식이라면 Equtable을 추가하는 것으로 비교가 가능하다.
// 클래스에서 == 연산자 구현
extension Person {
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
Equtable을 구현할 때 주의점은 모든 속성을 비교해야 한다는 점이 있다
해쉬란 단방향 암호 기법이고, 어떤 값을 고정된 길이의 문자열로 바꾸거나 고유한 정수값으로 바꾸는 것이다. 이런 행위를 해싱이라고 한다. 사용자 인증 등에 주로 사용합니다.
이 프로토콜을 채용하면 값의 유효성, 검색 속도의 향상의 장점이 있다.
자동으로 구현되는 조건에 대해서 알아보겠습니다. 먼저, 열거형 선언에 연관값이 포함되어 있지 않다면, 자동으로 구현된다. 이 때는 선언도 필요 x. 연관값이 포함되어 있는 경우에는, 선언을 해야 한다.
구조체의 선언된 속성들이 기본 형식이면 Hashable을 자동으로 구현해준다 이 때는 선언해줘야함.
클래스는 직접 구현을 해야한다. Hashable은 Equatable을 상속하므로 == 연산자도 구현 해줘야함.
extension Person: Hashable {
// == 구현 생략
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(age)
}
}
이는 값의 크기를 비교하거나 정렬할 떄 필수로 구현해야하는 프로토콜입니다.
열거형 선언에서 연관값이 없다면 프로토콜을 채용한다고 선언만 하면 자동으로 구현이 된다. 내부 형식이 위 프로토콜을 구현하고 있는 경우에도 선언만 하면 된다.
이 프로토콜도 Equatable을 상속하고 있다.
// 직접 구현
extension Weekdat: Comparable {
static func < (lhs: Weekday, rhs: Weekday) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}