เป็นไปได้ไหมที่จะอนุญาตให้เรียกใช้ didSet ในระหว่างการกำหนดค่าเริ่มต้นใน Swift


217

คำถาม

เอกสารของ Appleระบุว่า:

ผู้สังเกตการณ์ willSet และ didSet จะไม่ถูกเรียกเมื่อคุณสมบัติถูกเริ่มต้นครั้งแรก พวกเขาจะถูกเรียกเฉพาะเมื่อค่าของคุณสมบัติการตั้งค่านอกบริบทการเริ่มต้น

เป็นไปได้ไหมที่จะบังคับให้เรียกสิ่งเหล่านี้ในระหว่างการเริ่มต้น?

ทำไม?

สมมติว่าฉันมีชั้นนี้

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

ฉันสร้างวิธีการdoStuffเพื่อให้การประมวลผลมีความกระชับมากขึ้น แต่ฉันต้องการเพียงแค่ประมวลผลคุณสมบัติภายในdidSetฟังก์ชัน มีวิธีบังคับให้สิ่งนี้เรียกในระหว่างการเริ่มต้นหรือไม่?

ปรับปรุง

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


มันเป็นการดีที่สุดอย่างแท้จริงเพียงเพื่อ "ยอมรับสิ่งนี้เป็นวิธีที่รวดเร็ว" หากคุณใส่ค่าเริ่มต้นแบบอินไลน์ในคำสั่ง var แน่นอนว่าจะไม่เรียกว่า "didSet" นั่นเป็นสาเหตุที่สถานการณ์ "init ()" ต้องไม่เรียก "didSet" ด้วย ทุกอย่างสมเหตุสมผล
Fattie

@Logan คำถามตอบคำถามของฉันเอง;) ขอบคุณ!
AamirR

2
หากคุณต้องการใช้ initializer อำนวยความสะดวกคุณสามารถรวมกับdefer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

คำตอบ:


100

สร้าง set-Method ของตัวเองและใช้มันภายใน init-Method ของคุณ:

class SomeClass {
    var someProperty: AnyObject! {
        didSet {
            //do some Stuff
        }
    }

    init(someProperty: AnyObject) {
        setSomeProperty(someProperty)
    }

    func setSomeProperty(newValue:AnyObject) {
        self.someProperty = newValue
    }
}

ด้วยการประกาศsomePropertyเป็นประเภท: AnyObject!(ตัวเลือกที่ยังไม่ได้เปิดใช้โดยปริยาย) คุณอนุญาตให้ตัวเองเริ่มต้นได้อย่างเต็มที่โดยไม่ต้อง somePropertyตั้งค่า เมื่อคุณเรียก คุณโทรเทียบเท่าsetSomeProperty(someProperty) self.setSomeProperty(someProperty)โดยปกติคุณจะไม่สามารถทำสิ่งนี้ได้เพราะตนเองยังไม่ได้เริ่มต้นอย่างเต็มที่ เนื่องจาก somePropertyไม่ต้องการการเริ่มต้นและคุณกำลังเรียกใช้เมธอดที่ขึ้นอยู่กับตัวเอง Swift จะปล่อยบริบทการเริ่มต้นและ didSet จะทำงาน


3
ทำไมสิ่งนี้จึงแตกต่างจาก self.someProperty = newValue ใน init คุณมีกรณีทดสอบการทำงานหรือไม่?
mmmmmm

2
ไม่รู้ว่าทำไมมันถึงใช้ได้ - แต่มันก็ใช้ได้ การตั้งค่าใหม่โดยตรงภายใน init () - เมธอดไม่ได้เรียก didSet () - แต่ใช้เมธอด setSomeProperty () ที่กำหนด
โอลิเวอร์

50
ฉันคิดว่าฉันรู้ว่าเกิดอะไรขึ้นที่นี่ ด้วยการประกาศsomePropertyเป็นประเภท: AnyObject!(ตัวเลือกที่ไม่ได้เปิดใช้งานโดยปริยาย) คุณอนุญาตให้selfเริ่มต้นได้อย่างเต็มที่โดยไม่ต้องsomePropertyตั้งค่า เมื่อคุณเรียกคุณโทรเทียบเท่าsetSomeProperty(someProperty) self.setSomeProperty(someProperty)โดยปกติคุณจะไม่สามารถทำเช่นนี้เพราะselfยังไม่ได้รับการเริ่มต้นอย่างเต็มที่ เนื่องจากsomePropertyไม่ต้องการการกำหนดค่าเริ่มต้นและคุณกำลังเรียกใช้เมธอดที่พึ่งพาselfSwift จะปล่อยบริบทการเริ่มต้นและdidSetจะทำงาน
โลแกน

