มีข้อมูลสำคัญเฉพาะสองอย่างของ Swift ที่หายไปจากคำตอบที่มีอยู่ซึ่งฉันคิดว่าช่วยให้ชัดเจนขึ้น
- หากโปรโตคอลระบุตัวกำหนดค่าเริ่มต้นเป็นวิธีการที่จำเป็นตัวเริ่มต้นนั้นจะต้องถูกทำเครื่องหมายโดยใช้
required
คำหลักของ Swift
- Swift มีกฎการสืบทอดชุดพิเศษเกี่ยวกับ
init
วิธีการ
TL; DRคือ:
หากคุณใช้ initializers ใด ๆ คุณจะไม่ได้รับค่าเริ่มต้นที่กำหนดไว้ของ superclass อีกต่อไป
initializers เพียงตัวเดียวที่คุณจะได้รับนั้นเป็น initializers อำนวยความสะดวกระดับซุปเปอร์ที่ชี้ไปที่ initializer ที่กำหนดซึ่งคุณเกิดขึ้นเพื่อแทนที่
ดังนั้น ... พร้อมแล้วสำหรับรุ่นยาว
Swift มีกฎการสืบทอดชุดพิเศษเกี่ยวกับinit
วิธีการ
ฉันรู้ว่านี่เป็นครั้งที่สองที่ฉันทำ แต่เราไม่เข้าใจจุดแรกหรือทำไมrequired
คำหลักถึงมีอยู่จนกว่าเราจะเข้าใจจุดนี้ เมื่อเราเข้าใจประเด็นนี้แล้วอีกประเด็นก็ชัดเจน
ทุกปกข้อมูลผมในส่วนของคำตอบนี้นี้มาจากเอกสารของ Apple พบที่นี่
จากเอกสาร Apple:
แตกต่างจากคลาสย่อยใน Objective-C คลาสย่อย Swift ไม่ได้รับค่าเริ่มต้น superclass ของพวกเขาตามค่าเริ่มต้น วิธีการของ Swift ช่วยป้องกันสถานการณ์ที่ตัวเริ่มต้นอย่างง่ายจากซูเปอร์คลาสได้รับการสืบทอดโดยคลาสย่อยที่พิเศษกว่าและถูกใช้เพื่อสร้างอินสแตนซ์ใหม่ของคลาสย่อยที่ไม่ได้เริ่มต้นอย่างสมบูรณ์หรือไม่ถูกต้อง
เน้นการขุด
ดังนั้นตรงจากเอกสารของ Apple ที่นั่นเราจะเห็นว่าคลาสย่อย Swift จะไม่เสมอไป (และมักจะไม่) สืบทอดinit
วิธีการของซูเปอร์คลาส
ดังนั้นพวกเขาจะได้รับมรดกจากซุปเปอร์คลาสของพวกเขาเมื่อไหร่?
มีกฎสองข้อที่กำหนดเมื่อคลาสย่อยสืบทอดinit
เมธอดจากพาเรนต์ จากเอกสาร Apple:
กฎข้อที่ 1
หากคลาสย่อยของคุณไม่ได้กำหนดค่าเริ่มต้นที่กำหนดไว้ค่าเริ่มต้นจะรับค่าเริ่มต้นที่กำหนดระดับเริ่มต้นทั้งหมดของมันโดยอัตโนมัติ
กฎ 2
หากคลาสย่อยของคุณมีการนำไปใช้งานของตัวกำหนดค่าเริ่มต้นของซูเปอร์คลาสทั้งหมดไม่ว่าจะโดยการสืบทอดตามกฎ 1 หรือโดยการปรับใช้แบบกำหนดเองเป็นส่วนหนึ่งของคำจำกัดความ
กฎข้อที่ 2 ไม่ได้โดยเฉพาะอย่างยิ่งที่เกี่ยวข้องกับการสนทนานี้เพราะSKSpriteNode
's init(coder: NSCoder)
ไม่น่าจะเป็นวิธีการที่สะดวกสบาย
ดังนั้นคุณInfoBar
ชั้นสืบทอดrequired
initializer init(team: Team, size: CGSize)
ขวาขึ้นจนถึงจุดที่คุณเพิ่มเข้าไป
หากคุณไม่ได้ระบุinit
วิธีการนี้และทำให้InfoBar
คุณสมบัติที่เพิ่มเข้ามาของคุณเป็นทางเลือกหรือให้ค่าเริ่มต้นแทนคุณจะยังคงสืบทอดคุณสมบัติSKSpriteNode
ต่อinit(coder: NSCoder)
ไป อย่างไรก็ตามเมื่อเราเพิ่มตัวกำหนดค่าเริ่มต้นที่กำหนดเองของเราเองเราหยุดสืบทอดค่าเริ่มต้นที่กำหนดไว้ของ superclass (และตัวเริ่มต้นความสะดวกซึ่งไม่ได้ชี้ไปที่ตัวเริ่มต้นที่เราใช้)
ดังนั้นในฐานะที่เป็นตัวอย่างแบบง่าย ๆ ฉันขอนำเสนอ
class Foo {
var foo: String
init(foo: String) {
self.foo = foo
}
}
class Bar: Foo {
var bar: String
init(foo: String, bar: String) {
self.bar = bar
super.init(foo: foo)
}
}
let x = Bar(foo: "Foo")
ซึ่งแสดงข้อผิดพลาดต่อไปนี้:
ไม่มีอาร์กิวเมนต์สำหรับพารามิเตอร์ 'bar' ในการโทร
ถ้านี่คือ Objective-C มันจะไม่มีปัญหาในการสืบทอด ถ้าเราเริ่มต้นBar
กับinitWithFoo:
ใน Objective-C ที่สถานที่ให้บริการก็จะself.bar
nil
มันอาจไม่ดี แต่มันเป็นความสมบูรณ์แบบที่ถูกต้องรัฐสำหรับวัตถุที่จะอยู่ใน. มันไม่ได้เป็นรัฐที่ถูกต้องสมบูรณ์สำหรับวัตถุสวิฟท์ที่จะอยู่ใน. ไม่ได้เป็นตัวเลือกและไม่สามารถself.bar
nil
อีกครั้งวิธีเดียวที่เรารับค่าเริ่มต้นคือการไม่ให้บริการของเราเอง ดังนั้นถ้าเราพยายามที่จะสืบทอดโดยการลบBar
ของinit(foo: String, bar: String)
เช่น:
class Bar: Foo {
var bar: String
}
ตอนนี้เรากลับไปรับมรดก (เรียงลำดับ) แต่สิ่งนี้จะไม่รวบรวม ... และข้อความแสดงข้อผิดพลาดอธิบายว่าทำไมเราไม่สืบทอดinit
วิธีsuperclass :
ปัญหา: Class 'Bar' ไม่มี initializers
แก้ไขมัน:คุณสมบัติ 'บาร์' ที่เก็บไว้โดยไม่มี initializers ป้องกัน initializers สังเคราะห์
หากเราได้เพิ่มคุณสมบัติที่เก็บไว้ในคลาสย่อยของเราไม่มีวิธีที่รวดเร็วในการสร้างอินสแตนซ์ที่ถูกต้องของคลาสย่อยของเราด้วยตัวเริ่มต้น superclass ซึ่งอาจไม่รู้เกี่ยวกับคุณสมบัติที่เก็บไว้ของคลาสย่อยของเรา
โอเคทำไมฉันต้องใช้init(coder: NSCoder)
เลย? ทำไมrequired
ล่ะ
init
วิธีการของ Swift อาจเล่นโดยกฎการสืบทอดชุดพิเศษ แต่ความสอดคล้องของโปรโตคอลยังคงสืบทอดมาจากสายโซ่ หากคลาสพาเรนต์เป็นไปตามโปรโตคอลคลาสย่อยนั้นจะต้องสอดคล้องกับโปรโตคอลนั้น
ตามปกติแล้วนี่ไม่ใช่ปัญหาเพราะโปรโตคอลส่วนใหญ่ต้องการเพียงวิธีการที่ไม่เล่นโดยกฎการรับมรดกพิเศษใน Swift ดังนั้นหากคุณสืบทอดจากคลาสที่สอดคล้องกับโปรโตคอลคุณก็จะได้รับมรดกทั้งหมดด้วย วิธีการหรือคุณสมบัติที่อนุญาตให้คลาสเพื่อตอบสนองความสอดคล้องกับโปรโตคอล
อย่างไรก็ตามโปรดจำไว้ว่าinit
วิธีการของ Swift นั้นเล่นโดยกฎชุดพิเศษและไม่ได้รับการถ่ายทอดเสมอไป ด้วยเหตุนี้ในชั้นเรียนที่สอดคล้องกับโปรโตคอลซึ่งจะต้องมีพิเศษinit
วิธีการ (เช่นNSCoding
) ต้องว่าชั้นทำเครื่องหมายเหล่านั้นวิธีการเป็นinit
required
ลองพิจารณาตัวอย่างนี้:
protocol InitProtocol {
init(foo: Int)
}
class ConformingClass: InitProtocol {
var foo: Int
init(foo: Int) {
self.foo = foo
}
}
สิ่งนี้ไม่ได้รวบรวม มันสร้างคำเตือนต่อไปนี้:
ปัญหา:ความต้องการของเครื่องมือเริ่มต้น 'init (foo :)' สามารถทำได้โดยเครื่องมือกำหนดค่าเริ่มต้น 'ที่ต้องใช้' ใน ConformingClass ที่ไม่ใช่คลาสระดับสุดท้าย
แก้ไขมัน:ต้องใส่
มันต้องการให้ฉันทำinit(foo: Int)
initializer ที่ต้องการ ฉันสามารถทำให้มีความสุขได้ด้วยการทำให้ชั้นเรียนfinal
(ซึ่งหมายความว่าชั้นเรียนไม่สามารถสืบทอดได้)
แล้วจะเกิดอะไรขึ้นถ้าฉัน subclass จากจุดนี้ถ้าฉันคลาสย่อยฉันสบายดี ถ้าฉันจะเพิ่ม initializers ใด ๆ แม้ว่าผมจู่ ๆ init(foo:)
ก็ไม่มีการสืบทอดอีกต่อไป InitProtocol
นี่คือปัญหาเพราะตอนนี้ฉันไม่สอดคล้องกับ ฉันไม่สามารถคลาสย่อยจากคลาสที่สอดคล้องกับโพรโทคอลและจากนั้นก็ตัดสินใจว่าฉันไม่ต้องการที่จะปฏิบัติตามโพรโทคอลนั้นอีกต่อไป ฉันได้รับการสืบทอดโปรโตคอลที่สอดคล้องกัน แต่เนื่องจากวิธีการที่สวิฟท์ทำงานร่วมกับinit
การสืบทอดวิธีการฉันจึงไม่ได้รับส่วนหนึ่งของสิ่งที่จำเป็นเพื่อให้สอดคล้องกับโปรโตคอลนั้นและฉันจะต้องใช้มัน
เอาล่ะทั้งหมดนี้สมเหตุสมผลแล้ว แต่ทำไมฉันไม่ได้รับข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์มากกว่านี้?
เบบี้เกิดข้อผิดพลาดที่อาจจะมีความชัดเจนมากขึ้นหรือดีกว่าถ้ามันระบุว่าชั้นเรียนของคุณก็ไม่ได้เป็นไปตามสืบทอดโปรโตคอลและการที่จะแก้ไขได้คุณจำเป็นต้องใช้NSCoding
init(coder: NSCoder)
แน่ใจ
แต่ Xcode ไม่สามารถสร้างข้อความนั้นได้เพราะนั่นไม่ได้เป็นปัญหาจริงที่เกิดขึ้นกับการไม่ใช้หรือสืบทอดวิธีการที่จำเป็นเสมอไป มีเหตุผลอย่างน้อยหนึ่งเหตุผลในการทำinit
วิธีการrequired
นอกเหนือจากความสอดคล้องของโปรโตคอลและนั่นคือวิธีการจากโรงงาน
ถ้าฉันต้องการเขียนวิธีการจากโรงงานที่เหมาะสมฉันต้องระบุประเภทการคืนสินค้าให้เป็นSelf
(เทียบเท่ากับวัตถุประสงค์ของ C-Swift instanceType
) แต่เพื่อที่จะทำสิ่งนี้ฉันต้องใช้required
วิธีการเริ่มต้นจริง
class Box {
var size: CGSize
init(size: CGSize) {
self.size = size
}
class func factory() -> Self {
return self.init(size: CGSizeZero)
}
}
สิ่งนี้สร้างข้อผิดพลาด:
การสร้างออบเจ็กต์ประเภทคลาส 'ตัวเอง' ด้วยค่าเมตาประเภทต้องใช้เครื่องมือเริ่มต้น 'จำเป็น'
มันเป็นปัญหาเดียวกัน ถ้าเรา subclass Box
, subclasses factory
ของเราจะได้รับมรดกวิธีการเรียน SubclassedBox.factory()
ดังนั้นเราจึงสามารถเรียก อย่างไรก็ตามหากไม่มีrequired
คีย์เวิร์ดในinit(size:)
เมธอดBox
คลาสย่อยจะไม่รับประกันว่าจะสืบทอดสิ่งself.init(size:)
ที่factory
เรียก
ดังนั้นเราต้องทำวิธีนั้นrequired
ถ้าเราต้องการวิธีการจากโรงงานเช่นนี้และนั่นหมายความว่าถ้าชั้นเรียนของเราใช้วิธีการเช่นนี้เราจะมีrequired
วิธีการเริ่มต้นและเราจะพบปัญหาเดียวกันกับที่คุณพบที่นี่ ด้วยNSCoding
โปรโตคอล
ท้ายที่สุดแล้วมันก็ทำให้ความเข้าใจพื้นฐานที่ผู้เริ่มต้นของ Swift เล่นโดยกฎชุดการสืบทอดที่แตกต่างกันเล็กน้อยซึ่งหมายความว่าคุณไม่รับประกันว่าจะได้รับค่าเริ่มต้นจาก Superclass ของคุณ สิ่งนี้เกิดขึ้นเนื่องจาก initializer superclass ไม่สามารถรู้เกี่ยวกับคุณสมบัติใหม่ของคุณและพวกเขาไม่สามารถยกตัวอย่างวัตถุของคุณให้อยู่ในสถานะที่ถูกต้อง แต่ด้วยเหตุผลต่างๆเป็น superclass required
อาจทำเครื่องหมายการเริ่มต้นเป็น เมื่อไรก็ตามที่เราทำเราสามารถใช้หนึ่งในสถานการณ์ที่เฉพาะเจาะจงโดยที่เราทำการสืบทอดrequired
วิธีการจริงหรือเราต้องดำเนินการเอง
ประเด็นหลักที่นี่คือถ้าเราได้รับข้อผิดพลาดที่คุณเห็นที่นี่นั่นหมายความว่าชั้นเรียนของคุณไม่ได้ใช้วิธีการทั้งหมด
อาจเป็นหนึ่งตัวอย่างสุดท้ายที่จะเจาะลึกในความจริงที่ว่าคลาสย่อย Swift ไม่ได้สืบทอดinit
วิธีการของผู้ปกครองเสมอไป(ซึ่งฉันคิดว่าเป็นหัวใจสำคัญที่จะเข้าใจปัญหานี้อย่างสมบูรณ์) ลองพิจารณาตัวอย่างนี้:
class Foo {
init(a: Int, b: Int, c: Int) {
// do nothing
}
}
class Bar: Foo {
init(string: String) {
super.init(a: 0, b: 1, c: 2)
// do more nothing
}
}
let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)
สิ่งนี้ล้มเหลวในการรวบรวม
ข้อความแสดงข้อผิดพลาดจะทำให้เข้าใจผิดเล็กน้อย:
อาร์กิวเมนต์พิเศษ 'b' อยู่ระหว่างการโทร
แต่ประเด็นก็คือBar
ไม่ได้รับมรดกใด ๆ ของFoo
's init
วิธีเพราะมันยังไม่ได้รับความพึงพอใจทั้งสองกรณีพิเศษสำหรับการสืบทอดinit
วิธีการจากคลาสแม่ของมัน
หากนี่คือ Objective-C เราจะสืบทอดสิ่งนั้นinit
โดยไม่มีปัญหาเพราะ Objective-C มีความสุขอย่างสมบูรณ์ที่จะไม่คืนค่าคุณสมบัติของวัตถุ (แม้ว่าในฐานะนักพัฒนาคุณไม่ควรมีความสุขกับสิ่งนี้) ใน Swift สิ่งนี้จะไม่ทำ คุณไม่สามารถมีสถานะที่ไม่ถูกต้องและการรับค่าเริ่มต้น superclass สามารถนำไปสู่สถานะวัตถุที่ไม่ถูกต้องเท่านั้น
init(collection:MPMediaItemCollection)
ใช้ตัวอย่างเช่นการเริ่มต้นที่ต้องการของฉัน คุณต้องจัดหาคอลเลกชันรายการสื่อจริง นั่นคือจุดของคลาสนี้ คลาสนี้ไม่สามารถสร้างอินสแตนซ์ได้หากไม่มีอย่างใดอย่างหนึ่ง มันจะวิเคราะห์การรวบรวมและเริ่มต้นตัวแปรอินสแตนซ์โหล นั่นคือจุดรวมของการเป็นผู้เริ่มต้นที่ได้รับมอบหมายเพียงคนเดียว! ดังนั้นจึงinit(coder:)
ไม่มีความหมาย (หรือไม่มีความหมาย) MPMediaItemCollection ที่จะจัดหาที่นี่; เพียง แต่fatalError
วิธีการที่ถูกต้อง