ทำไมต้องใช้คีย์เวิร์ดเพื่อความสะดวกใน Swift


132

เนื่องจาก Swift รองรับวิธีการและตัวเริ่มต้นทำงานมากเกินไปคุณสามารถใส่หลาย ๆ ตัวinitควบคู่กันและใช้ตามที่คุณคิดว่าสะดวก:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

ทำไมconvenienceคำหลักถึงมีอยู่? อะไรทำให้สิ่งต่อไปนี้ดีขึ้นอย่างมาก

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}

13
เพิ่งอ่านเรื่องนี้ในเอกสารและก็สับสนเหมือนกัน : /
boidkan

คำตอบ:


236

คำตอบที่มีอยู่บอกเรื่องราวได้เพียงครึ่งconvenienceเดียว อีกครึ่งหนึ่งของเรื่องครึ่งหนึ่งที่ไม่มีคำตอบใด ๆ ครอบคลุมตอบคำถามที่เดสมอนด์โพสต์ไว้ในความคิดเห็น:

เหตุใด Swift จึงบังคับให้ฉันวางconvenienceหน้าตัวเริ่มต้นเพียงเพราะฉันต้องการโทรself.initจากมัน”

ฉันพูดถึงมันเล็กน้อยในคำตอบนี้ซึ่งฉันครอบคลุมรายละเอียดกฎการเริ่มต้นของ Swift หลายข้อ แต่จุดสนใจหลักอยู่ที่requiredคำนั้น แต่คำตอบนั้นยังคงกล่าวถึงบางสิ่งที่เกี่ยวข้องกับคำถามนี้และคำตอบนี้ เราต้องเข้าใจว่าการสืบทอด Swift initializer ทำงานอย่างไร

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

เพื่อความชัดเจนตัวแปรอินสแตนซ์ที่ไม่ได้กำหนดค่าเริ่มต้นคือตัวแปรอินสแตนซ์ใด ๆ ที่ไม่ได้กำหนดค่าเริ่มต้น (โปรดทราบว่าตัวเลือกและตัวเลือกที่ไม่ได้ปิดโดยปริยายจะถือว่าเป็นค่าเริ่มต้นโดยอัตโนมัติnil)

ดังนั้นในกรณีนี้:

class Foo {
    var a: Int
}

aเป็นตัวแปรอินสแตนซ์ที่ไม่ได้เริ่มต้น สิ่งนี้จะไม่รวบรวมเว้นแต่เราจะให้aค่าเริ่มต้น:

class Foo {
    var a: Int = 0
}

หรือเริ่มต้นaด้วยวิธีการเริ่มต้น:

