Swift - 프로토콜

임성빈·2022년 4월 17일
0

Swift

목록 보기
21/26
post-thumbnail
post-custom-banner

프로토콜


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


프로토콜 문법

프로토콜의 정의는 클래스, 구조체, 열거형 등과 유사하다.

protocol SomeProtocol {
	// protocol definition goes here
}

프로토콜을 따르는 타입을 정의하기 위해서는 타입 이름 뒤에 콜론 (:) 을 붙이고, 따를 프로토콜 이름을 적는다. 만약 따르는 프롵토콜이 여러개라면 콤마로 구분해 준다.

struct SomeStructure: FirstProtocol, SecondProtocol {
	// structure definition goes here
}

서브클래싱인 경우 Superclass를 프로토콜 앞에 적는다.

class SomeClass: SomeSuperClass, FirstProtocol, SecondProtocol {
	// class definition goes here
}

프로퍼티 요구사항

프로토콜에서는 프로퍼티가 저장된 프로포티인지 계산된 프로퍼티인지 명시하지 않는다. 하지만 프로퍼티의 이름과 타입 그리고 gettable, settable 한지는 명시한다. 필수 프로퍼티는 항상 var 로 선언해야한다.

protocol SomeProtocol {
	var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}	

타입 프로퍼티는 static 키워드를 적어 선언한다.

protocol SecondProtocol {
	static var someTypeProperty: Int { get set }
}

하나의 프로퍼티를 갖는 프로토콜을 선언한다.

protocol FullNamed {
	var fullName: String { get }
}

이 프로토콜을 따르는 구조체를 선언한다. fullName 프로퍼티는 저장된 프로퍼티로 사용될 수 있다.

struct Person: FullyNamed {
	var fullName: String
}
let john = Person(fullName: "John Appleseed")
print("\(john.fullName)")
// "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"

메소드 요구사항

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

protocol SomeProtocol {
	static func someTypeMethod()
}

필수 메소드 지정시 함수명과 반환값을 지정할 수 있고, 구현에 사용하는 괄호는 적지 않아도 된다.

protocol RandomNumberGenerator {
	func random() -> Double
}

다음 코드는 따르는 프로토콜의 필수 메소드 random() 을 구현한 클래스이다.

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 메소드를 선언한 프로토콜의 예제이다.

protocol Togglable {
	mutating func toggle()
}

이 프로토콜을 따르는 값타입 형에서 toggle() 메소드를 변경해 사용할 수 있다.

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

초기자 요구사항

프로토콜에서 필수로 구현해야하는 Initializer를 지정할 수 있다.

protocol SomeProtocol {
	init(someParameter: Int)
}

클래스에서 프로토콜 필수 Initializer의 구현

프로토콜에서 특정 Initializer가 필요하다고 명시했기 때문에 구현에서 해당 Initializer에 required 키워드를 붙여줘야한다.

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

특정 프로토콜의 필수 Initializer를 구현하고, Superclass의 Initializer를 서브클래싱하는 경우 Initializer 앞에 required 키워드와 override 키워드를 적어준다.

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
    }
}

실패 가능 초기자 요구사항

프로토콜에서 실패 가능 Initializer를 선언할 수 있다.


타입으로써의 프로토콜

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

  • 함수, 메소드, Initializer의 파라미터 타입 혹은 리턴 타입
  • 상수, 변수, 프로퍼티의 타입
  • 컨테이너인 배열, 사전 등의 아이템 타입

다음은 프로토콜을 타입으로 사용한 예제이다.

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
    }
}

RandomNumberGeneratorgenerator 상수의 타입으로 그리고 Initializer의 파라미터 형으로 사용했다.

위에서 선언한 Dice 를 초기화 할 때 generator 파라미터 부분에 RandomNumberGenerator 프로토콜을 따르는 인스턴스를 넣는다.

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

위임

위임은 클래스 혹은 인스턴스에 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴 중 하나이다.

