แนวทางปฏิบัติที่ดีที่สุดในการติดตั้งโปรแกรมเริ่มต้นที่ล้มเหลวใน Swift


100

ด้วยรหัสต่อไปนี้ฉันพยายามกำหนดคลาสโมเดลอย่างง่ายและเป็นตัวเริ่มต้นที่ล้มเหลวซึ่งใช้พจนานุกรม (json-) เป็นพารามิเตอร์ initializer ควรส่งคืนnilหากไม่ได้กำหนดชื่อผู้ใช้ใน json ดั้งเดิม

1. ทำไมโค้ดไม่คอมไพล์ ข้อความแสดงข้อผิดพลาดระบุว่า:

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

นั่นไม่สมเหตุสมผล ทำไมฉันจึงควรเริ่มต้นคุณสมบัติเหล่านั้นเมื่อผมวางแผนที่จะกลับมาnil?

2. แนวทางของฉันเป็นแนวทางที่ถูกต้องหรือมีแนวคิดอื่น ๆ หรือรูปแบบทั่วไปเพื่อให้บรรลุเป้าหมายของฉันหรือไม่?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}

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

คำตอบ:


71

อัปเดต:จากบันทึกการเปลี่ยนแปลงของSwift 2.2 (เผยแพร่เมื่อวันที่ 21 มีนาคม 2559):

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


สำหรับ Swift 2.1 และรุ่นก่อนหน้า:

ตามเอกสารของ Apple (และข้อผิดพลาดของคอมไพเลอร์ของคุณ) คลาสจะต้องเตรียมใช้งานคุณสมบัติที่เก็บไว้ทั้งหมดก่อนที่จะกลับมาnilจากตัวเริ่มต้นที่ล้มเหลว:

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

หมายเหตุ:มันใช้งานได้ดีสำหรับโครงสร้างและการแจงนับไม่ใช่คลาสเท่านั้น

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

ตัวอย่างจากเอกสาร:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

ในตัวอย่างข้างต้นคุณสมบัติชื่อของคลาสผลิตภัณฑ์ถูกกำหนดให้มีประเภทสตริงที่เป็นทางเลือก (String!) เนื่องจากเป็นประเภททางเลือกซึ่งหมายความว่าคุณสมบัติ name มีค่าดีฟอลต์เป็นศูนย์ก่อนที่จะกำหนดค่าเฉพาะระหว่างการเตรียมใช้งาน ค่าเริ่มต้นของศูนย์นี้หมายความว่าคุณสมบัติทั้งหมดที่นำมาใช้โดยคลาสผลิตภัณฑ์มีค่าเริ่มต้นที่ถูกต้อง เป็นผลให้ตัวเริ่มต้นที่ล้มเหลวสำหรับผลิตภัณฑ์สามารถทริกเกอร์ความล้มเหลวในการกำหนดค่าเริ่มต้นเมื่อเริ่มต้นตัวเริ่มต้นหากส่งผ่านสตริงว่างก่อนกำหนดค่าเฉพาะให้กับคุณสมบัติชื่อภายในตัวเริ่มต้น

ในกรณีของคุณ แต่เพียงการกำหนดuserNameเป็นไม่สามารถแก้ไขรวบรวมข้อผิดพลาดเพราะคุณยังต้องกังวลเกี่ยวกับการเริ่มต้นคุณสมบัติในระดับฐานของคุณString! NSObjectโชคดีที่userNameกำหนดเป็น a String!คุณสามารถโทรได้super.init()ก่อนที่คุณreturn nilจะเริ่มNSObjectคลาสพื้นฐานของคุณและแก้ไขข้อผิดพลาดในการคอมไพล์

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}

1
ขอบคุณมากไม่เพียง แต่ถูกต้อง แต่ยังอธิบายได้ดี
Kai Huppmann

9
ใน swift1.2 ตัวอย่างจากเอกสารทำให้เกิดข้อผิดพลาด "คุณสมบัติที่เก็บไว้ทั้งหมดของอินสแตนซ์คลาสจะต้องเริ่มต้นก่อนที่จะคืนค่าศูนย์จากตัวเริ่มต้น"
jeffrey

2
@jeffrey ถูกต้องตัวอย่างจากเอกสาร ( Productคลาส) ไม่สามารถทริกเกอร์ความล้มเหลวในการเริ่มต้นก่อนกำหนดค่าเฉพาะแม้ว่าเอกสารจะบอกว่าทำได้ก็ตาม เอกสารไม่ซิงค์กับ Swift เวอร์ชันล่าสุด ก็ควรที่จะทำให้มันเป็นตอนนี้แทนvar แหล่งที่มา: คริสแลตต์เนอร์ let
Arjan

1
เอกสารประกอบมีโค้ดส่วนนี้แตกต่างกันเล็กน้อย: คุณตั้งค่าคุณสมบัติก่อนจากนั้นตรวจสอบว่ามีอยู่หรือไม่ ดู“ ตัวเริ่มต้นที่ล้มเหลวสำหรับชั้นเรียน”“ ภาษาการเขียนโปรแกรมที่รวดเร็ว” `` คลาส Product {let name: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} ``
Misha Karpenko

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

132

นั่นไม่สมเหตุสมผล เหตุใดฉันจึงควรเริ่มต้นคุณสมบัติเหล่านั้นเมื่อฉันวางแผนที่จะคืนค่าศูนย์

ตามที่ Chris Lattner นี่คือข้อบกพร่อง นี่คือสิ่งที่เขาพูด:

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