3
ดูเหมือนว่าสิ่งใดก็ตามที่ตั้งอยู่นอก init จริง () DOES ได้รับ didSet ที่เรียกว่า ตัวอักษรใด ๆ ที่ไม่ได้ตั้งค่าinit() { *HERE* }จะมีการเรียกใช้ตั้งค่า ดีมากสำหรับคำถามนี้ แต่ใช้ฟังก์ชันรองเพื่อตั้งค่าบางอย่างนำไปสู่การตั้งค่า didSet ที่ถูกเรียก ไม่ดีถ้าคุณต้องการใช้ฟังก์ชัน setter นั้นซ้ำ
WCByrne

4
@ ทำเครื่องหมายอย่างจริงจังการพยายามใช้สามัญสำนึกหรือตรรกะกับ Swift นั้นไร้สาระ แต่คุณสามารถไว้วางใจ upvotes หรือลองด้วยตัวคุณเอง ฉันเพิ่งทำแล้วมันได้ผล และใช่มันไม่สมเหตุสมผลเลย
Dan Rosenstark

305

ถ้าคุณใช้deferภายในของการเริ่มต้นสำหรับการปรับปรุงคุณสมบัติตัวเลือกใด ๆ หรือการปรับปรุงเพิ่มเติมคุณสมบัติที่ไม่ใช่ตัวเลือกที่คุณได้เริ่มต้นแล้วและหลังจากที่คุณได้เรียกว่าใด ๆsuper.init()วิธีแล้วของคุณwillSet, didSetฯลฯ จะถูกเรียกว่า ฉันพบว่าวิธีนี้สะดวกกว่าการใช้วิธีแยกต่างหากที่คุณต้องติดตามการโทรในที่ที่เหมาะสม

ตัวอย่างเช่น:

public class MyNewType: NSObject {

    public var myRequiredField:Int

    public var myOptionalField:Float? {
        willSet {
            if let newValue = newValue {
                print("I'm going to change to \(newValue)")
            }
        }
        didSet {
            if let myOptionalField = self.myOptionalField {
                print("Now I'm \(myOptionalField)")
            }
        }
    }

    override public init() {
        self.myRequiredField = 1

        super.init()

        // Non-defered
        self.myOptionalField = 6.28

        // Defered
        defer {
            self.myOptionalField = 3.14
        }
    }
}

จะให้ผล:

I'm going to change to 3.14
Now I'm 3.14

6
เล่ห์เหลี่ยมเล่ห์เหลี่ยมฉันชอบมัน ... แปลกประหลาดของสวิฟท์แม้ว่า
Chris Hatton

4
ยิ่งใหญ่ deferคุณพบอีกกรณีที่มีประโยชน์ของการใช้ ขอบคุณ
Ryan

1
ใช้งานได้ดีมาก! หวังว่าฉันจะให้คุณมากกว่าหนึ่งโหวต
Christian Schnorr

3
น่าสนใจมาก .. ฉันไม่เคยเห็นแบบนี้มาก่อนเลย มันมีคารมคมคายและทำงาน
aBikis

แต่คุณตั้ง myOptionalField สองครั้งซึ่งไม่ได้ยอดเยี่ยมจากมุมมองด้านประสิทธิภาพ จะเกิดอะไรขึ้นถ้ามีการคำนวณจำนวนมาก
Yakiv Kovalskyi

77

ในรูปแบบของคำตอบของ Oliver คุณสามารถพันบรรทัดด้วยการปิด เช่น:

class Classy {

    var foo: Int! { didSet { doStuff() } }

    init( foo: Int ) {
        // closure invokes didSet
        ({ self.foo = foo })()
    }

}

แก้ไข: คำตอบของ Brian Westphal คือ nicer imho สิ่งที่ดีเกี่ยวกับเขาก็คือมันบ่งบอกถึงเจตนา


2
มันฉลาดและไม่ก่อให้เกิดมลพิษในชั้นเรียนด้วยวิธีการที่ซ้ำซ้อน
pointum

คำตอบที่ดีขอบคุณ! เป็นที่น่าสังเกตว่าใน Swift 2.2 คุณต้องปิดวงเล็บในวงเล็บเสมอ
สูงสุด

@ Max Cheers ตัวอย่างรวมถึง parens ตอนนี้
original_username

2
คำตอบที่ฉลาด! One note: ถ้าคลาสของคุณเป็นคลาสย่อยของอย่างอื่นคุณจะต้องเรียก super.init () ก่อน ({self.foo = foo}) ()
kcstricks

