[Swift 공식문서 읽기]Protocols

llim🧚🏻‍♀️·2021년 8월 25일
0

Swift

목록 보기
21/26
post-thumbnail

안녕하세요. 엘림입니다🙇🏻‍♀️

Swift 공식 문서를 정독하기 시리즈입니다!

제 스타일대로 정리했으니 추가적으로 더 필요한 정보는
공식문서 링크를 눌러 확인해주세용!

좀 더 편하게 보기위해 한국어로 번역된 사이트를 함께 확인했습니다!ㅎㅎ

자, 그럼 시작해볼까요

이 글은 공부하면서 작성한 글이기 때문에 잘못된 정보가 있을 수 있습니다.🥺
금방 잊어버릴... 미래의 저에게 다시 알려주기 위한 글이다보니
혹시라도 틀린 부분이 있다면, 댓글로 친절하게 알려주시길 부탁드립니다.🙏


프로토콜

프로토콜은 특정 기능 수행에 필수적인 요소를 정의한 청사진입니다. 프로토콜을 만족시키는 타입은 프로토콜을 따른다고 합니다. 프로토콜에 필수 구현을 추가하거나, 추가적인 기능을 더하기 위해 프로토콜도 확장할 수 있습니다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

서브클래싱인 경우 수퍼클래스를 프로토콜 앞에 적어 줍니다.

프로퍼티 요구사항

프로토콜에서는 프로퍼티가 저장 프로퍼티인지 연산 프로퍼티인지 명시하지 않습니다. 하지만 프로퍼티의 이름과 타입, gettable - settable에 대해서는 명시합니다. 또한 필수 프로퍼티는 항상 var로 선언합니다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
    static var someTypeProperty: Int { get set }
}
protocol FullyNamed {
    var fullName: String { get }
}

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

fullName 프로퍼티는 저장된 프로퍼티로 사용될 수 있고, 연산 프로퍼티로 사용될 수 있습니다.

메소드 요구사항

프로토콜에서는 필수 인스턴스 메소드와 타입 메소드를 명시할 수 있습니다. 하지만 메소드 파라미터의 기본 값은 프로토콜 안에서 사용할 수 없습니다.

protocol RandomNumberGenerator {
    func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

mutating 메소드 요구사항

mutating 키워드를 사용해 인스턴스에서 변경 가능하다는 것을 표시할 수 있습니다. 이 mutating 키워드는 값타입 형에만 사용합니다. (프로토콜에 mutating을 명시한 경우 이 프로토콜을 따르는 클래스 형을 구현할 때는 메소드에 mutating을 명시하지 않습니다.)

protocol Togglable {
    mutating func toggle() // mutating을 붙여서 선언하지 않으면, 
    					   //값타입 내부 값을 변경하는 형태로에서 정의할 수 없음.
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

class ButtonSwitch: Togglable {
    var button = true
    func toggle() {
        switch button {
        case true:
            button = false
        case false:
            button = true
        }
    }
}

var button = ButtonSwitch()
// true
button.toggle()
// false

이니셜라이저

프로토콜에서 필수로 구현해야하는 이니셜라이저를 지정할 수 있습니다.

클래스에서 프로토콜 필수 이니셜라이저의 구현

프로토콜에서 특정 이니셜라이저가 필요하다고 명시했기 때문에 구현에서 해당 이니셜라이저에 required 키워드를 붙여줘야 합니다. (클래스 타입에서 final로 선언된 것에는 required를 표시하지 않도 됩니다. final로 선언되면 서브클래싱 되지 않기 때문입니다.)

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}

특정 프로토콜의 필수 이니셜라이저를 구현하고, 수퍼클래스의 이니셜라이저를 서브클래싱하는 경우 이니셜라이저 앞에 required 키워드와 override 키워드를 적어줍니다.

실패가능한 이니셜라이저도 선언할 수 있습니다.

타입으로서 프로토콜

프로토콜도 하나의 타입으로 사용됩니다. 그렇기 때문에 다음과 같이 타입 사용이 허용되는 모든 곳에 프로토콜을 사용할 수 있습니다.