ที่มา

แก้ไข:

ดังนั้น swift จึงเป็นโอเพ่นซอร์สและจากบันทึกการเปลี่ยนแปลงนี้ได้รับการแก้ไขแล้วในสแนปชอตของ swift 2.2

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


2
ขอบคุณสำหรับการพูดถึงประเด็นของฉันว่าความคิดในการเริ่มต้นคุณสมบัติที่ไม่จำเป็นต้องมีอีกต่อไปดูเหมือนจะไม่สมเหตุสมผล และ +1 สำหรับการแบ่งปันแหล่งที่มาซึ่งพิสูจน์ได้ว่า Chris Lattner รู้สึกเหมือนฉัน;)
Kai Huppmann

22
FYI: "อันที่จริงนี่ยังคงเป็นสิ่งที่เราต้องการปรับปรุง แต่ยังไม่ได้ตัดต่อสำหรับ Swift 1.2" - Chris Lattner 10 กุมภาพันธ์ 2558
dreamlab

14
FYI: ใน Swift 2.0 beta 2 สิ่งนี้ยังคงเป็นปัญหาและยังเป็นปัญหากับ initializer ที่พ่นออกมา
aranasaurus

7

ฉันยอมรับว่าคำตอบของ Mike S เป็นคำแนะนำของ Apple แต่ฉันไม่คิดว่าเป็นแนวทางปฏิบัติที่ดีที่สุด จุดรวมของระบบประเภทที่แข็งแกร่งคือการย้ายข้อผิดพลาดรันไทม์เพื่อรวบรวมเวลา "วิธีแก้ปัญหา" นี้เอาชนะจุดประสงค์นั้น IMHO ที่ดีกว่าคือดำเนินการต่อและเริ่มต้นชื่อผู้ใช้""จากนั้นตรวจสอบหลังจาก super.init () หากอนุญาตให้ใช้ userNames ว่างให้ตั้งค่าสถานะ

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}

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

2
จากการตรวจสอบฉันเห็นว่าคุณพูดถูก แต่เนื่องจาก userName เป็นค่าคงที่เท่านั้น หากเป็นตัวแปรคำตอบที่ยอมรับจะแย่กว่าของฉันเนื่องจาก userName สามารถตั้งค่าเป็นศูนย์ได้ในภายหลัง
Daniel T.

ฉันชอบคำตอบนี้ @KaiHuppmann หากคุณต้องการอนุญาตให้ใช้ชื่อผู้ใช้ที่ว่างเปล่าคุณสามารถมี Bool needsReturnNil ง่ายๆได้ ถ้าค่าไม่มีอยู่ในพจนานุกรมให้ตั้งค่า needsReturnNil เป็น true และตั้งค่า userName เป็นอะไรก็ได้ หลังจาก super.init () ตรวจสอบ needsReturnNil และคืนค่าศูนย์หากจำเป็น
Richard Venable

6

อีกวิธีหนึ่งในการหลีกเลี่ยงข้อ จำกัด คือการทำงานกับคลาสฟังก์ชันเพื่อทำการเริ่มต้น คุณอาจต้องการย้ายฟังก์ชันนั้นไปยังส่วนขยาย:

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

การใช้มันจะกลายเป็น:

if let user = User.fromDictionary(someDict) {

     // Party hard
}

1
ฉันชอบสิ่งนี้; ฉันชอบให้ตัวสร้างมีความโปร่งใสเกี่ยวกับสิ่งที่พวกเขาต้องการและการส่งผ่านในพจนานุกรมนั้นทึบมาก
Ben Leggiero

3

แม้ว่า Swift 2.2 จะได้รับการเผยแพร่แล้วและคุณไม่จำเป็นต้องเริ่มต้นอ็อบเจ็กต์อย่างสมบูรณ์อีกต่อไปก่อนที่จะล้มเหลวในการเริ่มต้นคุณต้องถือม้าไว้จนกว่าhttps://bugs.swift.org/browse/SR-704จะได้รับการแก้ไข


1

ฉันพบว่าสิ่งนี้สามารถทำได้ใน Swift 1.2

มีเงื่อนไขบางประการ:

  • คุณสมบัติที่จำเป็นควรได้รับการประกาศเป็นตัวเลือกที่ไม่มีการปิดกั้นโดยปริยาย
  • กำหนดค่าให้กับคุณสมบัติที่คุณต้องการเพียงครั้งเดียว ค่านี้อาจเป็นศูนย์
  • จากนั้นเรียก super.init () หากคลาสของคุณสืบทอดมาจากคลาสอื่น
  • หลังจากกำหนดค่าคุณสมบัติที่คุณต้องการทั้งหมดแล้วให้ตรวจสอบว่าค่าของคุณสมบัตินั้นเป็นไปตามที่คาดไว้หรือไม่ ถ้าไม่ให้คืนค่าศูนย์

ตัวอย่าง:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}

0

ตัวเริ่มต้นที่ล้มเหลวสำหรับชนิดค่า (นั่นคือโครงสร้างหรือการแจงนับ) สามารถทำให้เกิดความล้มเหลวในการเริ่มต้น ณ จุดใดก็ได้ภายในการใช้งานตัวเริ่มต้น

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

ตัดตอนมาจาก: Apple Inc. “ The Swift Programming Language. ” iBooks https://itun.es/sg/jEUH0.l


0

คุณสามารถใช้การเริ่มต้นความสะดวก :

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

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