1
@Hlung ฉันไม่รู้สึกว่ามันจะมีแนวโน้มที่จะแตกหักในอนาคตมากกว่าสิ่งอื่นใด แต่ก็ยังมีปัญหาที่ไม่แสดงความคิดเห็นรหัสมันไม่ชัดเจนว่ามันทำอะไร อย่างน้อยคำตอบ "ชะลอ" ของ Brian ทำให้ผู้อ่านได้คำใบ้ว่าเราต้องการดำเนินการโค้ดหลังจากการเริ่มต้น
original_username

10

ฉันมีปัญหาเดียวกันและสิ่งนี้ใช้ได้สำหรับฉัน

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        defer { self.someProperty = someProperty }
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

คุณได้รับมาจากไหน
Vanya

3
วิธีนี้ใช้ไม่ได้อีกต่อไป คอมไพเลอร์โยนข้อผิดพลาดสองข้อ: - 'ตัวเอง' ที่ใช้ในการเรียกเมธอด '$ defer' ก่อนที่คุณสมบัติที่เก็บไว้ทั้งหมดจะถูกเตรียมใช้งาน - ส่งคืนจาก initializer โดยไม่ต้องเริ่มต้นคุณสมบัติที่เก็บไว้ทั้งหมด
Edward B

คุณพูดถูก แต่มีวิธีแก้ปัญหา คุณจะต้องประกาศคุณสมบัติบางอย่างโดยปริยายไม่ได้ใส่หรือไม่ใส่ก็ได้และจะให้เป็นค่าเริ่มต้นโดยไม่มีการรวมตัวกันเพื่อหลีกเลี่ยงความล้มเหลว TLDR; someProperty ต้องเป็นประเภทที่เป็นตัวเลือก
ทำเครื่องหมาย

2

วิธีนี้ใช้ได้ผลถ้าคุณทำในคลาสย่อย

class Base {

  var someProperty: AnyObject {
    didSet {
      doStuff()
    }
  }

  required init() {
    someProperty = "hello"
  }

  func doStuff() {
    print(someProperty)
  }
}

class SomeClass: Base {

  required init() {
    super.init()

    someProperty = "hello"
  }
}

let a = Base()
let b = SomeClass()

ในaตัวอย่างเช่นdidSetไม่ได้เรียก แต่ในbตัวอย่างdidSetจะถูกทริกเกอร์เพราะมันอยู่ในคลาสย่อย มันต้องทำอะไรซักอย่างกับสิ่งที่มีความinitialization contextหมายจริงๆในกรณีนี้พวกsuperclassเขาสนใจ


1

แม้ว่านี่จะไม่ใช่วิธีแก้ปัญหา แต่เป็นอีกวิธีหนึ่งในการดำเนินการโดยใช้ตัวสร้างคลาส:

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            // do stuff
        }
    }

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}

1
โซลูชันนี้ต้องการให้คุณเปลี่ยนทางเลือกsomePropertyเนื่องจากจะไม่ให้ค่าในระหว่างการเริ่มต้น
Mick MacCallum

1

ในกรณีเฉพาะที่คุณต้องการเรียกใช้willSetหรือdidSetภายในinitสำหรับคุณสมบัติที่มีอยู่ในซูเปอร์คลาสของคุณคุณสามารถกำหนดคุณสมบัติซุปเปอร์ของคุณโดยตรง:

override init(frame: CGRect) {
    super.init(frame: frame)
    // this will call `willSet` and `didSet`
    someProperty = super.someProperty
}

โปรดทราบว่าวิธีการแก้ปัญหาของCharlesism ที่มีการปิดก็จะทำงานได้เช่นกัน ดังนั้นทางออกของฉันเป็นเพียงทางเลือก


-1

คุณสามารถแก้ไขได้ด้วยวิธี obj-с:

class SomeClass {
    private var _someProperty: AnyObject!
    var someProperty: AnyObject{
        get{
            return _someProperty
        }
        set{
            _someProperty = newValue
            doStuff()
        }
    }
    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

1
สวัสดี @yshilov ฉันไม่คิดว่าจะแก้ปัญหาที่ฉันคาดหวังได้จริง ๆ ทางเทคนิคเมื่อคุณโทรหาself.somePropertyคุณกำลังออกจากขอบเขตการเริ่มต้น var someProperty: AnyObject! = nil { didSet { ... } }ผมเชื่อว่าสิ่งเดียวกันสามารถทำได้โดยการทำ ถึงกระนั้นบางคนก็อาจชื่นชมมันเป็นสิ่งที่ต้องทำขอบคุณสำหรับการเพิ่ม
Logan

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