class Foo {
    var a: Int

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

ตอนนี้เรามาดูกันว่าจะเกิดอะไรขึ้นถ้าเราซับคลาสFooเราจะทำอย่างไร?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

ขวา? เราได้เพิ่มตัวแปรและเราได้เพิ่มตัวเริ่มต้นเพื่อกำหนดค่าbเพื่อที่จะรวบรวม ทั้งนี้ขึ้นอยู่กับสิ่งที่ภาษาที่คุณมาจากคุณอาจคาดหวังว่าBarได้รับการถ่ายทอดของการเริ่มต้น,Foo init(a: Int)แต่มันไม่ได้ แล้วจะทำได้อย่างไร? อย่างไรFoo's init(a: Int)ทราบวิธีการกำหนดค่าให้กับbตัวแปรที่Barเพิ่ม? มันไม่ ดังนั้นเราจึงไม่สามารถเริ่มต้นBarอินสแตนซ์ด้วยตัวเริ่มต้นที่ไม่สามารถเริ่มต้นค่าทั้งหมดของเราได้

สิ่งนี้เกี่ยวข้องกับconvenienceอะไร?

ลองดูกฎเกี่ยวกับการสืบทอดค่าเริ่มต้น :

กฎข้อ 1

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

กฎข้อ 2

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

ประกาศกฎข้อ 2 ซึ่งกล่าวถึงตัวเริ่มต้นความสะดวก

ดังนั้นสิ่งที่เป็นconvenienceคำหลักที่ไม่ทำคือการแสดงให้เราซึ่ง initializers สามารถสืบทอดโดย subclasses ว่าตัวแปรเช่นเพิ่มโดยไม่ต้องเป็นค่าเริ่มต้น

ลองมาBaseเรียนตัวอย่างนี้:

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

สังเกตว่าเรามีตัวconvenienceเริ่มต้นสามตัวที่นี่ นั่นหมายความว่าเรามีตัวเริ่มต้นสามตัวที่สามารถสืบทอดได้ และเรามีตัวเริ่มต้นที่กำหนดไว้หนึ่งตัว (ตัวเริ่มต้นที่กำหนดเป็นเพียงตัวเริ่มต้นใด ๆ ที่ไม่ใช่ตัวเริ่มต้นที่สะดวก)

เราสามารถสร้างอินสแตนซ์ของคลาสพื้นฐานได้สี่วิธี:

ป้อนคำอธิบายภาพที่นี่

มาสร้างคลาสย่อยกัน

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Baseเรากำลังสืบทอดจาก เราเพิ่มตัวแปรอินสแตนซ์ของเราเองและเราไม่ได้ให้ค่าเริ่มต้นดังนั้นเราต้องเพิ่มตัวเริ่มต้นของเราเอง เราได้เพิ่มหนึ่งinit(a: Int, b: Int, c: Int)แต่มันไม่ตรงกับลายเซ็นของBaseระดับที่กำหนด init(a: Int, b: Int)initializer: นั่นหมายความว่าเราไม่ได้รับค่าเริ่มต้นใด ๆจากBase:

ป้อนคำอธิบายภาพที่นี่

ดังนั้นสิ่งที่จะเกิดขึ้นถ้าเราสืบทอดมาจากBaseแต่เราเดินไปข้างหน้าและการดำเนินการการเริ่มต้นที่ตรงกับที่ได้รับมอบหมายจากการเริ่มต้นBase?

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

ตอนนี้นอกจากตัวเริ่มต้นสองตัวที่เรานำมาใช้โดยตรงในคลาสนี้เนื่องจากเราติดตั้งตัวเริ่มต้นที่Baseกำหนดให้กับคลาสเริ่มต้นเราจะได้รับค่าเริ่มต้นทั้งหมดของBaseคลาสconvenience:

ป้อนคำอธิบายภาพที่นี่

ความจริงที่ว่าตัวเริ่มต้นที่มีลายเซ็นตรงกันถูกทำเครื่องหมายว่าconvenienceไม่มีความแตกต่างที่นี่ หมายความว่าInheritorมีตัวเริ่มต้นที่กำหนดไว้เพียงตัวเดียว ดังนั้นหากเราสืบทอดมาInheritorเราก็แค่ต้องใช้ initializer ที่กำหนดไว้ตัวหนึ่งจากนั้นเราก็จะรับInheritorค่า initializer ที่สะดวกซึ่งหมายความว่าเราได้ติดตั้งBaseinitializers ที่กำหนดไว้ทั้งหมดและสามารถสืบทอดconvenienceค่าเริ่มต้นได้


16
คำตอบเดียวที่ตอบคำถามได้จริงและเป็นไปตามเอกสาร ฉันจะยอมรับมันถ้าฉันเป็น OP
FreeNickname

12
คุณควรเขียนหนังสือ;)
coolbeet

1
@SLN คำตอบนี้ครอบคลุมมากมายเกี่ยวกับวิธีการทำงานของการสืบทอด Swift initializer
nhgrif

1
@SLN เนื่องจากการสร้าง Bar ด้วยinit(a: Int)จะทำให้bไม่ได้เริ่มต้น
Ian Warburton

2
@IanWarburton ฉันไม่รู้คำตอบสำหรับ "ทำไม" นี้โดยเฉพาะ ตรรกะของคุณในส่วนที่สองของความคิดเห็นของคุณดูเหมือนจะฟังดูดีสำหรับฉัน แต่เอกสารนี้ระบุอย่างชัดเจนว่านี่คือวิธีการทำงานและการยกตัวอย่างสิ่งที่คุณถามเกี่ยวกับใน Playground เป็นการยืนยันว่าพฤติกรรมนั้นตรงกับสิ่งที่บันทึกไว้
nhgrif

9

ความชัดเจนส่วนใหญ่ จากตัวอย่างที่สองของคุณ

init(name: String) {
    self.name = name
}

จำเป็นต้องมีหรือที่กำหนด ต้องเริ่มต้นค่าคงที่และตัวแปรทั้งหมดของคุณ ตัวเริ่มต้นความสะดวกเป็นทางเลือกและโดยทั่วไปสามารถใช้เพื่อทำให้การเริ่มต้นง่ายขึ้น ตัวอย่างเช่นสมมติว่าคลาส Person ของคุณมีตัวแปรทางเลือกเพศ:

var gender: Gender?

โดยที่ Gender คือ enum

enum Gender {
  case Male, Female
}

คุณสามารถเริ่มต้นได้อย่างสะดวกสบายเช่นนี้

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

ผู้เริ่มต้นอำนวยความสะดวกจะต้องเรียกผู้เริ่มต้นที่กำหนดหรือจำเป็นในนั้น หากคลาสของคุณเป็นคลาสย่อยต้องเรียกใช้super.init() ภายในการเริ่มต้น


2
ดังนั้นมันจะชัดเจนอย่างสมบูรณ์สำหรับคอมไพเลอร์ว่าฉันกำลังพยายามทำอะไรกับ initializers หลายตัวแม้ว่าจะไม่มีconvenienceคีย์เวิร์ด แต่ Swift ก็ยังคงดักฟังอยู่ นั่นไม่ใช่ความเรียบง่ายที่ฉันคาดหวังจาก Apple =)
Desmond Hume

