Search
💥

Protocol / Genreic / Associatedtype - 이창준

Protocol

Protocol

특정 역할을 하기 위한 메소드, 프로퍼티, 기타 요구사항 등의 청사진? 약속? 규약?
프로토콜은 일급 객체
클래스, 구조체, 열거형에서 사용할 수 있음
저장프로퍼티인지 연산프로퍼티인지 명시하지 않음
실제 구현은 사용하는 곳에서!
이름과 타입 그리고 gettable, settable한지 명시 ( 프로퍼티는 항상 var로 선언 )
메소드를 만들 시, { } 는 적지 않고 반환값도 명시 할 수 있음
mutating 을 사용하여 인스턴스에 변화를 줄 수 있음 → 값 타입에서만 가능
프로토콜을 무조건 따라야 한다..? optional 하게 만들 수는 없나? → 가능하다..!
@objc 를 protocol 앞에 붙이고, @objc optional var xxx: String { get set } 하면 된다!
⇒ 프로토콜을 채택 시, 다음과 같은 에러를 표시! fix 를 눌러주면 바로 Person 프로토콜을 따르도록 수정 됨
protocol Person { var name: String { get } var age: Int { get set } func sleep(time: Int) -> String mutating func work(month: Int) } struct Worker: Person { let name: String var age: Int var salary: Int var money: Int func sleep(time: Int) -> String { "\(time)시간을 잤네..." } mutating func work(month: Int) { self.money += (self.salary * month) } } var worker = Worker(name: "Lee", age: 31, salary: 400, money: 1000) print(worker.sleep(time: 9)) // 9시간을 잤네... worker.work(month: 8) print(worker.money) // 4200
Swift
복사
위 예시와 동일한 Person 프로토콜을 class 에서 받으면, class 이기 때문에, init 혹은 name, age 의 값을 설정하라는 에러가 난다.
struct 와 다른 점은, work 메서드에서 mutating 이 빠져있다는 것. → class 는 참조 타입으로 mutating 사용을 하지 않는다.
아까 위에서, var 로 설정해야한다고 했는데, class 와 struct 의 name 은 let 으로 받았다. 프로토콜을 채택하는 곳에서, 저장 프로퍼티 or 연산 프로퍼티 를 결정하게 되는데, 연산 프로퍼티는 애초에 let 이 될 수 없다. ( 값이 변화할 것이니까! ) 그래서 애초에 규약을 let 으로 만들 수 없다고 생각한다. 그럼 채택할 때, 다 let 으로 할 수 있는거야? → Nope, 프로토콜에서 { get } 으로 했을 때, 가능하다. → And, { get } 으로 설정해도 채택하는 곳에서, getter 만 가지고 있는 연산 프로퍼티로 사용 가능 ⇒ 따라서, var name: String { get } 으로 설정한 프로퍼티는, 프로토콜이 채택된 곳에서 let 으로 사용이 가능했다.

protocol 도 일급 객체 ?

Swift 에서 클로저 / 함수 외에도, 프로토콜 또한 일급 객체 이다..
protocol 이 독립적인 하나의 타입으로 사용될 수 있기 때문 !
protocol Person { var name: String { get } }
Swift
복사
변수 / 상수 타입으로 선언
⇒ 근데, 프로토콜의 인스턴스를 생성 할 수가 없다..
⇒ 타입 캐스팅 !!
Person 으로 타입 캐스팅이 되어있기 때문에, student 의 school 에는 접근이 되지 않음
Person 프로토콜을 채택하지 않은 Homeless 구조체는 타입캐스팅을 위해서 as! 를 사용하라고 한다.
as 나 as? 는 에러
→ 결국 에러
인자 값, 반환 값 으로 사용
프토토콜을 채택한, struct, class, enum 을 프로토콜 타입으로 캐스팅 해서 사용한다 !
doAssignment 함수에 Person 프로토콜 타입을 인자 값, 반환 값 으로 되어있다.
student 를 인자로 넣었지만, 함수에서 리턴으로 인스턴스 값을 변경해서 student2 에 반환해주었다.
출력 시, 값이 잘 변경된 것을 볼 수 있다. → student 의 값은 어떻게 되었나? → 당연히 변경되지 않는다. struct 는 값 타입이니깐!
그리고, student2 또한 Person 프로토콜 타입으로 캐스팅 되었기 때문에 school 에 접근은 불가하다.

Protocol - extension

프로토콜에서는 메서드의 선언부만을 정의하는데…!
sleep 함수처럼, 반환 값이나.. 내부 로직이 똑같은 경우는 이렇게 반복되는 코드가 나오는데..
→ 그럴때 나올 수 있는게 extension
같은 protocol 을 채택한, 여러 구조체중에 한두개만 함수 내부 로직이 다를 것 같다..? → 그럼 해당 구조체 내부에 오버로딩이나, override 표시 없이 그냥 재정의 해주면 된다.
추가로 extension 으로, 프로퍼티에도 기본값을 줄 수 있다!
(연산 프로퍼티만 가능)

Generic

Generic

