การตั้งค่าวัตถุเป็น null vs Dispose ()


108

ฉันรู้สึกทึ่งกับวิธีการทำงานของ CLR และ GC (ฉันกำลังขยายความรู้ในเรื่องนี้โดยการอ่าน CLR ผ่าน C # หนังสือ / โพสต์ของ Jon Skeet และอื่น ๆ )

อย่างไรก็ตามอะไรคือความแตกต่างระหว่างการพูดว่า:

MyClass myclass = new MyClass();
myclass = null;

หรือโดยการทำให้ MyClass ใช้ IDisposable และ destructor และเรียก Dispose ()?

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

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

คลาสสตรีม (เช่น BinaryWriter) มีวิธี Finalize หรือไม่? ทำไมฉันถึงต้องการใช้สิ่งนั้น?

คำตอบ:


210

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

Dispose, การเก็บขยะและการสรุป

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

การกำจัดเป็นเรื่องเกี่ยวกับทรัพยากรที่ไม่มีการจัดการ ( ทรัพยากรที่ไม่ใช่หน่วยความจำ) สิ่งเหล่านี้อาจเป็นที่จับ UI การเชื่อมต่อเครือข่ายที่จับไฟล์เป็นต้นสิ่งเหล่านี้เป็นทรัพยากรที่มี จำกัด ดังนั้นโดยทั่วไปคุณจึงต้องการเผยแพร่โดยเร็วที่สุด คุณควรใช้IDisposableเมื่อใดก็ตามที่ประเภทของคุณ "เป็นเจ้าของ" ทรัพยากรที่ไม่มีการจัดการไม่ว่าโดยตรง (โดยปกติจะผ่านทางIntPtr) หรือทางอ้อม (เช่นผ่านทางStreama SqlConnectionฯลฯ )

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

บิดคือการสรุป ตัวเก็บขยะจะเก็บรายการของวัตถุที่ไม่สามารถเข้าถึงได้อีกต่อไป แต่มีตัวสุดท้าย (เขียนเป็น~Foo()ภาษา C # ค่อนข้างสับสน - พวกมันไม่เหมือนกับตัวทำลาย C ++) มันเรียกใช้ Finalizers บนวัตถุเหล่านี้ในกรณีที่พวกเขาจำเป็นต้องทำการล้างข้อมูลเพิ่มเติมก่อนที่หน่วยความจำจะถูกปลดปล่อย

Finalizers มักใช้ในการล้างทรัพยากรในกรณีที่ผู้ใช้ประเภทนั้นลืมทิ้งอย่างเป็นระเบียบ ดังนั้นถ้าคุณเปิดFileStreamแต่ลืมโทรDisposeหรือClose, finalizer จะในที่สุดก็ปล่อยจับแฟ้มต้นแบบสำหรับคุณ ในโปรแกรมที่เขียนมาอย่างดีผู้เข้ารอบสุดท้ายแทบจะไม่เริ่มทำงานในความคิดของฉัน

การตั้งค่าตัวแปรเป็น null

จุดเล็ก ๆ จุดหนึ่งในการตั้งค่าตัวแปรเป็นnull- แทบจะไม่จำเป็นสำหรับการเก็บขยะ บางครั้งคุณอาจต้องการทำหากเป็นตัวแปรสมาชิกแม้ว่าจากประสบการณ์ของฉันมันหายากสำหรับ "บางส่วน" ของวัตถุที่ไม่จำเป็นอีกต่อไป เมื่อเป็นตัวแปรภายใน JIT มักจะฉลาดพอ (ในโหมดรีลีส) ที่จะรู้ว่าเมื่อใดที่คุณจะไม่ใช้การอ้างอิงอีก ตัวอย่างเช่น:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

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

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

การนำ IDisposable / finalizers

ดังนั้นประเภทของคุณเองควรใช้ Finalizers หรือไม่? เกือบจะไม่แน่นอน หากคุณถือทรัพยากรที่ไม่มีการจัดการทางอ้อมเท่านั้น(เช่นคุณมีFileStreamตัวแปรในฐานะสมาชิก) การเพิ่ม Finalizer ของคุณเองจะไม่ช่วยอะไร: สตรีมเกือบจะมีสิทธิ์ได้รับการรวบรวมขยะเมื่อวัตถุของคุณอยู่ดังนั้นคุณจึงสามารถพึ่งพาFileStreamมีโปรแกรมปิดท้าย (ถ้าจำเป็น - อาจหมายถึงอย่างอื่น ฯลฯ ) หากคุณต้องการถือทรัพยากรที่ไม่มีการจัดการ "เกือบ" โดยตรงSafeHandleคือเพื่อนของคุณ - ต้องใช้เวลาสักหน่อยในการดำเนินการ แต่หมายความว่าคุณแทบจะ ไม่ต้องเขียน Finalizer อีกเลย โดยปกติคุณควรจะต้องมีโปรแกรมสุดท้ายก็ต่อเมื่อคุณมีส่วนจัดการโดยตรงกับทรัพยากร (an IntPtr) และคุณควรมองหาSafeHandleโดยเร็วที่สุดเท่าที่คุณจะทำได้. (มีลิงก์สองลิงก์ - อ่านทั้งสองอย่างโดยดี)

Joe Duffy มีแนวทางที่ยาวมากเกี่ยวกับ Finalizers และ IDisposable (เขียนร่วมกับสมาร์ทโฟล์คมากมาย) ซึ่งควรค่าแก่การอ่าน เป็นสิ่งที่ควรทราบว่าหากคุณปิดชั้นเรียนมันจะทำให้ชีวิตง่ายขึ้นมาก: รูปแบบของการแทนที่Disposeเพื่อเรียกDispose(bool)วิธีเสมือนใหม่ฯลฯ จะเกี่ยวข้องก็ต่อเมื่อชั้นเรียนของคุณได้รับการออกแบบมาเพื่อการสืบทอด

นี่เป็นเรื่องเล็กน้อย แต่โปรดขอความกระจ่างในจุดที่คุณต้องการ :)


Re "ครั้งเดียวที่อาจคุ้มค่าที่จะตั้งค่าตัวแปรในเครื่องเป็น null" - บางทีสถานการณ์ "จับภาพ" ที่มีหนามมากกว่า (การจับหลายตัวแปรเดียวกัน) - แต่การโพสต์อาจไม่คุ้มค่า! +1 ...
Marc Gravell

@ มาร์ค: นั่นคือเรื่องจริง - ฉันไม่ได้คิดว่าจะจับตัวแปรได้ อืม. ใช่ฉันคิดว่าฉันจะปล่อยให้มันอยู่คนเดียว;)
Jon Skeet

