จุดประสงค์ของ willSet และ didSet ใน Swift คืออะไร?


265

Swift มีไวยากรณ์การประกาศคุณสมบัติที่คล้ายกับ C #:

var foo: Int {
    get { return getFoo() }
    set { setFoo(newValue) }
}

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


11
โดยส่วนตัวฉันไม่ชอบคำตอบมากมายที่นี่ มันลงไปในไวยากรณ์มากเกินไป ความแตกต่างนั้นเกี่ยวกับอรรถศาสตร์และการอ่านโค้ด Computed อสังหาริมทรัพย์ ( getและset) มีพื้นจะมีทรัพย์สินที่คำนวณขึ้นอยู่กับทรัพย์สินของผู้อื่นเช่นการแปลงเป็นป้ายชื่อลงในปี text & จะมีการพูดว่า ... เฮ้ค่านี้ถูกตั้งค่าตอนนี้ลองทำเช่นแหล่งข้อมูลของเราได้รับการปรับปรุง ... ดังนั้นลองโหลด tableView อีกครั้งเพื่อที่จะรวมแถวใหม่ สำหรับตัวอย่างอื่นดูคำตอบของ dfri เกี่ยวกับวิธีโทรหาผู้ได้รับมอบหมายในIntdidSetwillSetdidSet
น้ำผึ้ง

คำตอบ:


324

ประเด็นน่าจะเป็นในบางครั้งคุณต้องมีคุณสมบัติที่มีที่เก็บข้อมูลอัตโนมัติและพฤติกรรมบางอย่างเช่นเพื่อแจ้งวัตถุอื่น ๆ ที่คุณสมบัติเพิ่งเปลี่ยน เมื่อทั้งหมดที่คุณมีget/ setคุณจำเป็นต้องมีเขตข้อมูลอื่นเพื่อเก็บค่า ด้วยwillSetและdidSetคุณสามารถดำเนินการเมื่อค่าถูกแก้ไขโดยไม่จำเป็นต้องมีฟิลด์อื่น ตัวอย่างเช่นในตัวอย่างนั้น:

class Foo {
    var myProperty: Int = 0 {
        didSet {
            print("The value of myProperty changed from \(oldValue) to \(myProperty)")
        }
    }
}

myPropertyพิมพ์ค่าเก่าและใหม่ทุกครั้งที่มีการแก้ไข ด้วยแค่ getters และ setters ฉันต้องการสิ่งนี้แทน:

class Foo {
    var myPropertyValue: Int = 0
    var myProperty: Int {
        get { return myPropertyValue }
        set {
            print("The value of myProperty changed from \(myPropertyValue) to \(newValue)")
            myPropertyValue = newValue
        }
    }
}

ดังนั้นwillSetและdidSetเป็นตัวแทนของเศรษฐกิจของสองบรรทัดและเสียงน้อยลงในรายการเขตข้อมูล


248
ข้อควรสนใจ: willSetและdidSetจะไม่ถูกเรียกเมื่อคุณตั้งค่าคุณสมบัติจากภายในวิธีการเริ่มต้นเป็น Apple บันทึกย่อ:willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.
Klaas

4
แต่ดูเหมือนว่าพวกเขาจะถูกเรียกในคุณสมบัติอาร์เรย์เมื่อทำสิ่งนี้: myArrayProperty.removeAtIndex(myIndex)... ไม่ได้คาดหวัง
Andreas

4
คุณสามารถตัดการกำหนดค่าในคำสั่ง defer {} ภายใน initialiser ซึ่งทำให้เมธอด willSet และ didSet ถูกเรียกใช้เมื่อออกจากขอบเขต initialiser ฉันไม่จำเป็นต้องแนะนำเพียงแค่บอกว่าเป็นไปได้ หนึ่งในผลที่ตามมาก็คือมันจะทำงานได้ก็ต่อเมื่อคุณประกาศคุณสมบัติที่เป็นตัวเลือกเนื่องจากมันไม่ได้เริ่มต้นอย่างเข้มงวดจากผู้เริ่มต้น
Marmoy

โปรดอธิบายบรรทัดด้านล่าง ฉันไม่ได้รับมันเป็นวิธีการหรือตัวแปร var propertyChangedListener นี้: (Int, Int) -> Void = {println ("ค่าของ myProperty เปลี่ยนจาก ($ 0) เป็น ($ 1)")}
Vikash Rajput

