เหตุใด Swift initializers จึงไม่สามารถเรียกตัวเริ่มต้นความสะดวกในซูเปอร์คลาสได้


88

พิจารณาสองคลาส:

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        super.init() // Error: Must call a designated initializer of the superclass 'A'
    }
}

ฉันไม่เห็นว่าเหตุใดจึงไม่อนุญาต ในที่สุด initializer ที่กำหนดของแต่ละคลาสจะถูกเรียกด้วยค่าที่ต้องการดังนั้นทำไมฉันต้องทำซ้ำตัวเองในB's initโดยการระบุค่าเริ่มต้นxอีกครั้งเมื่อความสะดวกinitในAจะทำได้ดี?


2
ฉันค้นหาคำตอบ แต่ไม่พบคำตอบที่ตรงใจฉัน อาจเป็นเหตุผลในการนำไปใช้งาน บางทีการค้นหาตัวเริ่มต้นที่กำหนดในคลาสอื่นจะง่ายกว่าการค้นหาตัวเริ่มต้นที่สะดวก ... หรืออะไรทำนองนั้น
Sulthan

@ โรเบิร์ตขอบคุณสำหรับความคิดเห็นของคุณด้านล่าง ฉันคิดว่าคุณสามารถเพิ่มไว้ในคำถามของคุณหรือโพสต์คำตอบพร้อมกับสิ่งที่คุณได้รับ: "นี่คือการออกแบบและข้อบกพร่องที่เกี่ยวข้องได้รับการจัดการในพื้นที่นี้" ดูเหมือนว่าพวกเขาทำไม่ได้หรือไม่ต้องการอธิบายเหตุผล
Ferran Maylinch

คำตอบ:


24

นี่คือกฎข้อที่ 1 ของกฎ "Initializer Chaining" ตามที่ระบุไว้ในคู่มือการเขียนโปรแกรม Swift ซึ่งอ่านว่า:

กฎข้อที่ 1: ตัวเริ่มต้นที่กำหนดจะต้องเรียกตัวเริ่มต้นที่กำหนดจากซูเปอร์คลาสทันที

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

เน้นของฉัน ตัวเริ่มต้นที่กำหนดไม่สามารถเรียกตัวเริ่มต้นที่สะดวกได้

มีแผนภาพที่สอดคล้องกับกฎเพื่อแสดงให้เห็นว่าตัวเริ่มต้น "เส้นทาง" ใดที่อนุญาต:

Initializer Chaining


80
แต่ทำไมถึงทำแบบนี้? เอกสารระบุเพียงว่ามันทำให้การออกแบบง่ายขึ้น แต่ฉันไม่เห็นว่าจะเป็นอย่างไรถ้าฉันต้องทำซ้ำตัวเองโดยการระบุค่าเริ่มต้นอย่างต่อเนื่องบนตัวเริ่มต้นที่กำหนดฉันส่วนใหญ่ไม่สนใจเมื่อเป็นค่าเริ่มต้นเพื่อความสะดวก initializers จะทำอย่างไร
Robert

5
ฉันยื่นข้อผิดพลาด 17266917 สองสามวันหลังจากคำถามนี้ขอให้สามารถโทรหาผู้เริ่มต้นที่สะดวกได้ ยังไม่มีการตอบสนอง แต่ฉันยังไม่มีรายงานข้อผิดพลาด Swift อื่น ๆ ของฉันด้วย!
Robert

8
หวังว่าพวกเขาจะอนุญาตให้เรียกผู้เริ่มต้นอำนวยความสะดวก สำหรับบางชั้นเรียนใน SDK ไม่มีวิธีอื่นใดในการบรรลุพฤติกรรมบางอย่างนอกจากการเรียกผู้เริ่มต้นอำนวยความสะดวก ดูSCNGeometry: คุณสามารถเพิ่มSCNGeometryElements ได้โดยใช้ตัวเริ่มต้นความสะดวกเท่านั้นดังนั้นจึงไม่สามารถสืบทอดจากมันได้
Spak

