โปรโตคอลไม่สอดคล้องกับตัวเอง?


126

เหตุใดจึงไม่รวบรวมรหัส Swift นี้

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension Array where Element : P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()

คอมไพเลอร์ระบุว่า: "Type Pไม่เป็นไปตามโปรโตคอลP" (หรือใน Swift เวอร์ชันที่ใหม่กว่า "ไม่รองรับ" การใช้ "P" เป็นคอนกรีตที่สอดคล้องกับโปรโตคอล "P" ")

ทำไมจะไม่ล่ะ? รู้สึกเหมือนเป็นช่องโหว่ในภาษาอย่างใด ฉันตระหนักดีว่าปัญหาเกิดจากการประกาศอาร์เรย์arrเป็นอาร์เรย์ประเภทโปรโตคอลแต่นั่นเป็นสิ่งที่ไม่สมควรทำหรือไม่? ฉันคิดว่ามีโปรโตคอลที่จะช่วยจัดหาโครงสร้างที่มีลำดับชั้นประเภทหรือไม่


1
เมื่อคุณลบคำอธิบายประกอบประเภทในlet arrบรรทัดคอมไพลเลอร์จะอนุมานประเภท[S]และโค้ดที่คอมไพล์ ดูเหมือนว่าจะไม่สามารถใช้ประเภทโปรโตคอลในลักษณะเดียวกับความสัมพันธ์ระดับซุปเปอร์คลาส
vadian

1
@vadian ถูกต้องนั่นคือสิ่งที่ฉันอ้างถึงในคำถามของฉันเมื่อฉันพูดว่า "ฉันตระหนักดีว่าปัญหาเกิดจากการประกาศอาร์เรย์ arr เป็นอาร์เรย์ประเภทโปรโตคอล" แต่ที่ผมไปพูดในคำถามของฉันจุดรวมของโปรโตคอลมักจะว่าพวกเขาสามารถนำมาใช้ในทางเดียวกันเป็นชั้น - ความสัมพันธ์ superclass! พวกเขามีจุดมุ่งหมายเพื่อจัดเตรียมโครงสร้างลำดับชั้นให้กับโลกของโครงสร้าง และมักจะทำ คำถามคือทำไมถึงไม่ทำงานที่นี่ ?
แม

1
ยังคงไม่ทำงานใน Xcode 7.1 แต่ข้อผิดพลาดอยู่ในขณะนี้"โดยใช้ 'P' เป็นชนิดที่เป็นรูปธรรมสอดคล้องกับโพรโทคอ 'P' ไม่สนับสนุน"
Martin R

1
@MartinR มันเป็นข้อความแสดงข้อผิดพลาดที่ดีกว่า แต่มันก็ยังรู้สึกกับฉันเหมือนรูในภาษา
แมตต์

แน่นอน! แม้ว่าprotocol P : Q { }P จะไม่เป็นไปตาม Q.
Martin R

คำตอบ:


66

แก้ไข: อีกสิบแปดเดือนในการทำงานกับ Swift รุ่นใหญ่อีกรุ่น (ที่ให้การวินิจฉัยใหม่) และความคิดเห็นจาก @AyBayBay ทำให้ฉันต้องการเขียนคำตอบนี้ใหม่ การวินิจฉัยใหม่คือ:

"ไม่รองรับการใช้" P "เป็นคอนกรีตที่สอดคล้องกับโปรโตคอล" P ""

นั่นทำให้เรื่องทั้งหมดนี้ชัดเจนขึ้นมาก ส่วนขยายนี้:

extension Array where Element : P {

ไม่ได้นำไปใช้เมื่อElement == Pตั้งแต่จะไม่ถือว่าเป็นรูปธรรมของความสอดคล้องP P(วิธีแก้ปัญหา "ใส่ไว้ในกล่อง" ด้านล่างยังคงเป็นวิธีแก้ปัญหาทั่วไปส่วนใหญ่)


คำตอบเก่า:

ยังเป็นอีกกรณีหนึ่งของประเภทข้อมูล สวิฟท์จริงๆอยากให้คุณที่จะได้รับประเภทคอนกรีตสำหรับส่วนมากสิ่งที่ไม่น่ารำคาญ [P]ไม่ใช่ประเภทที่เป็นรูปธรรม (คุณไม่สามารถจัดสรรบล็อกหน่วยความจำขนาดที่ทราบได้P) (ฉันไม่คิดว่ามันเป็นเรื่องจริงคุณสามารถสร้างบางอย่างที่มีขนาดได้Pเพราะมันทำผ่านทางทิศทาง ) ฉันไม่คิดว่าจะมีหลักฐานว่านี่เป็นกรณีที่ "ไม่ควร" ได้ผล กรณีนี้ดูเหมือนหนึ่งในกรณีที่ "ยังใช้ไม่ได้" (น่าเสียดายที่แทบจะเป็นไปไม่ได้เลยที่ Apple จะยืนยันความแตกต่างระหว่างกรณีเหล่านั้น) ความจริงที่ว่าArray<P>อาจเป็นประเภทตัวแปร (โดยที่Arrayไม่สามารถ) บ่งชี้ว่าพวกเขาทำงานในทิศทางนี้ไปแล้ว แต่ metatypes ของ Swift มีขอบคมจำนวนมากและกรณีที่ไม่ได้ใช้งาน ฉันไม่คิดว่าคุณจะได้คำตอบ "ทำไม" ที่ดีไปกว่านั้น "เพราะคอมไพเลอร์ไม่อนุญาต" (ไม่พอใจฉันรู้ทั้งชีวิตที่รวดเร็วของฉัน ... )

วิธีแก้ปัญหาคือการใส่ของลงในกล่องเกือบตลอดเวลา เราสร้างยางลบชนิดหนึ่ง

protocol P { }
struct S: P { }

struct AnyPArray {
    var array: [P]
    init(_ array:[P]) { self.array = array }
}

extension AnyPArray {
    func test<T>() -> [T] {
        return []
    }
}

let arr = AnyPArray([S()])
let result: [S] = arr.test()

เมื่อ Swift อนุญาตให้คุณทำสิ่งนี้โดยตรง (ซึ่งในที่สุดฉันก็คาดหวัง) มันน่าจะเป็นเพียงการสร้างกล่องนี้ให้คุณโดยอัตโนมัติ Recursive enums มีประวัติเช่นนี้ คุณต้องใส่กล่องเหล่านั้นและมันก็น่ารำคาญและ จำกัด อย่างไม่น่าเชื่อและในที่สุดคอมไพเลอร์ก็เพิ่มindirectเพื่อทำสิ่งเดียวกันโดยอัตโนมัติมากขึ้น


ข้อมูลที่เป็นประโยชน์มากมายในคำตอบนี้ แต่วิธีแก้ปัญหาจริงในคำตอบของ Tomohiro นั้นดีกว่าวิธีแก้ปัญหามวยที่นำเสนอที่นี่
jsadler

@jsadler คำถามไม่ใช่วิธีการแก้ไขข้อ จำกัด แต่ทำไมจึงมีข้อ จำกัด ที่จริงแล้วเท่าที่อธิบายไปวิธีแก้ปัญหาของ Tomohiro ทำให้เกิดคำถามมากกว่าที่จะตอบ หากเราใช้==ในตัวอย่าง Array ของฉันเราได้รับข้อผิดพลาดข้อกำหนด Same-type ทำให้พารามิเตอร์ทั่วไป 'Element' ไม่ใช่แบบทั่วไป "เหตุใดการใช้ Tomohiro จึงไม่==เกิดข้อผิดพลาดเดียวกัน
แมตต์

@Rob Napier ฉันยังงงกับคำตอบของคุณ Swift เห็นความเป็นรูปธรรมมากขึ้นในโซลูชันของคุณเทียบกับของเดิมได้อย่างไร ดูเหมือนคุณจะห่อสิ่งต่างๆไว้ในโครงสร้าง ... Idk บางทีฉันอาจกำลังดิ้นรนเพื่อทำความเข้าใจระบบประเภทที่รวดเร็ว แต่ทั้งหมดนี้ดูเหมือนว่าเวทมนตร์วูดู
AyBayBay

@AyBayBay คำตอบที่อัปเดต
Rob Napier

ขอบคุณมาก @RobNapier ฉันประหลาดใจเสมอกับความเร็วในการตอบกลับของคุณและค่อนข้างตรงไปตรงมาว่าคุณหาเวลาช่วยเหลือผู้คนได้มากแค่ไหน อย่างไรก็ตามการแก้ไขใหม่ของคุณทำให้เกิดมุมมองอย่างแน่นอน อีกสิ่งหนึ่งที่ฉันอยากจะชี้ให้เห็นการทำความเข้าใจเกี่ยวกับการลบประเภทยังช่วยฉันด้วย โดยเฉพาะบทความนี้ได้ผลงานที่ยอดเยี่ยม: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk ฉันรู้สึกอย่างไรกับบางส่วนของสิ่งนี้ ดูเหมือนว่าเรากำลังพิจารณาถึงช่องโหว่ในภาษา แต่ Idk จะสร้างสิ่งนี้ขึ้นมาได้อย่างไร
AyBayBay

110

ทำไมโปรโตคอลไม่สอดคล้องกับตัวเอง?

การอนุญาตให้โปรโตคอลสอดคล้องกับตัวเองในกรณีทั่วไปนั้นไม่เป็นผล ปัญหาอยู่ที่ข้อกำหนดของโปรโตคอลแบบคงที่

ซึ่งรวมถึง:

  • 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พารามิเตอร์นัยซึ่งเกิดขึ้นเบื้องหลังกับสมาชิกส่วนขยายโปรโตคอลทั้งหมด เมื่อเรียกใช้เมธอดดังกล่าวบนค่าที่พิมพ์โพรโทคอลPSwift จะขุดประเภทคอนกรีตที่อยู่เบื้องหลังและใช้สิ่งนี้เพื่อตอบสนองSelfตัวยึดตำแหน่งทั่วไป นี่คือเหตุผลที่เราไม่สามารถที่จะเรียกtakesConcreteP(_:)ด้วยself- เรากำลังสร้างความพึงพอใจกับTSelf

ซึ่งหมายความว่าตอนนี้เราสามารถพูดได้ว่า:

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ได้


2
บางทีฉันอาจจะเป็นคนหนาแน่น แต่ฉันไม่เข้าใจว่าทำไมเคสแบบคงที่ถึงพิเศษ เรา (คอมไพเลอร์) รู้คุณสมบัติคงที่ของโปรโตทอลมากหรือน้อยในขณะคอมไพล์ตามที่เรารู้เกี่ยวกับคุณสมบัติอินสแตนซ์ของโปรโตคอลกล่าวคือผู้ใช้จะนำไปใช้ แล้วความแตกต่างคืออะไร?
แมต

1
@matt อินสแตนซ์ที่พิมพ์ด้วยโปรโตคอล (เช่นอินสแตนซ์ที่พิมพ์ด้วยคอนกรีตที่ห่อหุ้มด้วยอัตถิภาวนิยมP) เป็นเรื่องปกติเพราะเราสามารถโอนสายไปยังข้อกำหนดของอินสแตนซ์ไปยังอินสแตนซ์พื้นฐานได้ อย่างไรก็ตามสำหรับประเภทโปรโตคอลเอง (เช่น a P.Protocolแท้จริงเป็นเพียงประเภทที่อธิบายโปรโตคอล) - ไม่มีผู้ใช้งานดังนั้นจึงไม่มีสิ่งใดที่จะเรียกข้อกำหนดแบบคงที่ซึ่งเป็นสาเหตุที่ในตัวอย่างข้างต้นเราไม่มีSomeGeneric<P>(มัน แตกต่างกันสำหรับP.Type(metatype อัตถิภาวนิยม) ซึ่งอธิบาย metatype ที่เป็นรูปธรรมของสิ่งที่สอดคล้องกับP- แต่นั่นเป็นอีกเรื่องหนึ่ง)
Hamish

คำถามที่ฉันถามที่ด้านบนสุดของหน้านี้คือเหตุใดผู้ใช้งานประเภทโปรโตคอลจึงใช้ได้ดีและไม่มีประเภทโปรโตคอล ฉันเข้าใจว่าสำหรับประเภทโปรโตคอลนั้นไม่มีการนำไปใช้ - สิ่งที่ฉันไม่เข้าใจคือเหตุใดการโอนสายแบบคงที่ไปยังประเภทการนำไปใช้จึงยากกว่าการส่งต่อการเรียกอินสแตนซ์ไปยังประเภทการนำไปใช้ คุณกำลังโต้แย้งว่าสาเหตุที่มีปัญหาที่นี่เป็นเพราะธรรมชาติของข้อกำหนดคงที่โดยเฉพาะ แต่ฉันไม่เห็นว่าความต้องการคงที่ยากกว่าข้อกำหนดของอินสแตนซ์อย่างไร
แมต

@ Matt ก็ไม่ได้ว่าต้องการแบบคงที่จะ "ยาก" กว่าความต้องการเช่น - คอมไพเลอร์สามารถจัดการทั้งปรับทั้งผ่าน existentials สำหรับกรณี (เช่นเช่นพิมพ์เป็นP) และ metatypes อัตถิภาวนิยม (เช่นP.Typemetatypes) ปัญหาคือสำหรับยาชื่อสามัญเราไม่ได้เปรียบเทียบการชอบเหมือนกัน เมื่อTเป็นPเช่นนั้นจะไม่มีประเภทคอนกรีต (meta) ที่จะส่งต่อข้อกำหนดคงที่ไปยัง ( Tคือ a P.Protocolไม่ใช่ a P.Type) ....
Hamish

1
ฉันไม่สนใจเรื่องความเสียง ฯลฯ จริงๆฉันแค่อยากเขียนแอพและถ้ารู้สึกว่ามันควรใช้งานได้ก็ควร ภาษาควรเป็นเพียงเครื่องมือไม่ใช่ผลิตภัณฑ์ หากมีบางกรณีที่ใช้ไม่ได้จริง ๆ ก็ไม่อนุญาตในกรณีเหล่านั้น แต่ให้ทุกคนใช้กรณีที่ใช้งานได้และปล่อยให้พวกเขาเขียนแอปต่อไป
โจนาธาน

17

หากคุณขยายCollectionTypeโปรโตคอลแทนArrayและ จำกัด โดยโปรโตคอลเป็นชนิดคอนกรีตคุณสามารถเขียนโค้ดก่อนหน้านี้ใหม่ได้ดังนี้

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension CollectionType where Generator.Element == P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()

ผมไม่คิดว่าการเก็บเทียบกับอาร์เรย์มีความเกี่ยวข้องที่นี่, การเปลี่ยนแปลงที่สำคัญคือการใช้VS== P : Pด้วย == ตัวอย่างเดิมก็ใช้ได้เช่นกัน และปัญหาที่อาจเกิดขึ้น (ขึ้นอยู่กับบริบท) ด้วย == คือมันไม่รวมโปรโตคอลย่อย: ถ้าฉันสร้าง a protocol SubP: Pแล้วกำหนดarrตาม[SubP]นั้นarr.test()จะไม่ทำงานอีกต่อไป (ข้อผิดพลาด: SubP และ P ต้องเทียบเท่า)
ถึง
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.