ทำไมโปรโตคอลไม่สอดคล้องกับตัวเอง?
การอนุญาตให้โปรโตคอลสอดคล้องกับตัวเองในกรณีทั่วไปนั้นไม่เป็นผล ปัญหาอยู่ที่ข้อกำหนดของโปรโตคอลแบบคงที่
ซึ่งรวมถึง:
static
วิธีการและคุณสมบัติ
- Initialisers
- ประเภทที่เกี่ยวข้อง (แม้ว่าในปัจจุบันจะป้องกันไม่ให้ใช้โปรโตคอลเป็นประเภทจริงก็ตาม)
เราสามารถเข้าถึงข้อกำหนดเหล่านี้ได้จากตัวยึดตำแหน่งทั่วไป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()
ใช้ a [P]
ได้เนื่องจากP
(the 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
ข้อกำหนดซึ่ง (ปัจจุบัน) ป้องกันไม่ให้คุณใช้โปรโตคอลเป็นประเภท การมีข้อ จำกัด ที่ป้องกันไม่ให้คุณใช้โปรโตคอลเป็นประเภทที่สอดคล้องกับตัวมันเองเมื่อมีข้อกำหนดคงที่อาจเป็นตัวเลือกสำหรับภาษาในอนาคต
แก้ไข:และจากการสำรวจด้านล่างสิ่งนี้ดูเหมือนว่าทีม Swift ตั้งเป้าไว้
@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 ดังนั้นจึงไม่สามารถใช้งานได้บนแพลตฟอร์มที่ไม่ใช่ของ Apple เช่น Linux
แต่ฉันสงสัยว่าข้อ จำกัด นี้เป็น (หนึ่งใน) สาเหตุหลักว่าทำไมภาษาจึงใช้โปรโตคอล 'โดยไม่มีข้อกำหนดคงที่สอดคล้องกับตัวมันเอง' สำหรับ@objc
โปรโตคอล โค้ดทั่วไปที่เขียนขึ้นรอบ ๆ สามารถทำให้ง่ายขึ้นโดยคอมไพเลอร์
ทำไม? เพราะค่าโปรโตคอลการพิมพ์ได้อย่างมีประสิทธิภาพเพียงการอ้างอิงระดับที่มีความต้องการจะถูกส่งโดยใช้@objc
objc_msgSend
ในทางกลับกันค่าที่ไม่ใช่@objc
โปรโตคอลมีความซับซ้อนมากขึ้นเนื่องจากมีทั้งตารางค่าและพยานเพื่อจัดการหน่วยความจำของค่าที่ห่อไว้ (ที่อาจจัดเก็บโดยทางอ้อม) และกำหนดสิ่งที่จะเรียกใช้สำหรับสิ่งที่แตกต่างกัน ข้อกำหนดตามลำดับ
เนื่องจากการแสดง@objc
โปรโตคอลที่ง่ายขึ้นนี้ค่าของประเภทโปรโตคอลดังกล่าวP
สามารถแบ่งปันการแสดงหน่วยความจำเดียวกันกับ 'ค่าทั่วไป' ของตัวยึดตำแหน่งทั่วไปบางประเภทT : P
ซึ่งน่าจะทำให้ทีม Swift อนุญาตให้สอดคล้องกับตนเองได้ง่าย สิ่งเดียวกันนี้ไม่เป็นความจริงสำหรับผู้ที่ไม่ใช่@objc
โปรโตคอลอย่างไรก็ตามเนื่องจากค่าทั่วไปดังกล่าวไม่ได้นำเสนอค่าหรือตารางพยานโปรโตคอล
อย่างไรก็ตามคุณลักษณะนี้เป็นไปโดยเจตนาและหวังว่าจะถูกนำไปใช้กับ@objc
โปรโตคอลที่ไม่ใช่ตามที่สมาชิกทีม Swift ยืนยัน Slava Pestov ในความคิดเห็นของ SR-55เพื่อตอบคำถามของคุณเกี่ยวกับเรื่องนี้ (ถามโดยคำถามนี้ ):
Matt Neuburg ได้เพิ่มความคิดเห็น - 7 ก.ย. 2560 13: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 ได้เพิ่มความคิดเห็น - 7 ก.ย. 2560 13: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
แน่นอนตอนนี้ป้องกันไม่ให้เราเรียกมันว่าในอาร์เรย์ที่มีองค์ประกอบประเภทคอนกรีตที่สอดคล้องกับผู้ เราสามารถแก้ปัญหานี้ได้โดยการกำหนดส่วนขยายเพิ่มเติมสำหรับเวลา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()
อย่างไรก็ตามเป็นที่น่าสังเกตว่าการดำเนินการนี้จะทำการแปลง O (n) ของอาร์เรย์เป็น a [P]
เนื่องจากแต่ละองค์ประกอบจะต้องอยู่ในกล่องในคอนเทนเนอร์ที่มีอยู่ หากประสิทธิภาพเป็นปัญหาคุณสามารถแก้ไขได้โดยการนำวิธีส่วนขยายไปใช้ใหม่ นี่ไม่ใช่วิธีแก้ปัญหาที่น่าพอใจอย่างสิ้นเชิงหวังว่าภาษาในอนาคตจะมีวิธีแสดงข้อ จำกัด "ประเภทโปรโตคอลหรือสอดคล้องกับประเภทโปรโตคอล"
ก่อนหน้า Swift 3.1 วิธีทั่วไปที่สุดในการบรรลุสิ่งนี้ดังที่ Rob แสดงในคำตอบของเขาคือเพียงแค่สร้างประเภท wrapper สำหรับ a [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
ค่าพิมพ์เป็นห่อและทดแทนที่แทน? น่าเสียดายที่สิ่งนี้ต้องใช้คุณลักษณะภาษาที่เรียกว่าการเปิดอัตถิภาวนิยมซึ่งปัจจุบันผู้ใช้ไม่สามารถใช้ได้โดยตรง
อย่างไรก็ตาม Swift จะเปิดอัตถิภาวนิยม (ค่าที่พิมพ์ด้วยโปรโตคอล) โดยปริยายเมื่อเข้าถึงสมาชิกในพวกเขา (กล่าวคือมันขุดประเภทรันไทม์ออกและทำให้สามารถเข้าถึงได้ในรูปแบบของตัวยึดตำแหน่งทั่วไป) เราสามารถใช้ประโยชน์จากข้อเท็จจริงนี้ได้ในส่วนขยายโปรโตคอลบนP
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
สังเกตSelf
ตัวยึดตำแหน่งทั่วไปโดยนัยที่เมธอดส่วนขยายใช้ซึ่งใช้ในการพิมพ์self
พารามิเตอร์นัยซึ่งเกิดขึ้นเบื้องหลังกับสมาชิกส่วนขยายโปรโตคอลทั้งหมด เมื่อเรียกใช้เมธอดดังกล่าวบนค่าที่พิมพ์โพรโทคอล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]
และโค้ดที่คอมไพล์ ดูเหมือนว่าจะไม่สามารถใช้ประเภทโปรโตคอลในลักษณะเดียวกับความสัมพันธ์ระดับซุปเปอร์คลาส