4
นี่เป็นการตัดสินใจในการออกแบบภาษาที่แย่มาก จากจุดเริ่มต้นของแอปใหม่ของฉันฉันตัดสินใจที่จะพัฒนาอย่างรวดเร็วฉันต้องเรียก NSWindowController.init (windowNibName) จากคลาสย่อยของฉันและฉันไม่สามารถทำได้ :(
Uniqus

4
@Kaiserludi: ไม่มีประโยชน์อะไรมันถูกปิดด้วยคำตอบต่อไปนี้: "นี่คือการออกแบบและข้อบกพร่องที่เกี่ยวข้องได้ถูกแยกออกในพื้นที่นี้"
Robert

21

พิจารณา

class A
{
    var a: Int
    var b: Int

    init (a: Int, b: Int) {
        print("Entering A.init(a,b)")
        self.a = a; self.b = b
    }

    convenience init(a: Int) {
        print("Entering A.init(a)")
        self.init(a: a, b: 0)
    }

    convenience init() {
        print("Entering A.init()")
        self.init(a:0)
    }
}


class B : A
{
    var c: Int

    override init(a: Int, b: Int)
    {
        print("Entering B.init(a,b)")
        self.c = 0; super.init(a: a, b: b)
    }
}

var b = B()

เนื่องจากตัวเริ่มต้นที่กำหนดทั้งหมดของคลาส A ถูกแทนที่คลาส B จะสืบทอดค่าเริ่มต้นความสะดวกทั้งหมดของ A ดังนั้นการดำเนินการนี้จะได้ผลลัพธ์

Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)

ตอนนี้ถ้า initializer ที่กำหนด B.init (a: b :) จะได้รับอนุญาตให้เรียก initializer สะดวกระดับพื้นฐาน Ainit (a :) สิ่งนี้จะส่งผลให้มีการเรียกซ้ำไปที่ B.init (a:, b: ).


ง่ายต่อการแก้ไข ทันทีที่คุณเรียกตัวเริ่มต้นอำนวยความสะดวกระดับซูเปอร์คลาสจากภายในตัวเริ่มต้นที่กำหนดไว้ตัวเริ่มต้นอำนวยความสะดวกระดับซุปเปอร์คลาสจะไม่ได้รับการสืบทอดอีกต่อไป
fluidsonic

@fluidsonic แต่นั่นอาจจะบ้าเพราะโครงสร้างและวิธีการคลาสของคุณจะเปลี่ยนไปขึ้นอยู่กับวิธีการใช้ ลองนึกภาพความสนุกในการแก้จุดบกพร่อง!
kdazzle

2
@kdazzle โครงสร้างของคลาสและเมธอดคลาสจะไม่เปลี่ยนแปลง ทำไมต้อง? - ปัญหาเดียวที่ฉันคิดได้คือตัวเริ่มต้นความสะดวกจะต้องมอบหมายให้กับตัวเริ่มต้นที่กำหนดของคลาสย่อยแบบไดนามิกเมื่อสืบทอดและคงที่ไปยังตัวเริ่มต้นที่กำหนดของคลาสของตัวเองเมื่อไม่ได้รับการสืบทอด แต่มอบหมายให้จากคลาสย่อย
fluidsonic

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

13

เป็นเพราะคุณสามารถจบลงด้วยการเรียกซ้ำที่ไม่มีที่สิ้นสุด พิจารณา:

class SuperClass {
    init() {
    }

    convenience init(value: Int) {
        // calls init() of the current class
        // so init() for SubClass if the instance
        // is a SubClass
        self.init()
    }
}

class SubClass : SuperClass {
    override init() {
        super.init(value: 10)
    }
}

และดูที่:

let a = SubClass()

ซึ่งจะเรียกSubClass.init()ซึ่งจะเรียกซึ่งจะเรียกSuperClass.init(value:)SubClass.init()

กฎการเริ่มต้นที่กำหนด / ความสะดวกได้รับการออกแบบมาเพื่อให้การเริ่มต้นคลาสถูกต้องเสมอ


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

1

ฉันพบวิธีแก้ไขสำหรับสิ่งนี้ มันไม่สวยสุด ๆ แต่มันช่วยแก้ปัญหาที่ไม่รู้ค่าของซุปเปอร์คลาสหรือต้องการตั้งค่าเริ่มต้น

