Tại sao các giao thức không phù hợp với chính họ?
Cho phép các giao thức phù hợp với chính họ trong trường hợp chung là không có cơ sở. Vấn đề nằm ở yêu cầu giao thức tĩnh.
Bao gồm các:
static
phương pháp và tính chất
- Người khởi tạo
- Các loại liên kết (mặc dù các loại này hiện ngăn chặn việc sử dụng giao thức như một loại thực tế)
Chúng tôi có thể truy cập các yêu cầu này trên một trình giữ chỗ chung T
trong đó T : P
- tuy nhiên chúng tôi không thể truy cập chúng trên chính loại giao thức, vì không có loại tuân thủ cụ thể nào để chuyển tiếp. Vì vậy, chúng tôi không thể cho phép T
được P
.
Xem xét những gì sẽ xảy ra trong ví dụ sau nếu chúng tôi cho phép Array
tiện ích mở rộng được áp dụng cho [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()
Chúng tôi không thể gọi appendNew()
một [P]
, bởi vì P
(the Element
) không phải là một loại cụ thể và do đó không thể được khởi tạo. Nó phải được gọi trên một mảng với các phần tử được gõ cụ thể, trong đó kiểu đó phù hợp với P
.
Đó là một câu chuyện tương tự với các yêu cầu thuộc tính và phương thức tĩnh:
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()
Chúng tôi không thể nói chuyện về SomeGeneric<P>
. Chúng ta cần triển khai cụ thể các yêu cầu giao thức tĩnh (chú ý cách không có triển khai foo()
hoặc bar
được định nghĩa trong ví dụ trên). Mặc dù chúng ta có thể định nghĩa việc triển khai các yêu cầu này trong một P
phần mở rộng, nhưng chúng chỉ được xác định cho các loại cụ thể phù hợp với P
- bạn vẫn không thể tự gọi chúng P
.
Bởi vì điều này, Swift hoàn toàn không cho phép chúng tôi sử dụng một giao thức như một loại phù hợp với chính nó - bởi vì khi giao thức đó có các yêu cầu tĩnh, thì không.
Các yêu cầu giao thức sơ thẩm không có vấn đề gì, vì bạn phải gọi chúng theo một thực thể thực tế phù hợp với giao thức (và do đó phải thực hiện các yêu cầu). Vì vậy, khi gọi một yêu cầu trên một thể hiện được gõ là P
, chúng ta có thể chuyển tiếp cuộc gọi đó đến việc triển khai yêu cầu đó của loại cụ thể.
Tuy nhiên, việc đưa ra các ngoại lệ đặc biệt cho quy tắc trong trường hợp này có thể dẫn đến sự không nhất quán đáng ngạc nhiên về cách các giao thức được xử lý bằng mã chung. Mặc dù điều đó đang được nói, tình huống không quá giống với associatedtype
các yêu cầu - điều này (hiện tại) ngăn bạn sử dụng một giao thức như một loại. Có một hạn chế ngăn bạn sử dụng một giao thức là một loại phù hợp với chính nó khi nó có các yêu cầu tĩnh có thể là một tùy chọn cho phiên bản ngôn ngữ trong tương lai
Chỉnh sửa: Và như được khám phá dưới đây, nó trông giống như những gì nhóm Swift đang hướng tới.
@objc
giao thức
Và trên thực tế, đó chính xác là cách ngôn ngữ xử lý các @objc
giao thức. Khi họ không có yêu cầu tĩnh, họ tuân thủ chính họ.
Các biên dịch sau đây tốt:
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
đòi hỏi phải T
phù hợp với P
; nhưng chúng ta có thể thay thế trong P
cho T
vì P
không có những yêu cầu tĩnh. Nếu chúng ta thêm một yêu cầu tĩnh vào P
, ví dụ không còn biên dịch:
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)'
Vì vậy, một cách giải quyết cho vấn đề này là tạo giao thức của bạn @objc
. Cấp, đây không phải là một cách giải quyết lý tưởng trong nhiều trường hợp, vì nó buộc các loại tuân thủ của bạn phải là các lớp, cũng như yêu cầu thời gian chạy Obj-C, do đó không thể thực hiện được trên các nền tảng không phải của Apple như Linux.
Nhưng tôi nghi ngờ rằng giới hạn này là (một trong) lý do chính tại sao ngôn ngữ đã thực hiện 'giao thức mà không có yêu cầu tĩnh phù hợp với chính nó' cho các @objc
giao thức. Mã chung được viết xung quanh chúng có thể được đơn giản hóa đáng kể bởi trình biên dịch.
Tại sao? Bởi vì @objc
các giá trị gõ giao thức thực sự chỉ là các tham chiếu lớp có yêu cầu được gửi đi bằng cách sử dụng objc_msgSend
. Mặt khác, các @objc
giá trị không được gõ giao thức phức tạp hơn, vì chúng mang theo cả hai bảng giá trị và chứng kiến để vừa quản lý bộ nhớ của giá trị được gói (có khả năng được lưu trữ gián tiếp) của chúng và để xác định cách triển khai nào cần gọi khác yêu cầu, tương ứng.
Do cách biểu diễn đơn giản hóa này cho các @objc
giao thức, một giá trị của loại giao thức như vậy P
có thể chia sẻ cùng biểu diễn bộ nhớ như một 'giá trị chung' của một số trình giữ chỗ chung T : P
, có lẽ giúp nhóm Swift dễ dàng cho phép tự tuân thủ. Điều này cũng không đúng đối với các @objc
giao thức không phải là giao thức, tuy nhiên, vì các giá trị chung này hiện không mang theo các bảng chứng kiến giá trị hoặc giao thức.
Tuy nhiên, tính năng này là có chủ ý và hy vọng sẽ được triển khai cho các @objc
giao thức không , như được xác nhận bởi thành viên nhóm Swift, Slava Pestov trong các nhận xét của SR-55 khi trả lời câu hỏi của bạn về câu hỏi này (được nhắc bởi câu hỏi này ):
Matt Neuburg đã thêm một bình luận - 7 tháng 9 năm 2017 1:33 PM
Điều này không biên dịch:
@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) }
Thêm @objc
làm cho nó biên dịch; loại bỏ nó làm cho nó không biên dịch lại. Một số người trong chúng tôi trên Stack Overflow thấy điều này đáng ngạc nhiên và muốn biết liệu đó có phải là lỗi cố ý hay trường hợp lỗi không.
Slava Pestov đã thêm một bình luận - 7 tháng 9 năm 2017 1:53 CH
Đó là cố ý - dỡ bỏ hạn chế này là những gì lỗi này là về. Như tôi đã nói nó khó khăn và chúng tôi chưa có kế hoạch cụ thể nào.
Vì vậy, hy vọng rằng một ngày nào đó ngôn ngữ cũng sẽ hỗ trợ cho các @objc
giao thức không phải là giao thức.
Nhưng những giải pháp hiện tại nào dành cho các @objc
giao thức không?
Triển khai các phần mở rộng với các ràng buộc giao thức
Trong Swift 3.1, nếu bạn muốn một tiện ích mở rộng có ràng buộc rằng một trình giữ chỗ chung chung hoặc loại được liên kết phải là một loại giao thức nhất định (không chỉ là một loại cụ thể phù hợp với giao thức đó) - bạn có thể chỉ cần định nghĩa điều này bằng một ==
ràng buộc.
Ví dụ: chúng tôi có thể viết phần mở rộng mảng của bạn dưới dạng:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Tất nhiên, điều này bây giờ ngăn chúng ta gọi nó trên một mảng với các phần tử loại cụ thể phù hợp với P
. Chúng tôi có thể giải quyết vấn đề này bằng cách chỉ xác định một tiện ích mở rộng bổ sung khi nào Element : P
và chỉ cần chuyển tiếp vào == P
tiện ích mở rộng:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Tuy nhiên, điều đáng chú ý là điều này sẽ thực hiện chuyển đổi O (n) của mảng thành a [P]
, vì mỗi phần tử sẽ phải được đóng hộp trong một thùng chứa tồn tại. Nếu hiệu suất là một vấn đề, bạn chỉ cần giải quyết vấn đề này bằng cách triển khai lại phương thức mở rộng. Đây không phải là một giải pháp hoàn toàn thỏa đáng - hy vọng phiên bản ngôn ngữ trong tương lai sẽ bao gồm cách thể hiện 'loại giao thức hoặc tuân thủ ràng buộc của loại giao thức'.
Trước Swift 3.1, cách tổng quát nhất để đạt được điều này, như Rob thể hiện trong câu trả lời của mình , chỉ đơn giản là xây dựng một loại trình bao bọc cho a [P]
, sau đó bạn có thể xác định (các) phương thức tiện ích mở rộng của mình.
Truyền một thể hiện gõ giao thức cho một trình giữ chỗ chung bị ràng buộc
Hãy xem xét tình huống sau đây (có thể xảy ra, nhưng không phổ biến):
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)
Chúng ta không thể vượt qua p
để takesConcreteP(_:)
, vì chúng tôi không thể hiện thay thếP
cho một trình giữ chỗ chung T : P
. Chúng ta hãy xem một vài cách để chúng ta có thể giải quyết vấn đề này.
1. Mở hiện sinh
Hơn là cố gắng thay thế P
cho T : P
, những gì nếu chúng ta có thể thâm nhập vào các kiểu dữ liệu cụ cơ bản mà các P
giá trị đánh máy đã đóng gói và thay thế đó để thay thế? Thật không may, điều này đòi hỏi một tính năng ngôn ngữ gọi là mở hiện sinh , hiện không có sẵn cho người dùng.
Tuy nhiên, Swift thực hiện ngầm existentials mở (giá trị giao thức, đánh máy) khi truy cập vào các thành viên trên chúng (tức là nó đào ra kiểu thời gian chạy và làm cho nó dễ tiếp cận theo hình thức một placeholder generic). Chúng ta có thể khai thác thực tế này trong một phần mở rộng giao thức trên P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Lưu ý Self
trình giữ chỗ chung ẩn mà phương thức tiện ích mở rộng, được sử dụng để nhập self
tham số ẩn - điều này xảy ra đằng sau hậu trường với tất cả các thành viên mở rộng giao thức. Khi gọi một phương thức như vậy trên một giá trị gõ giao thức P
, Swift đào ra loại bê tông cơ bản và sử dụng phương thức này để đáp ứng Self
trình giữ chỗ chung. Đây là lý do tại sao chúng tôi có thể gọi takesConcreteP(_:)
với self
- chúng tôi hài lòng T
với Self
.
Điều này có nghĩa là bây giờ chúng ta có thể nói:
p.callTakesConcreteP()
Và takesConcreteP(_:)
được gọi với trình giữ chỗ chung T
được thỏa mãn bởi loại bê tông cơ bản (trong trường hợp này S
). Lưu ý rằng đây không phải là "giao thức phù hợp với chính họ", vì chúng tôi thay thế một loại cụ thể thay vì P
- thử thêm một yêu cầu tĩnh vào giao thức và xem điều gì xảy ra khi bạn gọi nó từ bên trong takesConcreteP(_:)
.
Nếu Swift tiếp tục không cho phép các giao thức tuân thủ chính mình, thì giải pháp thay thế tốt nhất tiếp theo sẽ hoàn toàn mở ra các tồn tại khi cố gắng chuyển chúng thành đối số cho các tham số của loại chung - thực hiện chính xác những gì trampoline mở rộng giao thức của chúng tôi đã làm, chỉ cần không có bản tóm tắt.
Tuy nhiên, lưu ý rằng việc mở các tồn tại không phải là một giải pháp chung cho vấn đề giao thức không tuân thủ chính chúng. Nó không xử lý các tập hợp không đồng nhất của các giá trị gõ giao thức, tất cả có thể có các loại cụ thể cơ bản khác nhau. Ví dụ: xem xét:
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)
Vì những lý do tương tự, một hàm có nhiều T
tham số cũng sẽ có vấn đề, vì các tham số phải lấy các đối số cùng loại - tuy nhiên nếu chúng ta có hai P
giá trị, không có cách nào chúng ta có thể đảm bảo tại thời điểm biên dịch rằng cả hai đều có cùng một bê tông cơ bản kiểu.
Để giải quyết vấn đề này, chúng ta có thể sử dụng một loại tẩy.
2. Xây dựng một loại tẩy
Như Rob nói , một loại tẩy , là giải pháp chung nhất cho vấn đề giao thức không tuân thủ chính họ. Chúng cho phép chúng ta bọc một thể hiện gõ giao thức theo một kiểu cụ thể phù hợp với giao thức đó, bằng cách chuyển tiếp các yêu cầu thể hiện đến thể hiện bên dưới.
Vì vậy, hãy xây dựng một hộp xóa loại chuyển tiếp P
các yêu cầu cá thể lên một thể hiện tùy ý cơ bản phù hợp với 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) }
}
Bây giờ chúng ta chỉ có thể nói về mặt AnyP
thay vì P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Bây giờ, hãy xem xét tại sao chúng ta phải xây dựng cái hộp đó. Như chúng ta đã thảo luận sớm, Swift cần một loại cụ thể cho các trường hợp giao thức có yêu cầu tĩnh. Xem xét nếu P
có một yêu cầu tĩnh - chúng tôi sẽ cần phải thực hiện điều đó trong AnyP
. Nhưng nó nên được thực hiện như thế nào? Chúng tôi đang xử lý các trường hợp tùy ý tuân thủ P
ở đây - chúng tôi không biết về cách các loại bê tông cơ bản của chúng thực hiện các yêu cầu tĩnh, do đó chúng tôi không thể diễn đạt điều này một cách có ý nghĩa trongAnyP
.
Do đó, giải pháp trong trường hợp này chỉ thực sự hữu ích trong trường hợp yêu cầu giao thức thể hiện . Trong trường hợp chung, chúng ta vẫn không thể coi P
là một loại bê tông phù hợp với P
.
let arr
dòng, trình biên dịch sẽ nhập kiểu vào[S]
và mã biên dịch. Có vẻ như một loại giao thức không thể được sử dụng giống như một mối quan hệ siêu lớp.