주요 프로토콜

Ios_Roy·2023년 3월 5일
0

swift 문법

목록 보기
27/29
post-thumbnail

주요 프로토콜

Equatable / Comparable / Hashable

  • Equatable : ==, != 비교 연산자 관련 프로토콜
  • Comparable : < 연산자 관련 프로토콜 (>, <=, >=)
  • Hashable : hash값을 갖게되어 값이 해셔블(값이 유일성을 갖게됨)해짐

Equatable 프로토콜

Equatable 프로토콜의 역할 (동일성의 판단)

  • Equatable 프로토콜의 요구사항은
  • static func == (lhs: Self, rhs: Self) -> Bool 메서드의 구현
  • 스위프트에서 제공하는 기본 타입은 모두다 채택을 하고 있음
let num1: Int = 123
let num2: Int = 456

num1 == num2
num1 != num2

let str1: String = "Hello"
let str2: String = "안녕"

str1 == str2
str1 != str2

[Int의 내부 구현]
   @frozen public struct Int : FixedWidthInteger, SignedInteger {
      ...
      public static func == (lhs: Int, rhs: Int) -> Bool
      ...
   }

원칙: 동일성을 비교(==)하려면, Equatable을 채택하면

➡︎ 비교연산자(==)메서드 자동구현

  • 원칙) 구조체, 열거형의 경우 Equatable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이
    Equatable 프로토콜을 채택한 타입이라면 비교연산자 메서드 자동구현
  • 예외) 1) 클래스는 인스턴스 비교를 하는 항등연산자(===)가 존재하기 때문에 비교연산자(==)
    구현방식에 대해 개발자에게 위임 (클래스는 원칙적으로 동일성(==) 비교 불가)
  • 2) 열거형의 경우 연관값이 없다면 기본적으로 Equatable/Hashable하기 때문에
    Equatable 프로토콜을 채택하지 않아도 됨

열거형(Enum)의 경우

  • 원칙) 구조체, 열거형의 경우 Equatable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이
  • Equatable 프로토콜을 채택한 타입이라면 비교연산자 메서드 자동구현
  • (추가정보들이 Int, Double 등 이미 Equatable프로토콜을 채택해서, 구체적인 정보들까지 동일성 판별이 가능하기 때문)
enum SuperComputer: Equatable {
    case cpu(core: Int, ghz: Double)
    case ram(Int)
    case hardDisk(gb: Int)
}

SuperComputer.cpu(core: 8, ghz: 3.5) == SuperComputer.cpu(core: 16, ghz: 3.5)
SuperComputer.cpu(core: 8, ghz: 3.5) != SuperComputer.cpu(core: 8, ghz: 3.5)
  • 예외-2) 연관값이 없다면(원시값 여부는 상관없음) Equatable 프로토콜을 채택하지 않아도 동일성 비교 가능
  • ===> 기본적으로 추가정보가 없다면 같은지/다른지 동일성은 판별 가능하다는 논리 ⭐️
enum Direction {
    case east
    case west
    case south
    case north
}

Direction.north == Direction.east    // false
Direction.north != Direction.east    // true

구조체(Struct)의 경우

  • 원칙) 구조체, 열거형의 경우 Equatable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이
    • Equatable 프로토콜을 채택한 타입이라면 비교연산자 메서드 자동구현
  • (저장속성이 Int, Double 등 이미 Equatable프로토콜을 채택해서, 동일성 판별이 가능하기 때문)
struct Dog {
    var name: String
    var age: Int
}

extension Dog: Equatable {}

// 이렇게 전체 구현할 필요 없음
//extension Dog: Equatable {
//    static func ==(lhs: Dog, rhs: Dog) -> Bool {
//        return lhs.name == rhs.name && lhs.age == rhs.age
//    }
//}

let dog1: Dog = Dog(name: "초코", age: 10)
let dog2: Dog = Dog(name: "보리", age: 2)

dog1 == dog2
dog1 != dog2

클래스(Class)의 경우

  • 예외-1) Equatable 프로토콜 채택시 클래스는 예외
  • 클래스는 인스턴스 비교를 하는 항등연산자(===)가 존재하기 때문에 비교연산자(==) 구현방식에 대해
  • 개발자에게 위임 (클래스는 원칙적으로 동일성(==) 비교 불가)
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 비교하고 싶어서, Equatable 프로토콜 채택 ====> 클래스에서는 에러 발생 ===> 비교연산자(==)를 구현 직접구현해야함