คุณสมบัติการเริ่มต้นในบรรทัดเดียวกันไม่รองรับใน Swift 3 คุณควรเปลี่ยนคำตอบเพื่อให้สอดคล้องกับ swift 3
Ramazan Polat

149

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

หากคุณมาจาก Objective-C โดยคำนึงว่าการตั้งชื่อนั้นเปลี่ยนไป ใน Swift ตัวแปร iVar หรืออินสแตนซ์ถูกตั้งชื่อคุณสมบัติที่เก็บไว้

ตัวอย่าง 1 (คุณสมบัติอ่านอย่างเดียว) - มีคำเตือน:

var test : Int {
    get {
        return test
    }
}

สิ่งนี้จะส่งผลให้เกิดการเตือนเพราะสิ่งนี้จะส่งผลให้เกิดการเรียกใช้ฟังก์ชันแบบเรียกซ้ำ (ตัวเรียกการเรียกตัวเอง) คำเตือนในกรณีนี้คือ "ความพยายามในการปรับเปลี่ยน

ตัวอย่างที่ 2 การอ่าน / เขียนตามเงื่อนไข - พร้อมคำเตือน

var test : Int {
    get {
        return test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        //(prevents same value being set)
        if (aNewValue != test) {
            test = aNewValue
        }
    }
}

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

ตัวอย่างที่ 3 คุณสมบัติการอ่าน / เขียนที่มีการสำรองข้อมูล

นี่คือรูปแบบที่อนุญาตการตั้งค่าตามเงื่อนไขของคุณสมบัติที่จัดเก็บจริง

//True model data
var _test : Int = 0

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

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

ตัวอย่างที่ 4 การใช้ความตั้งใจและไม่ได้ตั้งค่า

//True model data
var _test : Int = 0 {

    //First this
    willSet {
        println("Old value is \(_test), new value is \(newValue)")
    }

    //value is set

    //Finaly this
    didSet {
        println("Old value is \(oldValue), new value is \(_test)")
    }
}

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

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

ตัวอย่าง 5. ตัวอย่างคอนกรีต - คอนเทนเนอร์ ViewController

//Underlying instance variable (would ideally be private)
var _childVC : UIViewController? {
    willSet {
        //REMOVE OLD VC
        println("Property will set")
        if (_childVC != nil) {
            _childVC!.willMoveToParentViewController(nil)
            self.setOverrideTraitCollection(nil, forChildViewController: _childVC)
            _childVC!.view.removeFromSuperview()
            _childVC!.removeFromParentViewController()
        }
        if (newValue) {
            self.addChildViewController(newValue)
        }

    }

    //I can't see a way to 'stop' the value being set to the same controller - hence the computed property

    didSet {
        //ADD NEW VC
        println("Property did set")
        if (_childVC) {
//                var views  = NSDictionaryOfVariableBindings(self.view)    .. NOT YET SUPPORTED (NSDictionary bridging not yet available)

            //Add subviews + constraints
            _childVC!.view.setTranslatesAutoresizingMaskIntoConstraints(false)       //For now - until I add my own constraints
            self.view.addSubview(_childVC!.view)
            let views = ["view" : _childVC!.view] as NSMutableDictionary
            let layoutOpts = NSLayoutFormatOptions(0)
            let lc1 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("|[view]|",  options: layoutOpts, metrics: NSDictionary(), views: views)
            let lc2 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: layoutOpts, metrics: NSDictionary(), views: views)
            self.view.addConstraints(lc1)
            self.view.addConstraints(lc2)

            //Forward messages to child
            _childVC!.didMoveToParentViewController(self)
        }
    }
}


//Computed property - this is the property that must be used to prevent setting the same value twice
//unless there is another way of doing this?
var childVC : UIViewController? {
    get {
        return _childVC
    }
    set(suggestedVC) {
        if (suggestedVC != _childVC) {
            _childVC = suggestedVC
        }
    }
}

สังเกตการใช้ทั้งคุณสมบัติที่คำนวณและเก็บไว้ ฉันใช้คุณสมบัติที่คำนวณได้เพื่อป้องกันการตั้งค่าเดียวกันสองครั้ง (เพื่อหลีกเลี่ยงสิ่งเลวร้ายที่เกิดขึ้น!); ฉันใช้ willSet และ didSet เพื่อส่งต่อการแจ้งเตือนไปยัง viewControllers (ดูเอกสารประกอบ UIViewController และข้อมูลเกี่ยวกับคอนเทนเนอร์ viewController)