protocol DiceGame {
	var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame 프로토콜을 선언하고 DiceGameDelegate 에 선언해서 실제 DiceGame 의 행위와 관련된 구현을 DiceGameDelegate 를 따르는 인스턴스에 위임한다. DiceGameDelegateAnyObject 로 선언하면 클래스만 이 프로토콜을 따를 수 있게 만들 수 있다.

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)
    }
}

SnakesAndLaddersDiceGame 를 따르고 DiceGameDelegate 를 따르는 delegate 를 갖는다. 게임을 실행 (play()) 했을 때 delegate?.gameDidStart(self) , delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll) , delegate?.gameDidEnd(self) 를 실행한다. delegate 는 게임을 진행시키는데 반드시 필요한 건 아니라서 옵셔널로 정의돼 있다. 아래는 실제 DiceGameDelegate 를 상속하는 delegate DiceGameTracker 를 구현한 예입니다.

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")
    }
}

DiceGameTracker 를 이용해 게임을 진행시킨다. 게임의 tracking 관련된 작업은 이 DiceGameTracker 가 위임받아 그곳에서 실행된다.

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 }
}

익스텐션을 이용해 DiceTextRepresentable 프로토콜을 따르도록 구현하면 다음과 같다.

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

아래와 코드의 d12.textualDescription 같이Dice 에 추가한 익스텐션을 자동으로 그대로 사용할 수 있다.

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

Dice 와 같이 SnakesAndeLaddersTextRepresentable 을 따르도록 구현할 수 있다.

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 절을 사용해 정의한다. 아래 예제는 TextRepresentable 을 따르는 Array 중에 Array의 각 원소가 TextRepresentable 인 경우에만 따르는 프로토콜을 정의한다. textualDescription 은 Array의 각 원소가 TextRepresentable 를 따르기 때문에 textualDescription 프로퍼티를 사용할 수 있다. textualDescription 은 Array의 모든 아이템을 순회하고 각각의 textualDescription 를 결합해 반환하는 메소드이다.

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 {}

Hamster 인스턴스인 simonTheHamster 은 이제 TextRepresentable 타입으로 사용할 수 있다.

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

프로토콜 타입 콜렉션

프로토콜을 Array, Dictionary 등 Collection 타입에 넣기 위한 타입으로 사용할 수 있다. 아래는 TextRepresentable 프로토콜을 따르는 객체 Array에 대한 선언이다.

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

Array의 모든 객체는 TextRepresentable 을 따르므로 textualDescription 프로퍼티를 갖는다.

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

프로토콜 상속

클래스 상속처럼 프로토콜도 상속할 수 있다. 여러 프로토콜을 상속받는 경우 콤마로 구분한다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

위의 TextRepresentable 프로토콜을 상속받아 새로운 프로토콜 PrettyTextRepresentable 을 구현한다. 이 프로토콜은 text를 예쁘게 보여주는 프로퍼티를 구현한다.

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

SnakesAndLadders 클래스는 위에서 선언한 PrettyTextRepresentable 프로토콜을 따르도록 선언하고 prettyTextualDescription 프로퍼티를 아래와 같이 구현한다.

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
    }
}

square 가 0보다 큰지, 작은지 혹은 0인지에 대해 구분해 각각 알맞은 기호를 반환한다.

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

클래스 전용 프로토콜

구조체, 열거형에서 사용하지 않고 클래스 타입에만 사용 가능한 프로토콜을 선언하기 위해서는 프로토콜에 AnyObject 를 추가한다.

protocol SomeClassOnlyProtocol: AnyObject, 	SomeInheritedProtocol {
	// class-only protocol definition goes here
}

프로토콜 합성

동시에 여러 프로토콜을 따르는 타입을 선언할 수 있다. 아래 PersonNamedAged 프로토콜을 동시에 따르는 구조체이다.

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)
// "Happy birthday, Malcolm, you're 21!" 출력