extension Person: Equatable {
    static func ==(lhs: Person, rhs: Person) -> Bool {  // 특별한 이유가 없다면 모든 속성에 대해, 비교 구현
        return lhs.name == rhs.name && lhs.age == rhs.age
        //return lhs.name == rhs.name     // 이름만 같아도 동일하다고 보려면 이렇게 구현
        //return lhs.age == lhs.age       // 나이만 같아도 동일하다고 보려면 이렇게 구현
    }
}

let person1: Person = Person(name: "홍길동", age: 20)
let person2: Person = Person(name: "임꺽정", age: 20)

person1 == person2
person1 != person2

Comparable 프로토콜

Comparable 프로토콜의 역할(값의 크기나 순서 비교, 정렬 가능하도록 하려면)

  • Comparable 프로토콜의 요구사항은
  • static func < (lhs: Self, rhs: Self) -> Bool 메서드의 구현
  • 일반적으로 < 만 구현하면 <, <=, >= 연산자도 자동 구현
  • Comparable 프로토콜은 Equatable프로토콜을 상속하고 있음
    (필요한 경우, 비교연산자(==)도 구현해야함)
  • 스위프트에서 제공하는 기본 숫자 타입 및 String은 모두 다 채택을 하고 있음
    (Bool타입은 채택하지 않음)
let num1: Int = 123
let num2: Int = 456

num1 < num2
num1 > num2

let str1: String = "Hello"
let str2: String = "안녕"

str1 < str2
str1 > str2

[Int의 내부 구현]
   @frozen public struct Int : FixedWidthInteger, SignedInteger {
      ...
      public static func < (lhs: Int, rhs: Int) -> Bool
      ...
   }

원칙: 값의 크기나 순서를 비교하려면, Comparable을 채택하고, < (less than)연산자 구현해야함

  • 원칙) 구조체, 클래스의 모든 저장 속성(열거형은 원시값이 있는 경우)이 Comparable을 채택한
    경우라도, <(less than)연산자 직접 구현해야함
    (순서 정렬 방식에 대해서는 무조건 구체적인 구현이 필요하다는 논리)
  • 예외) 열거형의 경우, 원시값이 없다면(연관값이 있더라도) Comparable을 채택만 하면
    <(less than)연산자는 자동 제공
    (원시값을 도입하는 순간, 개발자가 직접 대응되는 값을 제공하므로 정렬방식도 구현해야한다는 논리)

열거형(Enum)의 경우

  • 원칙) 구조체, 클래스의 모든 저장 속성(열거형은 원시값이 있는 경우)이 Comparable을 채택한
    • 경우라도, <(less than)연산자 직접 구현해야함 (순서 정렬 방식에 대해서는 무조건 구체적인 구현이 필요하다는 논리)
  • (원시값을 도입하는 순간, 개발자가 직접 대응되는 값을 제공하므로 정렬방식도 구현해야한다는 논리) ⭐️
enum Direction: Int {
    case east
    case west
    case south
    case north
}

extension Direction: Comparable {   //Type 'Direction' does not conform to protocol 'Comparable'
    static func < (lhs: Direction, rhs: Direction) -> Bool {
        return lhs.rawValue < rhs.rawValue
    }
}

Direction.north < Direction.east    // false
Direction.north > Direction.east    // true
  • 예외) 열거형의 경우, 원시값이 없다면(연관값이 있더라도) Comparable을 채택만 하면
  • <(less than)연산자는 자동 제공
  • (원시값을 도입하는 순간, 개발자가 직접 대응되는 값을 제공하므로 정렬방식도 구현해야한다는 논리) ⭐️
enum SuperComputer: Comparable {
    case cpu(core: Int, ghz: Double)
    case ram(Int)
    case hardDisk(gb: Int)
}

SuperComputer.cpu(core: 8, ghz: 3.5) < SuperComputer.cpu(core: 16, ghz: 3.5)    //  true
SuperComputer.cpu(core: 8, ghz: 3.5) > SuperComputer.cpu(core: 8, ghz: 3.5)     //  false

enum MyDirection: Comparable {
    case east
    case west
    case south
    case north
}

MyDirection.north < MyDirection.east    // false
MyDirection.north > MyDirection.east    // true

구조체(Struct)의 경우

  • 원칙) 구조체, 클래스의 모든 저장 속성(열거형은 원시값이 있는 경우)이 Comparable을 채택한
  • 경우라도, <(less than)연산자 직접 구현해야함
  • (순서 정렬 방식에 대해서는 무조건 구체적인 구현이 필요하다는 논리)
