ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] 프로토콜
    🍏Swift 2022. 8. 9. 19:09
    오늘 배운 프로토콜에 대해서 상세하게 정리해봤습니다 :)

     

    프로토콜의 개념

    • 프로토콜의 필요성

    클래스와 상속의 단점에는 다중 상속이 불가능, 기본적인 상위 클래스의 필요하지 않은 속성 메서드도 다 상속됨 그리고 클래스에서만 가능하다는 점들이 있는데 프로토콜은 이런 단점들을 보완할 수 있는 것임

    protocol CanFly {
        func fly()     // 구체적인 구현은 하지 않고 자격증을 채택한 곳에서
    }
    
    class Bird1 {
        
        var isFemale = true
        
        func layEgg() {
            if isFemale {
                print("새가 알을 낳는다.")
            }
        }
    
    }
    
    class Eagle1: Bird1, CanFly {    //CanFly 자격증을 채택
        
        // isFemale
        // layEgg()
        
        func fly() {
            print("독수리가 하늘로 날라올라 간다.")
        }
        
        func soar() {
            print("공중으로 활공한다.")
        }
    }
    
    //구조체에서도 채택 가능함
    struct Airplane1: CanFly {
        func fly() {
            print("비행기가 날아간다")
        }
    }
    

     

    문법

    • 기본 문법
    // 1)정의
    protocol MyProtocol {   // 최소한의 요구사항 나열
    	func doing() -> Int
    }
    
    // 2)채택(클래스, 구조체, 열거형에서 채택 가능)
    class Class: MyProtocol{
    
    }
    
    // 3)구현(프로토콜에서 요구하는 사항을 직접구현)
    class Class: MyProtocol{
    	func doing() -> Int{
    		return 7
    	}
    }
    
    //상속이 있는 경우에
    //상속하려는 클래스를 먼저 선언하고 그 다음 채택하려는 프로토콜 선언
    class Person: Student, MyProtocol{ }
    

     

    속성 요구사항 정의

    • 인스턴스 속성 요구사항

    최소한의 요구사항을 지정하고 저장 속성 / 계산 속성으로 모두 구현 가능

    //인스턴스 속성 요구사항
    //{ get } -> 저장 속성: let / var
    //			  -> 계산 속성: 읽기(get) / 읽기쓰기(get/set)
    //{ get set } -> 저장 속성: var
    //            -> 계산 속성: 읽기쓰기(get/set)
    protocol RemoteMouse{
    
    		var id: String { get } //let 저장속성 / var 저장속성 / 읽기계산속성 / 읽기,쓰기 계산속성
    		
    		var name: String { get set } //var 저장속성 / 읽기,쓰기 계산속성
    		
    		static var type: String { get set } //타입 저장 속성 (static)
                                            //타입 계산 속성 (class)
    }
    
    // 채택하면 최소한의 요구사항을 정확하게 따라서 구현해야함
    // 인스턴스 저장/계산 속성
    struct TV: RemoteMouse {
        
        let id: String = "456"
        
        var name: String = "삼성티비"
        
        static var type: String = "리모콘"
    }
    
    let myTV = TV()
    myTV.id
    myTV.name
    TV.type
    

     

    • 타입 속성 요구사항

    최소한의 요구사항 지정하고 채택 시 저장 타입 속성에서 static 키워드로만 구현 가능하고 클래스에서 채택 시 계산 타입 속성에서 static / class 키워드로 모두 구현 가능

    // 타입 속성
    // 1) 저장 타입 속성으로 구현
    class SmartPhone: RemoteMouse {
        var id: String {
            return "777"
        }
        
        var name: String {
            get { "아이폰" }
            set { }
        }
        
        static var type: String = "리모콘"     // 타입 저장 속성은 재정의 원칙적 불가능
    }
    
    // 2) 계산 타입 속성으로 구현
    class Ipad: RemoteMouse {
        var id: String = "777"
        
        var name: String = "아이패드"
        
        class var type: String {       // 클래스에서만 static / class 키워드로 모두 구현 가능(class키워드는 재정의 가능)
            get { "리모콘" }
            set { }
        }
    }
    

     

    메서드 요구사항 정의

    • 메서드 요구사항

    메서드의 헤드부분(인풋/아웃풋)의 형태만 요구사항으로 정의 mutating 키워드: (구조체로 제한하는 것은 아님) 구조체에서 저장 속성 변경하는 경우 구조체도 채택 가능하도록 허락하는 키워드 타입 메서드: 클래스에서 채택 시 static / class 키워드로 모두 구현 가능(class 키워드는 재정의 가능)

    // 1)정의
    protocol RandomNumber {
        static func reset()     //최소한 타입 메서드가 되야함 (class로 구현해서 재정의를 허용하는 것도 가능)
        func random() -> Int
        //mutating func doSomething()
    }
    
    // 2)채택 / 3)구현
    class Number: RandomNumber {
        
        static func reset() {
            print("다시 셋팅")
        }
        
        func random() -> Int {
            return Int.random(in: 1...100)
        }
       
    }
    
    // 1) 정의
    protocol Togglable {
        mutating func toggle()        // mutating의 키워드는 메서드 내에서 속성 변경의 의미일뿐(클래스에서 사용 가능)
    }
    
    // 2) 채택 / 3) 구현
    enum OnOffSwitch: Togglable {
        case on
        case off
        
        mutating func toggle() {
            switch self {   // .on   .off
            case .off:
                self = .on
            case .on:
                self = .off
            }
        }
    }
    
    class BigSwitch: Togglable {
    
        var isOn = false
        
        func toggle() {      // mutating 키워드 필요없음 (클래스 이기 때문)
            isOn = isOn ? false : true
        }
    }
    

     

    • 생성자 요구사항

    클래스에서 생성자 채택시 required를 붙여야 함(하위에서 구현을 강제) 클래스가 final로 선언되면 required 생략 가능 클래스에서는 반드시 지정 생성자로 구현할 필요 없음(편의 생성자로 구현도 가능)

    //예1
    protocol SomeProtocol {     // 생성자를 요구사항으로 지정 가능
        init(num: Int)
    }
    
    class SomeClass: SomeProtocol {
        required init(num: Int) {
            // 실제 구현
        }
    }
    
    class SomeSubClass: SomeClass {
        // 하위 클래스에서 생성자 구현 안하면 필수 생성자는 자동 상속
        // required init(num: Int)
        
    }
    
    //예2
    protocol AProtocol {
        init()
    }
    
    class ASuperClass {
        init() {
            // 생성자의 내용 구현
        }
    }
    
    class ASubClass: ASuperClass, AProtocol {
        // AProtocol을 채택함으로 "required" 키워드 필요하고, 상속으로 인한 "override(재정의)" 재정의 키워드도 필요
        required override init() {
            // 생성자의 내용 구현
        }
    }
    
    // init?() 요구사항 ->  init() / init?() / init!()로 구현가능 
    // init()  요구사항 -> init?() 로 구현불가능(범위가 더 넓어지는 것 안됨)
    //예3
    // 실패가능 생성자
    protocol AProto {
        init?(num: Int)      //(객체의 타입이 맞을까?)  AClass? <- AClass은 범위가 속해있음
    }
    
    // 구조체에서 채택 (required 키워드는 필요없음)
    struct AStruct: AProto {  
        
    //    init?(num: Int) {}    //이것도 가능
        
        init(num: Int)  {}
        
        //init!(num: Int)  {}     // 이것도 괜찮음
    }
    
    // 클래스에서 채택
    class AClass: AProto {
        required init(num: Int) {} //편의생성자로도 구현 가능
    }
    

     

    • 서브 스크립트 요구사항

    최소한의 요구사항을 지정 { get } → 최소한 읽기(get)는 구현 / 읽기 쓰기(get/set) 모두 구현 가능 { get set } → 반드시 읽기 쓰기(get/set) 모두 구현

    protocol DataList {
        subscript(idx: Int) -> Int { get }     // (서브스크립트 문법에서) get 필수 (set 선택)
    }
    
    struct DataStructure: DataList {
        
    //    subscript(idx: Int) -> Int {    // 읽기 전용 서브스크립트로 구현한다면
    //        get {
    //            return 0
    //        }
    //    }
        
        subscript(idx: Int) -> Int {    // (최소한만 따르면 됨)
            get {
                return 0
            }
            set {                 // 구현은 선택
                // 상세구현 생략
            }
        }
    }
    

     

    protocol Certificate {
        func doSomething()
    }
    
    class Person {
        
    }
    
    // 관습적으로 본체보다는 확장에서 채택 구현(코드의 깔끔한 정리 가능하기 때문)
    extension Person: Certificate {
        func doSomething() {
            print("Do something")
        }
    }
    

     

    타입으로서의 프로토콜과 기타 문법

    • 프로토콜은 타입

    프로토콜을 변수에 할당할 수 있음 함수를 호출할때 프로토콜을 파라미터로 전달 가능 함수에서 프로토콜을 반환할 수 있음

    // 프로토콜은 First Class Citizen(일급객체)이기 때문에 타입(형식)으로 사용할 수 있음
    
    protocol Remote {
        func turnOn()
        func turnOff()
    }
    
    class TV: Remote {
        func turnOn() {
            print("TV켜기")
        }
        
        func turnOff() {
            print("TV끄기")
        }
    }
    
    struct SetTopBox: Remote {
        func turnOn() {
            print("셋톱박스켜기")
        }
        
        func turnOff() {
            print("셋톱박스끄기")
        }
        
        func doNetflix() {
            print("넷플릭스 하기")
        }
    }
    
    let tv = TV()
    let sbox = SetTopBox()
    
    //프로토콜 타입 취급의 장점
    let electronic: [Remote] = [tv, sbox]     //프로토콜의 형식으로 담겨있음
    
    for item in electronic {   // 켜기, 끄기 기능만 사용하니 타입캐스팅을 쓸 필요도 없음 (다만, 프로토콜에 있는 멤버만 사용가능)
        item.turnOn()
    }
    
    func turnOnSomeElectronics(item: Remote) {
        item.turnOn()
    }
    
    turnOnSomeElectronics(item: tv)
    turnOnSomeElectronics(item: sbox)
    

     

    • 프로토콜 준수성 검사

    is / as 연산자 사용 가능 is 연산자: 특정 타입이 프로토콜을 채택하고 있는지 확인 (참 또는 거짓) / 그 반대도 확인 가능
    as 연산자: 타입 캐스팅 (특정 인스턴스를 프로토콜로 변환하거나, 프로토콜을 인스턴스 실제 형식으로 캐스팅)

    // 1) is연산자
    
    // 특정타입이 프로토콜을 채택하고 있는지 확인
    tv is Remote
    sbox is Remote
    
    // 프로토콜 타입으로 저장된 인스턴스가 더 구체적인 타입인지 확인 가능
    electronic[0] is TV
    electronic[1] is SetTopBox
    
    // 2) as연산자
    
    // 업캐스팅(as)
    let newBox = sbox as Remote
    newBox.turnOn()
    newBox.turnOff()
    
    // 다운캐스팅(as?/as!)
    let sbox2: SetTopBox? = electronic[1] as? SetTopBox
    sbox2?.doNetflix() //앞에 타입이 옵셔널이라면 ?로 명시해줘야함
    //(electronic[1] as? SetTopBox)?.doNetflix()
    

     

    • 프로토콜의 상속

    프로토콜은 프로토콜 간의 상속도 가능하고 다중 상속도 가능

    protocol Remote {
        func turnOn()
        func turnOff()
    }
    
    protocol AirConRemote {
        func Up()
        func Down()
    }
    
    protocol SuperRemoteProtocol: Remote, AirConRemote {   // 프로토콜끼리, 상속 구조를 만드는 것이 가능 ⭐️
        // func turnOn()
        // func turnOff()
        // func Up()
        // func Down()
        
        func doSomething()
    }
    
    // 프로토콜의 채택 및 구현
    class HomePot: SuperRemoteProtocol {
        func turnOn() { }
        
        func turnOff() { }
        
        func Up() { }
        
        func Down() { }
        
        func doSomething() { }
    }
    

     

    • 클래스 전용 프로토콜

    AnyObject는 클래스 전용 프로토콜로 구조체에서는 사용 불가

    // 클래스 전용 프로토콜로 선언
    // (AnyObject프로토콜을 상속)
    protocol SomeProtocol: AnyObject {     //AnyObject는 클래스 전용 프로토콜
        func doSomething()
    }
    
    // 구조체에서는 채택할 수 없게 됨 
    //struct AStruct: SomeProtocol {
    //
    //}
    
    // 클래스에서만 채택 가능
    class AClass: SomeProtocol {
        func doSomething() {
            print("Do something")
        }
    }
    

     

    • 프로토콜 합성 문법

    프로토콜을 &로 연결해서 프로토콜 두 개를 병합해서 타입으로 사용하는 것 가능

    // 프로토콜을 합성하여 임시타입으로 활용 가능
    protocol Named {
        var name: String { get }
    }
    
    protocol Aged {
        var age: Int { get }
    }
    
    // 하나의 타입에서 여러개의 프로토콜을 채택하는 것 당연히 가능 (다중 상속과 비슷한 역할)
    struct Person: Named, Aged {
        var name: String
        var age: Int
    }
    
    // 프로토콜을 두개를 병합해서 사용 하는 문법(&로 연결)
    func wishHappyBirthday(to celebrator: Named & Aged) {   // 임시적인 타입으로 인식
        print("생일축하해, \\(celebrator.name), 넌 이제 \\(celebrator.age)살이 되었구나!")
    }
    
    let birthdayPerson = Person(name: "홍길동", age: 20)
    wishHappyBirthday(to: birthdayPerson)
    
    let whoIsThis: Named & Aged = birthdayPerson      // 임시적인 타입으로 저장 (두개의 프로토콜을 모두 채택한 타입만 저장 가능)
    

     

    프로토콜 지향 프로그래밍

    💡 여러 개의 프로토콜 채택 가능(다중 상속과 유사)
    💡 메모리 구조에 대한 특정 요구사항 없음(필요한 속성 / 메서드만 채택도 가능)
    💡 모든 타입에서 채택 가능(벨류 타입도 가능)
    💡 확장에서 구체적 정의(재정의 가능)

     

    참고 자료

    Allen’s Swift Master School

     

    꿀팁

    💡 프로토콜의 채택은 관습적으로 확장에서 구현하는 것을 권장함
    💡 프로토콜은 보통 한 폴더에 모아서 관리하고 프로토콜의 파일명은 해당 기능에 맞게 써주는 게 좋음
    💡 프로토콜을 원하는 파일에서 채택할 때 위치는 제일 앞인 상속받는 클래스 이외에는 어디에다 채택을 해줘도 상관없음
    💡 프로토콜을 채택한 것에 따라서 코드가 일관성 있게 짜일 수 있기 때문에 많이 사용함
    💡 inputView는 키보드 대체할 아이를 넣어주는 것

    '🍏Swift' 카테고리의 다른 글

    [Swift] Optional  (0) 2023.02.19
    [Swift] Enumeration  (0) 2023.02.09
    [Swift] Access Control  (0) 2023.02.06
    [Swift] map, filter, reduce 등 고차함수  (0) 2023.02.03
    [Swift] 메서드의 종류  (0) 2022.10.13
Designed by Tistory.