지정된 타입에 의존하지 않기 위해 사용 !
함수나, 타입에 사용 할 수 있음 !
제네릭 함수 예시
기존 코드
각 파라미터의 타입을 지정하기 때문에, 같은 로직의 함수를 여러개 만들어서 각각 사용
Generic 사용
이처럼 제네릭을 사용하여, 오버로딩을 하지 않고 불필요한 코드 중복을 제거할 수 있음 ! 타입 파라미터는 T 로 정해져 있는 것은 아니지만, 단일 대문자를 추천한다고 한다 ( 가독성을 위해 ! )
제네릭 타입 예시
기존 코드
Generic 사용

제네릭 확장 - extension

위와 같은 Stack 구조체에서, top 아이템을 가져오는 프로퍼티를 확장해서 만들기
Stack 의 제네릭 타입이 T 로 선언이 되어있기 때문에, 확장한 프로퍼티 ( 함수도! ) 에서 T 타입을 사용해야 한다 ! → Optional 하게 표시한 이유는 items.last 가 nil 일 수 있기 때문 -

제네릭 타입 제약

평소에 사용되는 여러 표현과 자료구조는 대부분 일정한 프로토콜을 준수하고 있음
→ Dictionary 는 (key, value) 를 갖고 있는데, key 는 Hashable 준수해야 함
→ Hashable 을 들어가보니, Equatable 을 채택하고 있다
Equatable 은 가끔씩 마주치는데, 우리가 제네릭 타입을 사용할때, 연산자 == 를 사용하려면 값을 비교할 수 있어야 한다는 에러가 나오기도 한다. 이때, <T: Equatable> 처럼 제약을 주기도 한다.
Equatable 처럼 프로토콜으로 제약을 줄 수 있지만, 클래스 제약을 제네릭 타입에 주는 것도 가능하다.
코드를 작성하면서 마주치게 되는 에러들이 많은데, 그럴때 그냥 fix 하고 넘기기보다, 내부에 선언을 한번 알아보는 것도 좋겠다 !

Associatedtype - 프로토콜에서의 제네릭

associatedtype

프로토콜을 제네릭하게 사용하는 방법
프로토콜 에서 사용되는 플레이스 홀더 타입
사용 방법
protocol Container { associatedtype Item var element: Item { get } mutating func append(_ item: Item) } /* protocol Container { associatedtype T var element: T { get } mutating func append(_ item: T) } */
Swift
복사
→ 프로토콜 사용 타입에서, typealias 로 지정
struct Bag: Container { typealias Item = String var element: String mutating func append(_ item: String) { } }
Swift
복사
// typealias 생략 -> 타입 추론 도 가능 struct Bag: Container { var element: String mutating func append(_ item: String) { } }
Swift
복사
→ 프로토콜 사용 타입에서, associatedtype 이 사용된 자리에 타입 지정
struct Box<T>: Container { var element: T mutating func append(_ item: T){ } }
Swift
복사
[ 추가 ]
앞서 설명한듯이 프로토콜은 assoiciatedtype 을 사용하여 제네릭을 사용한다.
위 오류 예시처럼, 프로토콜 자체에 제네릭을 선언하지도 않았고 assoiciatedtype 도 사용하지 않았지만, 아래 사진처럼, 프로토콜에 메서드를 선언할때, 메서드에 제네릭을 선언 할 수는 있음 !

associatedtype + 제약

타입 제약
struct Box<T: Equatable>: Container { var element: T mutating func append(_ item: T){ } }
Swift
복사
where 절 사용
struct Box<T>: Container where T: Equatable { var element: T mutating func append(_ item: T){ } }
Swift
복사
where 절에서는 associatedtype과 관련된 제약등 더 다양한 제약을 추가할 수 있음
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } } func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { if someContainer.count != anotherContainer.count { return false } for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } return true }
Swift
복사
C1 과 C2 컨테이너는 동일한 타입의 컨테이너일 필요는 없다. 하지만, 동일한 타입의 아이템들을 가지고 있어야 함! → 타입 제약과 제네릭 where절의 조합으로 표현
associatedtype을 사용한 프로토콜은 타입으로 사용이 불가 ?
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } } struct C: Container { var arr = [String]() typealias Item = String var count: Int { arr.count } mutating func append(_ item: String) { arr.append(item) } subscript(i: Int) -> String { arr[i] } }
Swift
복사
→ 이와 같이 나와서 any 를 붙여 보았는데….
안되는 것 같다..
Swift 5.7 에서 아래 예시를 봤는데 저 any 부분과, 어떻게 다르게 사용하는 것인지 알고 싶다..
protocol IntCollection: RangeReplaceableCollection where Self.Element == Int {} extension Array : IntCollection where Element == Int {} var array: any IntCollection = [3, 1, 4, 1, 5] array.append(9) print(array) // [3, 1, 4, 1, 5, 9]
Swift
복사
⇒ 이것저것 찾아보다보니, some & any 에 대해 학습을 해야하는걸 알았다..
Existential any …..
Opaque some ….
🥹
조금은 이해했는데, 아직은….! ㅎㅎ 수목금 중에 공유해보자..