프로토콜이 왜 자신과 맞지 않습니까?
일반적인 경우에 프로토콜이 자신을 준수하도록 허용하는 것은 좋지 않습니다. 문제는 정적 프로토콜 요구 사항에 있습니다.
여기에는 다음이 포함됩니다.
static
방법과 속성
- 이니셜 라이저
- 연관된 유형 (현재는 실제 유형으로 프로토콜 사용을 방해하지만)
우리는 일반적인 자리 표시 자 T
에서 이러한 요구 사항에 액세스 할 수 있습니다. T : P
그러나 앞으로 구체적인 형식이 없으므로 프로토콜 유형 자체에서는 액세스 할 수 없습니다 . 그러므로 우리는 허용 할 수 없습니다 T
수 P
.
Array
확장을 적용 할 수 있도록 허용 한 경우 다음 예제에서 어떤 일이 발생하는지 고려하십시오 [P]
.
protocol P {
init()
}
struct S : P {}
struct S1 : P {}
extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}
var arr: [P] = [S(), S1()]
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()
우리는 가능하게 호출 할 수 없습니다 appendNew()
에 [P]
있기 때문에, P
((가) Element
) 구체적인 유형 아니므로 인스턴스화 할 수 없습니다. 그것은 해야한다 콘크리트 형식의 요소 유형의 요건에 적합와 배열에 호출 P
.
정적 메서드 및 속성 요구 사항과 비슷한 이야기입니다.
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
// If T is P, what's the value of bar? There isn't one – because there's no
// implementation of bar's getter defined on P itself.
print(T.bar)
T.foo() // If T is P, what method are we calling here?
}
}
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()
의 관점에서 이야기 할 수 없습니다 SomeGeneric<P>
. 우리는 (이없는 방법 통지 정적 프로토콜 요구 사항의 구체적인 구현해야 할 더 의 구현 foo()
또는 bar
위의 예에서 정의 된). P
확장 에서 이러한 요구 사항의 구현을 정의 할 수 있지만 이러한 요구 사항 은 구체적인 유형에 대해서만 정의됩니다. P
여전히 P
자체 호출 할 수는 없습니다 .
이 때문에 Swift는 프로토콜을 자체 요구 사항에 맞는 유형으로 사용하지 못하도록합니다. 프로토콜에 정적 요구 사항이있을 때는 그렇지 않기 때문입니다.
인스턴스 프로토콜 요구 사항은 프로토콜에 맞는 실제 인스턴스에서 호출 해야 하므로 문제가되지 않으므로 요구 사항을 구현해야합니다. 따라서로 유형이 지정된 인스턴스에서 요구 사항을 호출하면 해당 요구 사항 P
의 기본 구체 유형 구현으로 해당 호출을 전달할 수 있습니다.
그러나이 경우 규칙에 특별한 예외를두면 프로토콜이 일반 코드로 처리되는 방식에 놀라운 불일치가 발생할 수 있습니다. 그러나 현재 상황은 associatedtype
요구 사항과 크게 다르지 않습니다. 이는 현재 프로토콜을 유형으로 사용하지 못하게합니다. 정적 요구 사항이있을 때 프로토콜 자체를 따르는 유형으로 프로토콜을 사용하지 못하게하는 제한이 있으면 향후 버전의 언어에 대한 옵션이 될 수 있습니다.
편집 : 그리고 아래에서 살펴본 것처럼 이것은 스위프트 팀이 목표로하는 것과 같습니다.
@objc
프로토콜
사실 실제로 언어가 프로토콜을 처리하는 방식과 정확히 같습니다@objc
. 정적 요구 사항이 없으면 자체적으로 준수합니다.
다음은 잘 컴파일됩니다.
import Foundation
@objc protocol P {
func foo()
}
class C : P {
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c)
baz
그 요구 T
에 부합를 P
; 그러나 우리는에서 대체 할 수 있습니다 P
에 대한 T
때문에 P
정적 요구 사항이 없습니다. 에 정적 요구 사항을 추가 P
하면 예제가 더 이상 컴파일되지 않습니다.
import Foundation
@objc protocol P {
static func bar()
func foo()
}
class C : P {
static func bar() {
print("C's bar called")
}
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'
따라서이 문제에 대한 한 가지 해결 방법은 프로토콜을 만드는 것입니다 @objc
. 물론 이것은 Obj-C 런타임을 요구할뿐만 아니라 적합한 유형을 클래스로 강제해야하므로 Linux와 같은 Apple 이외의 플랫폼에서는 실행 가능하지 않기 때문에 많은 경우에 이상적인 해결 방법은 아닙니다.
그러나이 제한이 언어가 프로토콜에 대해 '정적 요구 사항없이 프로토콜을 자체적으로 준수'하는 이미 구현 한 주된 이유 중 하나라고 생각 @objc
합니다. 컴파일러에 의해 작성된 일반적인 코드는 크게 단순화 될 수 있습니다.
왜? 때문에 @objc
프로토콜 형식의 값은 그 요구 사항을 사용하여 파견 효과적으로 단지 클래스 참조입니다 objc_msgSend
. 반대로 @objc
프로토콜이 아닌 유형의 값은 (잠재적으로 저장된) 랩핑 된 값의 메모리를 관리하고 다른 구현을 호출 할 구현을 결정하기 위해 값과 감시 테이블을 모두 가지고 있기 때문에 더 복잡합니다. 요구 사항.
때문에이 단순화 표현의 @objc
프로토콜 같은 프로토콜 유형의 값은 P
유형의 '일반적인 값'몇 가지 일반적인 자리와 같은 메모리 표현을 공유 할 수 있습니다 T : P
, 아마도 자기 적합성을 허용하도록 스위프트 팀이 쉽게 그것을 만드는. 비- @objc
프로토콜에 대해서도 마찬가지 이지만, 이러한 일반 값에는 현재 값 또는 프로토콜 감시 테이블이 없습니다.
그러나이 기능 은 의도적이며 @objc
Swift 팀 구성원 Slava Pestov 가 SR-55 에 대한 귀하의 질문에 대한 답변 ( 이 질문에 의해 표시됨) 에 의해 확인 된 것처럼 비 프로토콜 로 롤아웃되기를 바랍니다 .
Matt Neuburg 님이 댓글을 추가했습니다-2017 년 9 월 7 일 오후 1시 33 분
이것은 컴파일합니다 :
@objc protocol P {}
class C: P {}
func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }
추가 @objc
하면 컴파일됩니다. 제거하면 다시 컴파일되지 않습니다. Stack Overflow를 통해 우리 중 일부는이 놀라운 것을 발견하고 그것이 의도적인지 또는 버그가있는 경우인지 알고 싶습니다.
Slava Pestov는 코멘트를 추가했습니다-2017 년 9 월 7 일 오후 1시 53 분
고의적으로이 제한을 해제하는 것이이 버그에 관한 것입니다. 내가 말했듯이 그것은 까다 롭고 아직 구체적인 계획이 없습니다.
언젠가 언젠가는 언어가 비 @objc
프로토콜에 대해서도 지원할 수 있기를 바랍니다.
그러나 비 @objc
프로토콜에 대한 현재 솔루션은 무엇 입니까?
프로토콜 제약 조건으로 확장 구현
Swift 3.1에서, 주어진 일반 플레이스 홀더 또는 관련 타입이 주어진 프로토콜 타입이어야한다는 제약 조건을 가진 확장을 원한다면 (그 프로토콜에 맞는 콘크리트 타입이 아니라) – ==
제약 조건으로 간단히 정의 할 수 있습니다 .
예를 들어 배열 확장을 다음과 같이 작성할 수 있습니다.
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
물론 이것은 이제 우리가에 맞는 콘크리트 타입 요소를 가진 배열에서 그것을 호출하는 것을 막습니다 P
. when에 대한 추가 확장을 정의하고 확장으로 Element : P
넘어 가면 이를 해결할 수 == P
있습니다.
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
그러나 이것은 [P]
각 요소가 존재하는 컨테이너에 박스되어야하기 때문에 배열을 a로 변환하는 O (n) 변환을 수행한다는 점은 주목할 가치가 있습니다. 성능에 문제가있는 경우 확장 방법을 다시 구현하여 간단히 해결할 수 있습니다. 이것은 완전히 만족스러운 해결책 은 아닙니다 . 향후 언어 버전에 '프로토콜 유형 또는 프로토콜 유형에 따른 것'제약 조건 을 표현하는 방법이 포함되기를 바랍니다 .
Swift 3.1 이전에는 Rob 이 그의 답변에서 알 수 있듯이 이것을 달성하는 가장 일반적인 방법은 단순히에 대한 래퍼 유형을 작성하는 [P]
것입니다. 그런 다음 확장 메소드를 정의 할 수 있습니다.
프로토콜 형식의 인스턴스를 제한된 일반 자리 표시 자에 전달
다음과 같은 상황을 고려하십시오.
protocol P {
var bar: Int { get set }
func foo(str: String)
}
struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}
func takesConcreteP<T : P>(_ t: T) {/* ... */}
let p: P = S(bar: 5)
// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)
우리는 전달할 수 없습니다 p
에 takesConcreteP(_:)
우리가 현재 대체 할 수 없기 때문에, P
일반적인 자리를 위해 T : P
. 이 문제를 해결할 수있는 몇 가지 방법을 살펴 보겠습니다.
1. 실존 존재
에 P
대한 대체 를 시도하는 대신 T : P
유형이 P
지정된 래핑 된 콘크리트 유형을 파고 대체 할 수 있다면 어떨까요? 안타깝게도이 옵션은 현재 사용자가 직접 사용할 수없는 opening existentials 라는 언어 기능이 필요 합니다.
그러나 Swift 는 멤버에 액세스 할 때 암시 적으로 존재 항목 (프로토콜 유형 값)을 엽니 다 (즉, 런타임 유형을 발굴하여 일반 플레이스 홀더 형식으로 액세스 가능하게 함). 다음의 프로토콜 확장에서이 사실을 활용할 수 있습니다 P
.
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Self
확장 메서드가 사용 하는 암시 적 일반 자리 표시 자에 주의하십시오 . 이는 암시 적 self
매개 변수 를 입력하는 데 사용됩니다. 이는 모든 프로토콜 확장 멤버가있는 장면에서 발생합니다. 프로토콜 유형의 value에서 이러한 메소드를 호출 할 때 P
Swift는 기본 콘크리트 유형을 발굴하고이를 사용하여 Self
일반 자리 표시자를 만족시킵니다 . 우리가 호출 할 수있는 이유입니다 takesConcreteP(_:)
함께 self
우리가 만족하고 - T
와 Self
.
이것은 우리가 지금 말할 수 있음을 의미합니다 :
p.callTakesConcreteP()
그리고 기본 콘크리트 유형 (이 경우 ) 에 의해 takesConcreteP(_:)
일반 자리 표시자가 T
충족되어 호출됩니다 S
. 구체적인 유형을 대체하는 것이 아니라 "자체에 맞는 프로토콜"이 아닙니다. 프로토콜 P
에 정적 요구 사항을 추가하고 내부에서 호출 할 때 발생하는 상황을 확인하십시오 takesConcreteP(_:)
.
Swift가 프로토콜이 자체 규정을 준수하지 못하게하는 경우, 다음 최선의 대안은 프로토콜을 일반 유형의 매개 변수에 대한 인수로 전달하려고 할 때 암시 적으로 존재 성을 여는 것입니다.
그러나 존재를 여는 것이 프로토콜 자체에 맞지 않는 문제에 대한 일반적인 해결책은 아닙니다. 프로토콜 유형 값의 이기종 콜렉션을 다루지 않으며, 각각 다른 기본 콘크리트 유형을 가질 수 있습니다. 예를 들어, 다음을 고려하십시오.
struct Q : P {
var bar: Int
func foo(str: String) {}
}
// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]
// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)
같은 이유로 여러 T
매개 변수를 가진 함수 도 문제가 될 수 있습니다. 매개 변수는 동일한 유형의 인수를 가져와야합니다. 그러나 P
값 이 두 개인 경우 컴파일 타임에 둘 다 동일한 기본 콘크리트를 가지고 있다고 보장 할 수있는 방법이 없습니다 유형.
이 문제를 해결하기 위해 타입 지우개를 사용할 수 있습니다.
2. 지우개 작성
마찬가지로 롭 말한다 하는 타입 지우개 , 프로토콜 자체에 부합하지 않는 문제에 대한 가장 일반적인 솔루션입니다. 이를 통해 인스턴스 요구 사항을 기본 인스턴스로 전달하여 프로토콜 유형의 인스턴스를 해당 프로토콜에 맞는 구체적인 유형으로 래핑 할 수 있습니다.
따라서 P
인스턴스 요구 사항을 다음과 같은 기본 임의 인스턴스에 전달하는 유형 지우기 상자를 작성해 보겠습니다 P
.
struct AnyP : P {
private var base: P
init(_ base: P) {
self.base = base
}
var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}
func foo(str: String) { base.foo(str: str) }
}
이제 우리는 AnyP
대신에 P
다음 과 같이 말할 수 있습니다 .
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
이제 왜 우리가 그 상자를 만들어야하는지 잠시 생각해보십시오. 앞서 논의했듯이 Swift에는 프로토콜에 정적 요구 사항이있는 경우 구체적인 유형이 필요합니다. P
정적 요구 사항이 있는지 고려하십시오 –에 구현해야했습니다 AnyP
. 그러나 무엇으로 구현 되었습니까? P
여기서 준수하는 임의의 인스턴스를 처리 하고 있습니다. 기본 콘크리트 유형이 정적 요구 사항을 구현하는 방법을 알지 못하므로이를로 의미있게 표현할 수 없습니다 AnyP
.
따라서이 경우 솔루션은 인스턴스 프로토콜 요구 사항 의 경우에만 실제로 유용 합니다. 일반적인 경우, 우리는 여전히을 P
따르는 구체적인 유형으로 취급 할 수 없습니다 P
.
let arr
줄 에서 형식 주석을 제거하면 컴파일러에서 형식을 유추[S]
하고 코드를 컴파일합니다. 프로토콜 유형은 클래스-슈퍼 클래스 관계와 같은 방식으로 사용할 수없는 것 같습니다.