struct Dog {
    var name: String
    var age: Int
}

extension Dog: Comparable {       // 이름순으로 갈것인지 / 나이순으로 갈 것인지 구현해야함

//    static func ==(lhs: Dog, rhs: Dog) -> Bool {                   // Equatable은 name, age의 저장 속성이 Equatable프로토콜을 구현하기에 자동제공
//        return lhs.name == rhs.name && lhs.age == rhs.age
//    }
    
    static func < (lhs: Dog, rhs: Dog) -> Bool {
        return lhs.age < rhs.age
    }
}

let dog1: Dog = Dog(name: "초코", age: 10)
let dog2: Dog = Dog(name: "보리", age: 2)

dog1 < dog2
dog1 > dog2

클래스(Class)의 경우

  • 원칙) 구조체, 클래스의 모든 저장 속성(열거형은 원시값이 있는 경우)이 Comparable을 채택한
  • 경우라도, <(less than)연산자 직접 구현해야함
  • 순서 정렬 방식에 대해서는 무조건 구체적인 구현이 필요하다는 논리)
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

extension Person: Comparable {
    
    // 클래스의 경우, ==연산자도 구현해야함
    static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
    
    // 나이순으로 정렬하고자 함 ⭐️
    static func < (lhs: Person, rhs: Person) -> Bool {
        return lhs.age < rhs.age
    }
    
}

let person1: Person = Person(name: "홍길동", age: 20)
let person2: Person = Person(name: "임꺽정", age: 22)

person1 < person2
person1 > person2

Hashable 프로토콜

Hashable 프로토콜의 역할(유일한 값을 갖도록 해서, Dictionary의 키값 또는 Set의 요소가 될 수 있음)

  • Hashable 프로토콜의 요구사항은
  • func hash(into hasher: inout Hasher) 메서드의 구현
  • 예전 버전에서는 var hashValue: Int { get } 와 같이
    hashValue 계산 속성으로 구현되어있었음 / 현재는 위의 방식으로 구현해야함
  • 스위프트에서 제공하는 기본 숫자 타입은 모두 다 채택을 하고 있음
let num1: Int = 123
let num2: Int = 456

let set: Set = [num1, num2]
// Int가 해셔블하기 때문에, Set의 원소가 될 수 있음

let str1: String = "Hello"
let str2: String = "안녕"

let set2: Set = [str1, str2]
// String이 해셔블하기 때문에, Set의 원소가 될 수 있음

//123.hashValue    // ===> 이 방식은 현재 deprecated되었음(앞으로 사용못하게될 예정)