wishHappyBirthday 메소드의 celebrator 파라미터는 Named 프로토콜과 Aged 프로토콜을 동시에 따르는 타입으로 선언하기 위해 Named & Aged 로 표시했다. 아래 예제는 Location 프로토콜과 위의 Named 프로토콜을 따르는 City 클래스를 구현한 예제이다.

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! 는 강제로 특정 프로토콜을 따르도록 정의한다. 만약 다운케스트에 실패하면 런타임 에러가 발생한다.
    관련한 예제를 살펴보면 area 라는 값을 필요로하는 HasArea 프로토콜을 선언한다.
protocol HasArea {
	var area: Double { get }
}

HasArea 프로토콜을 따르는 Circle 클래스와 Country 클래스를 선언한다. Circle 클래스에서는 area 프로퍼티를 계산된 프로퍼티로 구현했고, Country 클래스에서는 저장 프로퍼티로 구현한다.

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 }
}

HasArea 를 따르지 않는 Animal 이란 클래스도 선언한다.

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

Circle , Country , Animal 의 인스턴스를 objects 라는 배열에 넣는다.

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

objects 배열을 순회하면 as? HasArea 구문을 사용해 HasArea 프로토콜을 따르는지 확인하고 따르는 경우 HasArea 타입으로 다운케스트한다.

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

결과로 처음 두 인스턴스는 HasArea 를 따르기 때문에 area 값이 반환되고 마지막 인스턴스는 HasArea 를 따르지 않기 때문에 else 절이 실행된다.


선택적 프로토콜 요구조건

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

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

아래 코드는 CounterDataSource 를 따르는 dataSource 를 선언한 예제이다.

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
        }
    }
}

increment?(forCount: count)fixedIncrement 는 옵셔널이므로 구현이 안돼 있을 수 있기 때문에 옵셔널 체이닝을 이용해 확인해 본다. 다음 코드는 CounterDataSource 를 따르는 예제이다.

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

Counter 인스턴스의 dataSourceThreeSource 로부터 입력받아 그 값을 증가시킬 수 있다.

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

다음은 조금 더 복잡한 예제로 CounterDataSource 를 이용해 어떤 값을 0에 수렴하도록 만드는 클래스를 선언한다.

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에서 시작하지만 TowardsZeroSourceincrement 에 의해 counter.increment() 가 호출될때마다 0에 가까워지면서 결국 0에 수렴하게 된다.

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

프로토콜 익스텐션

익스텐션을 이용해 프로토콜을 확장할 수 있다. 아래 코드는 random() 을 따르는 RandomNumberGeneratorrandomBool() 을 따르도록 추가한 예제이다.

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

아래 코드와 같이 generator 에서 generator.random()generator.randomBool() 을 둘 다 이용할 수 있음을 확인할 수 있다.

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"

익스텐션을 이용해 구현을 추가할 수는 있어도 다른 프로토콜로 확장 및 상속할 수는 없다. 만약 그렇게 하고자 한다면 익스텐션이 아닌 프로토콜 자체에 구현해야 한다.

기본 구현 제공

익스텐션을 기본 구현을 제공하는데 사용할 수 있다. 특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현을 사용하게 된다. 즉, 프로토콜에서는 선언만 할 수 있는데 익스텐션을 이용해 기본 구현을 제공할 수 있다.

아래 예제는 PrettyTextRepresentableprettyTextualDescription 프로퍼티의 구현을 그냥 textualDescription 을 반환하도록 구현한 예제이다.

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

프로토콜 익스텐션에 제약 추가

프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있다. 이 선언에는 where 절을 사용한다. 다음은 Collection 엘리먼트가 Equatable 인 경우에만 적용되는 allEqual() 메소드를 구현한 예제이다.

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

Collection 의 모든 엘리먼트가 같은지 비교하는 것이기 때문에 비교 기준을 first 원소로 잡고 확인한다.

위 예제를 실행하기 위해 아래와 같이 숫자 배열 2개를 선언한다.

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

각 배열의 엘리먼트는 모드 Euqatable 프로토콜을 따르기 때문에 allEqual() 을 호출할 수 있다.

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
profile
iOS 앱개발
post-custom-banner

0개의 댓글