티스토리 뷰
INDEX
1. 프로토콜(Protocol)이란?
2. 프로토콜의 요구사항(Protocol Requirements)
3. 타입으로서의 프로토콜(Protocols as Types)
4. 기타 프로토콜 문법들
5. 마무리
1. 프로토콜(Protocol)이란?
프로토콜 (Protocols)
클래스의 상속의 단점을 보완하는 자격증의 개념
어떤 클래스, 구조체, 열거형에서나 채택가능하며
최소한의 요구사항만을 구현하면 해당 프로토콜의 기능을 사용가능
// 정의
protocol SomeProtocol { // 요구사항을 정의 (자격증의 필수 능력만 정의)
func playPiano() //내부구현은 하지 않는다!
}
// 채택 및 구현
// 구조체에서 채택
struct MyStruct: SomeProtocol { // 이제 프로토콜의 능력이 생긴 것
func playPiano() {
// 구체적인 구현
}
}
// 클래스에서 채택
class MyClass: SomeProtocol { // 이제 프로토콜의 능력이 생긴 것
func playPiano() {
// 구체적인 구현
}
}
상속의 단점 - 원하지 않는 기능까지 강제로 받게 됨
class Bird {
var isFemale = true
func layEgg() {
if isFemale {
print("새가 알을 낳는다.")
}
}
func fly() {
print("새가 하늘로 날아간다.")
}
}
class Eagle: Bird {
// isFamale
// layEgg()
// fly()
func soar() {
print("공중으로 치솟아 난다.")
}
}
class Penguin: Bird {
// isFamale
// layEgg()
// fly() // 상속 구조에서는 펭귄이 어쩔 수 없이 날개됨 ⭐️
func swim() {
print("헤엄친다.")
}
}
// struct가 될 수도 없고(클래스로만 구현가능), 무조건 Bird를 상속해야만 함
class Airplane: Bird {
// isFamale
// layEgg() // 상속 구조에서는 비행기가 알을 낳게됨 ⭐️
override func fly() {
print("비행기가 엔진을 사용해서 날아간다")
}
}
// 플라잉 박물관을 만듦
struct FlyingMuseum {
func flyingDemo(flyingObject: Bird) {
flyingObject.fly()
}
}
let myEagle = Eagle()
myEagle.fly()
myEagle.layEgg()
myEagle.soar()
let myPenguin = Penguin()
myPenguin.layEgg()
myPenguin.swim()
myPenguin.fly() // 문제 ===> 펭귄이 날개 됨(무조건적인 멤버 상속의 단점)
let myPlane = Airplane()
myPlane.fly()
myPlane.layEgg() // 문제 ===> 비행기가 알을 낳음
let museum = FlyingMuseum() // 타입 정의 ===> 오직 Bird 클래스 밖에 안됨
museum.flyingDemo(flyingObject: myEagle)
museum.flyingDemo(flyingObject: myPenguin)
museum.flyingDemo(flyingObject: myPlane) // Bird를 상속해야만 사용 가능
프로토콜은 위와 같은 상속의 단점을 보완하며, 하나의 타입으로 인식가능
//프로토콜은 위와 같은 상속의 단점을 보완한다
// "fly()"라는 기능을 따로 분리해 내기
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("공중으로 활공한다.")
}
}
class Penguin1: Bird1 {
// isFemale
// layEgg()
func swim() {
print("물 속을 헤엄칠 수 있다.")
}
}
// 구조체에서 채택도 가능
struct Airplane1: CanFly {
func fly() {
print("비행기가 날아간다")
}
}
//프로토콜도 타입으로 인식가능하다.
// 박물관을 만듦
struct FlyingMuseum1 {
func flyingDemo(flyingObject: CanFly) { //프로토콜을 타입으로 인식
flyingObject.fly()
}
}
let myEagle1 = Eagle1()
myEagle1.fly()
myEagle1.layEgg()
myEagle1.soar()
let myPenguin1 = Penguin1()
myPenguin1.layEgg()
myPenguin1.swim()
//myPenguin1.fly() // 더이상 펭귄이 날지 않음
let myPlane1 = Airplane1()
//myPlane1.layEgg() // 더이상 비행기가 알을 낳지 않음
myPlane1.fly()
//Canfly 프로토콜을 채택한 타입만 받는다
let museum1 = FlyingMuseum1()
museum1.flyingDemo(flyingObject: myEagle1)
//museum1.flyingDemo(flyingObject: myPenguin1) // 더이상 "CanFly"자격증이 없는 펭귄은 날지 못함
museum1.flyingDemo(flyingObject: myPlane1)
https://www.udemy.com/course/ios-13-app-development-bootcamp/
클래스에서 상속이 있고, 프로토콜도 채택하는 경우(클래스 - 프로토콜 순서로 선언)
protocol MyProtocol { // 최소한의 요구사항 나열
}
class FamilyClass { }
// 2) 채택 (클래스, 구조체, 열거형 다 가능)
class MyClass: FamilyClass, MyProtocol { // 상위클래스인 FamilyClass를 먼저 선언❗️
// 3) (속성/메서드) 구체적인 구현
}
2. 프로토콜의 요구사항(Protocol Requirements)
프로토콜은 자신을 채택한 타입이 '어떤 프로퍼티를 구현해야 하는지 요구'할 수 있다.
1. 속성 요구사항(Property Requirements)
2. 메서드 요구사항(Method Requirements)
3. 생성자 요구사항(Initializer Requirements)
4. 서브스크립트 요구사항(Subscript Requirements)
1. 속성 요구사항(Property Requirements)
1.
속성이기에 var로 선언
2. get, set 키워드로 읽기/쓰기 여부 설정 (최소한의 요구사항일뿐)
3. 저장 속성/계산 속성으로 모두 구현 가능
저장속성 및 계산속성
get키워드만 사용해 read-only로 프로토콜에서 정의시,
채택하는 쪽에서 get만 사용해도되고, get과 set키워드 둘다 사용해도됨
But❗️ 프로토콜에서 { get,set } 키워드로 읽기/쓰기를 동시에 설정했다면
채택하는쪽의 계산속성은 get,set을 필수 구현해줘야함(저장속성은 var로 선언해주면됨 => 똑같이 읽기 쓰기 가능)
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 //getter만 있는 저장속성일 경우 let선언 가능
var id: String = "456"
//아래처럼 구현해줘도됨(read-only는 getter,setter모두 가능)
// var id: String {
// get { return "456" }
// set { self.name = newValue }
// }
var name: String = "삼성티비"
static var type: String = "리모콘"
}
let myTV = TV()
myTV.id //456
myTV.name //삼성티비
TV.type //리모콘
// 타입 속성 ===================================
// 1) 저장 타입 속성(read-only)으로 구현
class SmartPhone: RemoteMouse {
var id: String {
return "777"
}
var name: String {
get { "아이폰" }
set { }
}
static var type: String = "리모콘" // read-only! 재정의는 불가함
}
// 2) 계산 타입 속성으로 구현
class Ipad: RemoteMouse {
var id: String = "777"
var name: String = "아이패드"
class var type: String { // 타입 계산 속성은 재정의 가능 (class키워드 가능)
get { "리모콘" }
set { } //class키워드 -> 재정의도 가능하다!!
}
}
2. 메서드 요구사항(Methods Requirements)
1. 메서드의 헤드부분(인풋/아웃풋)의 형태만 요구사항으로 정의
2. mutating 키워드: 값타입(구조체와 열거형)에서 저장 속성 변경을 허락하는 키워드
(참조타입인 클래스는 원래 가능하니 mutating이 필요가 없다)
3. 타입 메서드로 제한 하려면, static키워드만 붙이면 됨
채택하여 구현하는 쪽에서 static / class(재정의를 원한다면) 키워드 모두 사용 가능
(다만 프로토콜에서는 무조건 static❗️❗️으로 정의)
// 1) 정의
protocol RandomNumber {
static func doSomething() // 최소한 타입 메서드가 되야함 (class로 구현해서 재정의를 허용하는 것도 가능)
func random() -> Int
mutating func add()
}
// 2) 구조체의 경우
struct Number: RandomNumber {
var x = 2
static func doSomething() {
print("do Something")
}
func random() -> Int {
return Int.random(in: 1...100)
}
mutating func add() { //mutating이 있어야 저장속성 값 변경 가능
self.x += 2
}
}
var n = Number()
n.random()
n.add()
n.x //4
Number.doSomething() //do Something
// 3) 클래스의 경우
class Number2: RandomNumber{
var x = 2
//class로 하여 서브클래스에서 타입메서드를 재정의 하고자 함
class func doSomething() {
print("do Something")
}
func random() -> Int {
return Int.random(in: 1...100)
}
func add() { //mutating이 없어도 저장 속성값 변경 가능
self.x += 2
}
}
class Number3: Number2{
//타입 메서드 재정의
class override func doSomething() {
print("do Something")
}
}
열거형에서 프로토콜 채택할때
// 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 //mutating의 키워드가 있기에 속성 변경이 가능함!
case .on:
self = .off //mutating의 키워드가 있기에 속성 변경이 가능함!
}
}
}
var s = OnOffSwitch.off
s.toggle()
s.toggle()
class BigSwitch: Togglable {
var isOn = false
func toggle() { // mutating 키워드 필요없음 (클래스 이기 때문)
isOn = isOn ? false : true
}
}
var big = BigSwitch()
print(big.isOn) //false
big.toggle()
print(big.isOn) //true
3. 생성자 요구사항(Initializer Requirements)
(실제 프로젝트에서 사용하는 경우 드물다)
1) 클래스는 (상속 문제 때문에) 생성자 앞에 required 붙여야함 (하위에서 구현을 강제)
2) final 키워드 사용시 상속이 불가하기에 required 생략가능
3) 클래스에서는 반드시 지정생성자로 구현할 필요 없음(편의생성자로 구현도 가능)
protocol SomeProtocol { // 생성자를 요구사항으로 지정 가능
init(num: Int)
}
// 예제 - 1 ======================
class SomeClass: SomeProtocol {
var myNum = 1
required init(num: Int) {
// 실제 구현
myNum = num
}
convenience required init() {
self.init(num: 2)
}
}
var x = SomeClass()
x.myNum
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!()로 구현 (O)
- init() 요구사항 ➡︎ init?() 로 구현 (X - 범위가 더 넓어지는 것 안됨)
// 실패가능 생성자
protocol AProto {
init?(num: Int) // (객체의 타입이 맞을까?) AClass? <==== AClass은 범위가 속해있음
}
// 구조체에서 채택 (required 키워드는 필요없음)
struct AStruct: AProto { // Failable/Non-failable 모두 요구사항을 충족시킴
//init?(num: Int) {}
init(num: Int) {}
//init!(num: Int) {} // 이것도 괜찮음
}
// 클래스에서 채택
class AClass: AProto {
required init(num: Int) {}
}
4. 서브스크립트 요구사항(Subscript requirements)
get, set 키워드를 통해서 읽기/쓰기 여부를 설정 (최소한의 요구사항일뿐)
get => 최소한 읽기 서브스크립트 구현 / 읽기,쓰기 모두 구현 가능
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 { // 구현은 선택
// 상세구현 생략
}
}
}
3. 타입으로서의 프로토콜(Protocols as Types)
프로토콜은 일급객체(First Class Citizen)이므로, 타입으로 사용가능함
1. is 연산자
특정 타입이 프로토콜을 채택하고 있는지 확인 (참 또는 거짓) / 그 반대도 확인가능
2. as 연산자
타입 캐스팅 (특정 인스턴스를 프로토콜로 변환하거나, 프로토콜을 인스턴스 실제형식으로 캐스팅)
protocol Remote {
func turnOn()
func turnOff()
static func turnDown()
}
class TV: Remote {
func turnOn() {
print("TV켜기")
}
func turnOff() {
print("TV끄기")
}
//타입 메서드
static func turnDown() {
print("TV소리 낮춰")
}
}
struct SetTopBox: Remote {
func turnOn() {
print("셋톱박스켜기")
}
func turnOff() {
print("셋톱박스끄기")
}
func doNetflix() {
print("넷플릭스 하기")
}
static func turnDown() {
print("Netflix 소리 낮춰")
}
}
let tv = TV()
tv.turnOn() //TV켜기
tv.turnOff() //TV끄기
TV.turnDown() //TV소리 낮춰
let sbox = SetTopBox()
sbox.turnOn() //셋톱박스켜기
sbox.turnOff() //셋톱박스끄기
sbox.doNetflix() //넷플릭스 하기
SetTopBox.turnDown() //Netflix 소리 낮춰
// 프로토콜의 타입 취급의 장점 - 1 ⭐️
let electronic: [Remote] = [tv, sbox] // 프로토콜의 형식으로 담겨있음
for item in electronic { // 켜기, 끄기 기능만 사용하니 타입캐스팅을 쓸 필요도 없음 (다만, 프로토콜에 있는 멤버만 사용가능)
item.turnOn()
}
// 프로토콜의 타입 취급의 장점 - 2 ⭐️
func turnOnSomeElectronics(item: Remote) {
item.turnOn()
}
//프로토콜 타입의 인스턴스만 파라미터로 받겟다!
turnOnSomeElectronics(item: tv)
turnOnSomeElectronics(item: sbox)
func turnOfSomeElectronics(item: Remote){
item.turnOff()
}
//func turnOfSomeElectronics(item: Remote){
// item.turnDown() //클래스를 파라미터로 받는게 아니므로 불가능
//}
// is연산자(특정타입이 프로토콜을 채택하고 있는지 확인)
tv is Remote
sbox is Remote
// 프로토콜 타입으로 저장된 인스턴스가 더 구체적인 타입인지 확인 가능
electronic[0] is TV
electronic[1] is SetTopBox
// 업캐스팅(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()
4. 기타 프로토콜 문법들
1. 프로토콜 간에 상속(다중상속도 가능)
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)
protocol SomeProtocol: AnyObject { // AnyObject는 클래스 전용 프로토콜
func doSomething()
}
// 구조체에서는 채택할 수 없게 됨 ⭐️
//struct AStruct: SomeProtocol {
//
//}
// 클래스에서만 채택 가능
class AClass: SomeProtocol {
func doSomething() {
print("Do something")
}
}
프로토콜 합성(Protocol Composition) 문법
// 프로토콜을 합성하여 임시타입으로 활용 가능 ⭐️
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
protocol Job{
var job: String { get }
}
//protocol Human: Named, Aged, Job{
//
//}
//// 하나의 타입에서 여러개의 프로토콜을 채택하는 것 당연히 가능 (다중 상속과 비슷한 역할)
//
//class Me: Human{
// var name: String
// var age: Int
// var job: String
//
// init(name: String, age: Int, job: String){
// self.name = name
// self.age = age
// self.job = job
// }
//}
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 // 임시적인 타입으로 저장 (두개의 프로토콜을 모두 채택한 타입만 저장 가능)
5. 마무리
프로토콜은 상속의 단점을 보완하는 자격증의 개념으로 자유로운 일급객체. 가장 기본적인 부분을 다루었고 2부에 이어서...
'Swift' 카테고리의 다른 글
Swift [Closure 1부] (0) | 2023.03.01 |
---|---|
Swift [Protocol 2부] (0) | 2023.02.27 |
Swift [Extension] (0) | 2023.02.26 |
Swift [Any, AnyObject] (0) | 2023.02.25 |
Swift [Type Casting(is, as)] (0) | 2023.02.23 |