ฉันหวังว่าสิ่งนี้จะช่วยได้และช่วยให้ใครบางคนตะโกนถ้าฉันทำผิดที่ใดก็ได้ที่นี่!


3
ทำไมใช้ไม่ได้ฉันใช้ didSet พร้อมกับ get และ set .. ?
Ben Sinclair

//I can't see a way to 'stop' the value being set to the same controller - hence the computed property คำเตือนหายไปหลังจากที่ฉันใช้ if let newViewController = _childVC { แทน if (_childVC) {
evfemist

5
get และ set ถูกใช้เพื่อสร้างคุณสมบัติที่คำนวณได้ นี่เป็นวิธีการล้วนๆและไม่มีที่เก็บข้อมูลสำรอง (ตัวแปรอินสแตนซ์) willSet และ didSet สำหรับสังเกตการเปลี่ยนแปลงคุณสมบัติตัวแปรที่เก็บไว้ ภายใต้ประทุนเหล่านี้ได้รับการสนับสนุนโดยการจัดเก็บข้อมูล แต่ใน Swift มันรวมกันเป็นหนึ่งเดียว
user3675131

ในตัวอย่าง 5 ของคุณในgetผมคิดว่าคุณต้องเพิ่มแล้วif _childVC == nil { _childVC = something } return _childVC
JW.ZG

18

สิ่งเหล่านี้เรียกว่าผู้สังเกตการณ์อสังหาริมทรัพย์ :

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

ข้อความที่ตัดตอนมาจาก: Apple Inc. “ ภาษาการเขียนโปรแกรม Swift” iBooks https://itun.es/ca/jEUH0.l

ฉันสงสัยว่าจะอนุญาตสำหรับสิ่งที่เราจะทำกับKVOเช่นการผูกข้อมูลกับองค์ประกอบ UI หรือเรียกผลข้างเคียงของการเปลี่ยนคุณสมบัติการทริกเกอร์กระบวนการซิงค์การประมวลผลพื้นหลัง ฯลฯ เป็นต้น


16

บันทึก

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


16

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

let minValue = 1

var value = 1 {
    didSet {
        if value < minValue {
            value = minValue
        }
    }
}

value = -10 // value is minValue now.

10

คำตอบที่มีอยู่เป็นจำนวนมากครอบคลุมคำถาม แต่ฉันจะพูดถึงในรายละเอียดเพิ่มเติมที่ฉันเชื่อว่ามีมูลค่าครอบคลุม


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

ฉันจะอ้างถึง Klaas ความเห็นที่โหวตแล้วกับคำตอบที่ยอมรับ:

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

มันค่อนข้างจะเรียบร้อยเพราะมันหมายถึง didSetคุณสมบัติที่เป็นตัวเลือกที่ดีสำหรับจุดเริ่มต้นสำหรับการเรียกกลับ & ฟังก์ชันสำหรับชั้นเรียนที่คุณกำหนดเอง

ยกตัวอย่างเช่นพิจารณาวัตถุควบคุมผู้ใช้ที่กำหนดเองโดยมีคุณสมบัติหลักบางอย่างvalue(เช่นตำแหน่งในการควบคุมการให้คะแนน) ซึ่งมีการใช้เป็นคลาสย่อยของUIView:

// CustomUserControl.swift
protocol CustomUserControlDelegate {
    func didChangeValue(value: Int)
    // func didChangeValue(newValue: Int, oldValue: Int)
    // func didChangeValue(customUserControl: CustomUserControl)
    // ... other more sophisticated delegate functions
}

class CustomUserControl: UIView {

    // Properties
    // ...
    private var value = 0 {
        didSet {
            // Possibly do something ...

            // Call delegate.
            delegate?.didChangeValue(value)
            // delegate?.didChangeValue(value, oldValue: oldValue)
            // delegate?.didChangeValue(self)
        }
    }

    var delegate: CustomUserControlDelegate?

    // Initialization
    required init?(...) { 
        // Initialise something ...

        // E.g. 'value = 1' would not call didSet at this point
    }

    // ... some methods/actions associated with your user control.
}

หลังจากที่ทำงานของผู้ได้รับสิทธิ์สามารถนำมาใช้ในการพูดบางอย่างควบคุมมุมมองที่จะสังเกตเห็นการเปลี่ยนแปลงที่สำคัญในรูปแบบสำหรับCustomViewControllerเหมือนที่คุณต้องการใช้ฟังก์ชั่นของผู้ร่วมประชุมโดยธรรมชาติของUITextFieldDelegateสำหรับUITextFieldวัตถุ (เช่นtextFieldDidEndEditing(...))

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