  • 함수, 메소드, 이니셜라이저의 파라미터 타입 혹은 리턴 타입
  • 상수, 변수, 프로퍼티의 타입
  • 컨테이너인 배열, 사전 등의 아이템 타입
class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

프로토콜 타입 콜렉션

프로토콜을 Array, Dictionary등 Collection 타입에 넣기위한 타입으로 사용할 수 있습니다.

let things: [TextRepresentable] = [game, d12, simonTheHamster]

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

Delegation

딜리게이션은 클래스 혹은 구조체 인스턴스의 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴 중 하나입니다.

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject { // AnyObject로 선언하면 클래스만 이 프로토콜을 따를 수 있게 만들 수 있습니다.
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

익스텐션으로 채택하기

이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 익스텐션을 사용할 수 있습니다. 원래 값에 접근 권한이 없어도 익스텐션을 사용해 기능을 확장할 수 있습니다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"

where

특정 조건을 만족시킬때만 프로토콜을 따르도록 제한할 수 있습니다. 이 선언은 where 절을 사용해 정의합니다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

빈 익스텐션에 선언하기

만약 어떤 프로토콜을 충족에 필요한 모든 조건을 만족하지만 아직 그 프로토콜을 따른다는 선언을 하지 않았다면 그 선언을 빈 익스텐션으로 선언할 수 있습니다. 프로토콜의 요구사항을 기술하는 것만으로 프로토콜 사용의 조건을 충족시킬 수 없습니다. 반드시 어떤 프로토콜을 따르는지에 대해 기술해야 합니다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

상속

클래스 상속같이 프로토콜도 상속할 수 있습니다. 여러 프로토콜을 상속받는 경우 콤마(,)로 구분합니다.

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

합성

동시에 여러 프로토콜을 따르는 타입을 선언할 수 있습니다.

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("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

wishHappyBirthday 메소드의 celebrator파라미터는 Named 프로토콜과 Aged 프로토콜을 동시에 따르는 타입으로 선언하기 위해 Named & Aged로 표시했습니다.

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

프로토콜 순응 확인

어떤 타입이 특정 프로토콜을 따르는지 다음과 같은 방법으로 확인 할 수 있습니다.

  • is연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있습니다. 특정 프로토콜을 따르면 true를 아니면 false를 반환합니다.
  • as?는 특정 프로토콜 타입을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트를 하게 되고 따르지 않는 경우는 nil을 반환합니다.
  • as!는 강제로 특정 프로토콜을 따르도록 정의합니다. 만약 다운캐스트에 실패하면 런타임 에러가 발생합니다.
protocol HasArea {
    var area: Double { get }
}

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi radius radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

선택적 프로토콜 요구조건(Optional Protocol)

프로토콜을 선언하면서 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있습니다. 이 프로토콜의 정의를 위해서 @objc키워드를 프로토콜 앞에 붙이고, 개별 함수 혹은 프로퍼티에는 @objc와 optional 키워드를 붙입니다. @objc 프로토콜은 클래스 타입에서만 채용될 수 있고 구조체나 열거형에서는 사용할 수 없습니다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

엄격하게 말해 CounterDataSource를 따르면서 구현은 하나도 하지 않는 클래스를 선언할 수는 있습니다. 기술적으로는 가능하지만 이렇게 구현하는 것은 좋은 방법이 아닙니다. 만약 하나도 구현하지 않는다면 필요하지 않다는 것 이므로 굳이 CounterDataSource 프로토콜을 따르도록 선언할 필요가 없습니다.

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

프로토콜 확장

익스텐션을 이용해 프로토콜을 확장할 수 있습니다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

기본 구현 제공

익스텐션을 기본 구현을 제공하는데 사용할 수 있습니다. 특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현을 사용하게 됩니다. 즉, 프로토콜에서는 선언만 할 수 있는데 익스텐션을 이용해 기본 구현을 제공할 수 있습니다.
(프로토콜 익스텐션에 의해 구현된 기본 구현은 옵셔널 요구조건과 다릅니다. 둘 다 꼭 구현하지 않아도 되는 것은 같지만 사용시 기본 구현은 옵셔널 체이닝을 사용하지 않아도 됩니다.)

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

제약 추가

프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있습니다. 이 선언에는 where 절을 사용합니다.

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

오늘도 스위프트 공식문서를 정리해보았군욥~
다음편도 힘내보겠습니다!

감사합니다🙇🏻‍♀️

profile
한달 차 iOS 개발자입니다🐥

0개의 댓글