สิ่งที่คุณต้องทำก็คือสร้างอินสแตนซ์ของซูเปอร์คลาสโดยใช้ความสะดวกinitในinitคลาสย่อย จากนั้นคุณเรียกinitซุปเปอร์ที่กำหนดโดยใช้อินสแตนซ์ที่คุณสร้าง

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        // calls A's convenience init, gets instance of A with default x value
        let intermediate = A() 

        super.init(x: intermediate.x) 
    }
}

1

พิจารณาแยกรหัสเริ่มต้นจากinit()ฟังก์ชันที่สะดวกของคุณไปยังฟังก์ชันตัวช่วยใหม่foo()เรียกfoo(...)ให้ทำการเริ่มต้นในคลาสย่อยของคุณ


ข้อเสนอแนะที่ดี แต่ไม่สามารถตอบคำถามได้จริง
SwiftsNamesake

0

ดูวิดีโอ WWDC "403 intermediate Swift" เวลา 18:30 น. เพื่อดูคำอธิบายเชิงลึกเกี่ยวกับตัวเริ่มต้นและการถ่ายทอดทางพันธุกรรม ตามที่ฉันเข้าใจแล้วให้พิจารณาสิ่งต่อไปนี้:

class Dragon {
    var legs: Int
    var isFlying: Bool

    init(legs: Int, isFlying: Bool) {
        self.legs = legs
        self.isFlying = isFlying
    }

    convenience initWyvern() { 
        self.init(legs: 2, isFlying: true)
    }
}

แต่ตอนนี้พิจารณาคลาสย่อยของ Wyrm: Wyrm เป็นมังกรที่ไม่มีขาและไม่มีปีก ดังนั้น Initializer สำหรับ Wyvern (2 ขา 2 ปีก) จึงผิด! ข้อผิดพลาดนั้นสามารถหลีกเลี่ยงได้หากไม่สามารถเรียกใช้ Wyvern-Initializer ที่สะดวกได้ แต่มีเพียง Initializer ที่กำหนดแบบเต็มเท่านั้น:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}

12
นั่นไม่ใช่เหตุผลจริงๆ จะเกิดอะไรขึ้นถ้าฉันสร้างคลาสย่อยเมื่อมีinitWyvernเหตุผลที่จะถูกเรียก?
Sulthan

4
ใช่ฉันไม่มั่นใจ ไม่มีอะไรหยุดยั้งการWyrmลบล้างจำนวนขาได้หลังจากเรียกใช้งานตัวเริ่มต้นความสะดวกสบาย
Robert

เป็นเหตุผลที่ให้ไว้ในวิดีโอ WWDC (เฉพาะกับรถยนต์ -> รถแข่งและคุณสมบัติบูลีนเทอร์โบ) หากคลาสย่อยใช้ตัวเริ่มต้นที่กำหนดไว้ทั้งหมดมากกว่าที่คลาสย่อยจะรับค่าเริ่มต้นแบบอำนวยความสะดวกด้วย ฉันเห็นความรู้สึกในนั้นและฉันก็ไม่มีปัญหากับวิธีการทำงานของ Objective-C ดูอนุสัญญาใหม่ที่จะเรียก super.init ล่าสุดใน init ไม่ใช่ครั้งแรกเหมือนใน Objective-C
Ralf

IMO อนุสัญญาที่เรียก super.init ว่า "last" ไม่ใช่แนวคิดใหม่มันเหมือนกับใน objective-c เพียงแค่นั้นทุกอย่างจะถูกกำหนดค่าเริ่มต้น (ศูนย์, 0, อะไรก็ตาม) โดยอัตโนมัติ เพียงแค่นั้นใน Swift เราต้องทำการเริ่มต้นระบบปฏิบัติการเฟสนี้ด้วยตัวเอง ข้อดีของแนวทางนี้คือเรามีตัวเลือกในการกำหนดค่าเริ่มต้นที่แตกต่างกัน
Roshan

-1

ทำไมคุณไม่มี initializers สองตัว - ตัวแรกมีค่าดีฟอลต์

class A {
  var x: Int

  init(x: Int) {
    self.x = x
  }

  init() {
    self.x = 0
  }
}

class B: A {
  override init() {
    super.init()

    // Do something else
  }
}

let s = B()
s.x // 0
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.