คุณช่วยบอกได้ไหมว่าจะเกิดอะไรขึ้นเมื่อคุณตั้งค่า "foo = null" ในข้อมูลโค้ดด้านบน เท่าที่ฉันรู้บรรทัดนั้นจะล้างเฉพาะค่าของตัวแปรที่ชี้ไปยังวัตถุ foo ในฮีปที่มีการจัดการ? ดังนั้นคำถามคือจะเกิดอะไรขึ้นกับ foo object ที่นั่น? เราไม่ควรเรียกว่าทิ้ง?
odiseh

@odiseh: ถ้าวัตถุถูกทิ้งใช่ - คุณควรกำจัดมัน ส่วนคำตอบนั้นเกี่ยวข้องกับการเก็บขยะเท่านั้นซึ่งแยกจากกันโดยสิ้นเชิง
Jon Skeet

1
ฉันกำลังมองหาคำชี้แจงเกี่ยวกับข้อกังวลที่ไม่สามารถระบุได้ดังนั้นฉันจึงค้นหา "IDisposable Skeet" และพบสิ่งนี้ เยี่ยมมาก! : D
Maciej Wozniak

22

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

myclass = null;

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


7
มันอาจจะไม่ได้ยังคงอยู่หลังจากรันบรรทัดว่า - มันอาจจะได้รับการเก็บขยะก่อนที่จะสายว่า JIT ฉลาด - การสร้างเส้นแบบนี้แทบจะไม่เกี่ยวข้องกันเลย
Jon Skeet

6
การตั้งค่าเป็น null อาจหมายความว่าทรัพยากรที่ถืออยู่โดยวัตถุจะไม่ถูกปลดปล่อย GC ไม่ได้กำจัด แต่จะทำการสรุปเท่านั้นดังนั้นหากออบเจ็กต์มีทรัพยากรที่ไม่มีการจัดการโดยตรงและตัวสุดท้ายของมันไม่ทิ้ง (หรือไม่มีตัวสุดท้าย) ทรัพยากรเหล่านั้นก็จะรั่วไหล สิ่งที่ควรระวัง
LukeH

6

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

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

พื้นที่เดียวของการทับซ้อนกันคือเมื่อไม่มีการอ้างอิงถึงวัตถุอีกต่อไปในที่สุดก็จะได้รับขยะที่เก็บรวบรวม และถ้าคลาสใช้อินเทอร์เฟซ IDisposable ดังนั้น Dispose () จะถูกเรียกบนวัตถุก่อนที่จะได้รับขยะที่รวบรวม

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

การเรียก Dispose () บนวัตถุไม่ได้ "ฆ่า" วัตถุ แต่อย่างใด โดยทั่วไปจะใช้ในการล้างข้อมูลเพื่อให้สามารถลบวัตถุได้อย่างปลอดภัยในภายหลัง แต่ในที่สุดก็ไม่มีอะไรวิเศษเกี่ยวกับ Dispose แต่เป็นเพียงวิธีการคลาสเท่านั้น


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