// ViewController.swift
Import UIKit
// ...

class ViewController: UIViewController, CustomUserControlDelegate {

    // Properties
    // ...
    @IBOutlet weak var customUserControl: CustomUserControl!

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...

        // Custom user control, handle through delegate callbacks.
        customUserControl = self
    }

    // ...

    // CustomUserControlDelegate
    func didChangeValue(value: Int) {
        // do some stuff with 'value' ...
    }

    // func didChangeValue(newValue: Int, oldValue: Int) {
        // do some stuff with new as well as old 'value' ...
        // custom transitions? :)
    //}

    //func didChangeValue(customUserControl: CustomUserControl) {
    //    // Do more advanced stuff ...
    //}
}

ที่นี่valueสถานที่ให้บริการได้รับการห่อหุ้ม แต่โดยทั่วไป: ในสถานการณ์เช่นนี้โปรดระวังที่จะไม่ปรับปรุงvalueคุณสมบัติของcustomUserControlวัตถุในขอบเขตของฟังก์ชั่นตัวแทนที่เกี่ยวข้อง (ที่นี่:) didChangeValue()ในตัวควบคุมมุมมองหรือคุณจะจบลงด้วย การเรียกซ้ำไม่สิ้นสุด


4

ผู้สังเกตการณ์ willSet และ didSet สำหรับคุณสมบัติเมื่อใดก็ตามที่คุณสมบัติถูกกำหนดค่าใหม่ สิ่งนี้เป็นจริงแม้ว่าค่าใหม่จะเหมือนกับค่าปัจจุบัน

และโปรดทราบว่าwillSetต้องการชื่อพารามิเตอร์เพื่อหลีกเลี่ยงdidSetไม่ได้

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


2

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


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

1
@zneak ฉันใช้คำผิด ฉันอ้างสิทธิ์ในความพยายามของโปรแกรมเมอร์ไม่ใช่ค่าใช้จ่ายในการประมวลผล
Eonil

1

ในคลาส (ฐาน) ของคุณเองwillSetและdidSetค่อนข้างซ้ำซ้อนเนื่องจากคุณสามารถกำหนดคุณสมบัติที่คำนวณได้ (เช่น get- และ set- methods) ที่เข้าถึง a_propertyVariableและทำการpre-post และ prosessing ที่ต้องการ

หากแต่คุณแทนที่ระดับที่ทรัพย์สินเป็นกำหนดไว้แล้ว , แล้วwillSetและdidSetมีประโยชน์และไม่ซ้ำซ้อน!


1

สิ่งหนึ่งที่ didSetมีประโยชน์จริงๆคือเมื่อคุณใช้ช่องเสียบเพื่อเพิ่มการกำหนดค่าเพิ่มเติม

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }

หรือการใช้ willSet จะทำให้รู้สึกถึงผลกระทบบางอย่างในวิธีการของร้านนี้ใช่ไหม?
elia

-5

ฉันไม่รู้ C # แต่ด้วยการคาดเดาเล็กน้อยฉันคิดว่าฉันเข้าใจอะไร

foo : int {
    get { return getFoo(); }
    set { setFoo(newValue); }
}

ทำ. มันดูคล้ายกับสิ่งที่คุณมีใน Swift แต่มันไม่เหมือนกัน: ใน Swift คุณไม่มีgetFooและsetFooและนั่นไม่แตกต่างกันเล็กน้อย: หมายความว่าคุณไม่มีพื้นที่เก็บข้อมูลพื้นฐานสำหรับค่าของคุณ

Swift ได้จัดเก็บและคำนวณคุณสมบัติ

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

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


นี่คือตัวอย่างที่รวดเร็ว getFooและsetFooเป็นตัวยึดตำแหน่งแบบง่าย ๆ สำหรับทุกสิ่งที่คุณต้องการให้ผู้ได้รับและผู้ตั้งถิ่นฐานทำ C # ไม่ต้องการทั้งสองอย่าง (ฉันไม่พลาดรายละเอียดปลีกย่อยการสร้างประโยคน้อยผมถามก่อนที่จะมีการเข้าถึงคอมไพเลอร์.)
zneak

1
โอวตกลง. แต่จุดสำคัญคือคุณสมบัติที่คำนวณได้ไม่มีที่เก็บข้อมูลพื้นฐาน ดูคำตอบอื่น ๆ ของฉัน: stackoverflow.com/a/24052566/574590
ไฟล์อะนาล็อก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.