2
คำตอบนี้ไม่ตอบโจทย์อะไร คุณบอกว่า "ความชัดเจน" แต่ไม่ได้อธิบายว่ามันทำให้อะไรชัดเจนขึ้นได้อย่างไร
Robo Robok

7

สิ่งแรกที่ฉันคิดคือมันถูกใช้ในการสืบทอดคลาสสำหรับการจัดระเบียบรหัสและความสามารถในการอ่าน ต่อเนื่องกับคุณPersonชั้นคิดว่าสถานการณ์เช่นนี้

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

ด้วยเครื่องมือเริ่มต้นที่สะดวกสบายฉันสามารถสร้างEmployee()วัตถุที่ไม่มีค่าได้ดังนั้นคำนี้convenience


2
ด้วยconvenienceคำหลักที่ถูกนำออกไป Swift จะไม่ได้รับข้อมูลเพียงพอที่จะทำงานในลักษณะเดียวกันหรือไม่?
Desmond Hume

ไม่ถ้าคุณนำconvenienceคำหลักออกไปคุณจะไม่สามารถเริ่มต้นEmployeeวัตถุโดยไม่มีข้อโต้แย้งใด ๆ
u54r

โดยเฉพาะการเรียกEmployee()สาย (สืบทอดเนื่องจากconvenience) initializer ซึ่งสายinit() นอกจากนี้ยังเป็นตัวเริ่มต้นอำนวยความสะดวกสำหรับเรียกตัวเริ่มต้นที่กำหนด self.init(name: "Unknown")init(name: String)Employee
BallpointBen

1

นอกเหนือจากประเด็นที่ผู้ใช้รายอื่นได้อธิบายไว้นี่คือความเข้าใจของฉันเล็กน้อย

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

ตัวอย่างเช่นคลาสของบุคคลที่สามบางคลาสที่คุณใช้มีinitพารามิเตอร์สี่ตัว แต่ในแอปพลิเคชันของคุณสองตัวสุดท้ายมีค่าเท่ากัน เพื่อหลีกเลี่ยงการพิมพ์มากขึ้นและทำให้โค้ดของคุณสะอาดขึ้นคุณสามารถกำหนด a convenience initโดยมีพารามิเตอร์เพียงสองตัวและภายในนั้นเรียกself.initด้วยพารามิเตอร์ last to ด้วยค่าเริ่มต้น


1
เหตุใด Swift จึงบังคับให้ฉันวางconvenienceหน้าตัวเริ่มต้นเพียงเพราะฉันต้องโทรself.initจากมัน สิ่งนี้ดูเหมือนซ้ำซ้อนและค่อนข้างไม่สะดวก
เดสมอนด์ฮูม

1

ตามเอกสาร Swift 2.1ผู้convenienceเริ่มต้นจะต้องปฏิบัติตามกฎบางประการ:

  1. convenienceinitializer สามารถโทร intializers ในระดับเดียวกันไม่ได้อยู่ในชั้นเรียนสุด (เฉพาะข้ามไม่ได้ขึ้นไป)

  2. convenienceinitializer มีการโทรที่กำหนดค่าเริ่มต้นที่ไหนสักแห่งในห่วงโซ่

  3. ตัวconvenienceเริ่มต้นไม่สามารถเปลี่ยนแปลงใด ๆคุณสมบัติก่อนที่มันจะได้เรียก initializer อื่น - ในขณะที่การเริ่มต้นกำหนดให้ มีการเริ่มต้นคุณสมบัติที่ได้รับการแนะนำให้รู้จักกับระดับปัจจุบันก่อนที่จะเรียกค่าเริ่มต้นอีก

ด้วยการใช้convenienceคีย์เวิร์ดคอมไพเลอร์ Swift รู้ว่าต้องตรวจสอบเงื่อนไขเหล่านี้มิฉะนั้นจะทำไม่ได้


สรุปได้ว่าคอมไพเลอร์สามารถจัดเรียงสิ่งนี้ได้โดยไม่ต้องใช้convenienceคีย์เวิร์ด
nhgrif

นอกจากนี้ประเด็นที่สามของคุณยังทำให้เข้าใจผิด ตัวเริ่มต้นอำนวยความสะดวกสามารถเปลี่ยนคุณสมบัติได้เท่านั้น (และไม่สามารถเปลี่ยนletคุณสมบัติได้) ไม่สามารถเริ่มต้นคุณสมบัติ เครื่องมือเริ่มต้นที่กำหนดมีหน้าที่ในการเริ่มต้นคุณสมบัติที่แนะนำทั้งหมดก่อนที่จะเรียกไปยังตัวsuperเริ่มต้นที่กำหนด
nhgrif

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

1

คลาสสามารถมีตัวเริ่มต้นที่กำหนดไว้ได้มากกว่าหนึ่งตัว ตัวเริ่มต้นอำนวยความสะดวกเป็นตัวเริ่มต้นสำรองที่ต้องเรียกตัวเริ่มต้นที่กำหนดไว้ในคลาสเดียวกัน

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