/**=============================================================
 [Int의 내부 구현]
     extension Int : Hashable {

         @inlinable public func hash(into hasher: inout Hasher)

         public var hashValue: Int { get }
     }

원칙: 커스텀타입이 딕셔너리의 키값이나 Set의 요소로 사용가능 하려면, Hashable을 채택하면 ➞ hash(into:)메서드 자동구현

  • 원칙) 구조체, 열거형의 경우 Hashable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이
    Hashable 프로토콜을 채택한 타입이라면, hash(into:)메서드 자동구현
  • 1) 클래스는 인스턴스의 유일성 갖게 하기위해서는 hash(into:)메서드 직접 구현해야함
    (클래스는 원칙적으로 Hashable 지원 불가)
  • 2) 열거형의 경우 연관값이 없다면 기본적으로 Equatable/Hashable하기 때문에
    Hashable 프로토콜을 채택하지 않아도 됨

열거형(Enum)의 경우

  • 원칙) 구조체, 열거형의 경우 Equatable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이 Hashable 프로토콜을 채택한 타입이라면, hash(into:)메서드 자동구현
  • (추가정보들이 Int, Double 등 이미 Hashable프로토콜을 채택해서, 구체적인 정보들까지 유일성 판별이 가능하기 때문)
enum SuperComputer: Hashable {
    case cpu(core: Int, ghz: Double)
    case ram(Int)
    case hardDisk(gb: Int)
}

let superSet: Set = [SuperComputer.cpu(core: 8, ghz: 3.5), SuperComputer.cpu(core: 16, ghz: 3.5)]

// 예외-2) 연관값이 없다면(원시값 여부는 상관없음) Hashable 프로토콜을 채택하지 않아도 유일성 판별 가능
// ===> 기본적으로 추가정보가 없다면 유일성 판별 가능하다는 논리 ⭐️

enum Direction {
    case east
    case west
    case south
    case north
}

let directionSet: Set = [Direction.north, Direction.east]

구조체(Struct)의 경우

  • 원칙) 구조체, 열거형의 경우 Hashable 프로토콜 채택시 모든 저장 속성(열거형은 모든 연관값)이 Hashable 프로토콜을 채택한 타입이라면, hash(into:)메서드 자동구현
  • (저장속성이 Int, Double 등 이미 Hashable프로토콜을 채택해서, 유일성 판별이 가능하기 때문)
struct Dog {
    var name: String
    var age: Int
}

extension Dog: Hashable {}

// 이렇게 전체 구현할 필요 없음
//extension Dog: Hashable {
//    func hash(into hasher: inout Hasher) {
//        hasher.combine(name)
//        hasher.combine(age)
//    }
//}

let dog1: Dog = Dog(name: "초코", age: 10)
let dog2: Dog = Dog(name: "보리", age: 2)

let dogSet: Set = [dog1, dog2]

클래스(Class)의 경우

  • 예외-1) 클래스는 인스턴스의 유일성 갖게 하기위해서는 hash(into:)메서드 직접 구현해야함
  • (클래스는 원칙적으로 Hashable 지원 불가)
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// Set에 넣고 싶어서, Hashable 프로토콜 채택 ====> 클래스에서는 에러 발생 ===> hash(into:)메서드 직접구현해야함
// extension Person: Hashable {}

extension Person: Hashable {
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(age)
    }
    
    // == 연산자도 직접구현해야함
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

let person1: Person = Person(name: "홍길동", age: 20)
let person2: Person = Person(name: "임꺽정", age: 20)

let personSet: Set = [person1, person2]

CaseIterable 프로토콜

Enum타입에서 사용할 수 있는 CaseIterable (Swift 5.2 ~ )

  • Iterable: 영어의 뜻 ==> 반복가능한
  • 열거형에서 CaseIterable 프로토콜을 채택하면 타입 계산 속성이 자동으로 구현됨
  • static var allCases: Self.AllCases { get }
  • 이 "타입 계산속성"을 컴파일러가 자동으로 구현 제공 ====> 모든 케이스를 (정의한 순서대로) 포함한 배열을 리턴
  • 연관값(associated value)이 없는 경우에만 채택 가능 (원시값은 상관없음)
enum Color: CaseIterable {  // Int
    case red, green, blue
}

//var color = Color.red
//color = .blue
//color = .green

Color.allCases     // [Color.red, Color.green, Color.blue]
print(Color.allCases)

//var color2 = Color(rawValue: 0)
//color2 = Color(rawValue: Int.random(in: 0...Color.allCases.count))

배열의 장점을 사용해 여러가지 편의적 기능 활용 가능

// 손쉽게 반복문 사용 가능
for color in Color.allCases {
    print("\(color)")
}

// 필요로 하는 곳에서 선언도 간단하게
struct SomeView {
    let colors: [Color] = [Color.red, Color.green, Color.blue]
    //let colors = Color.allCases
}

// 공식문서의 예제

enum CompassDirection: CaseIterable {
    case north, south, east, west
}

// 1) 케이스의 갯수를 세기 편해짐 (케이스를 늘려도 코드 고칠 필요 없음)
print("방향은 \(CompassDirection.allCases.count)가지")

// 2) 배열 ===> 고차함수 이용 가능

let caseList = CompassDirection.allCases
                               .map({ "\($0)" })
                               .joined(separator: ", ")  // 배열 ===> 문자열화
// "north, south, east, west"

// 랜덤 케이스를 뽑아낼 수 있음

let randomValue = CompassDirection.allCases.randomElement()

열거형 단원에서 다뤘던 원시값(Raw Value)를 이용한 케이스

enum RpsGame: Int, CaseIterable {
    case rock = 0
    case paper = 1
    case scissors = 2
}

let number = Int.random(in: 0...100) % 3    // 3을 조금 더 멋지게 처리할 수 있는 것은 고급내용에서 다룸

let number2 = Int.random(in: 0...100) % RpsGame.allCases.count    // 나머지를 구하는 것이니 무조건 0, 1, 2 중에 한가지임

print(RpsGame.init(rawValue: number)!)
profile
iOS 개발자 공부하는 Roy

0개의 댓글