พนักงานเก็บขยะจะโทรหา IDisposable ทิ้งให้ฉันไหม


139

NET IDisposable Pattern หมายความว่าหากคุณเขียน Finalizer และใช้ IDisposable โปรแกรมสุดท้ายของคุณจะต้องเรียก Dispose อย่างชัดเจน นี่เป็นเหตุผลและเป็นสิ่งที่ฉันทำมาโดยตลอดในสถานการณ์ที่หายากซึ่งมีการรับประกันขั้นสุดท้าย

อย่างไรก็ตามจะเกิดอะไรขึ้นถ้าฉันทำสิ่งนี้:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

และอย่าใช้โปรแกรมปิดท้ายหรืออะไรเลย เฟรมเวิร์กจะเรียกเมธอด Dispose ให้ฉันหรือไม่

ใช่ฉันรู้ว่านี่ฟังดูโง่และตรรกะทั้งหมดก็บอกเป็นนัยว่ามันจะไม่เกิดขึ้น แต่ฉันมักจะมี 2 สิ่งที่ด้านหลังศีรษะซึ่งทำให้ฉันไม่แน่ใจ

  1. เมื่อสองสามปีก่อนเคยบอกฉันว่ามันจะทำได้จริงและคน ๆ นั้นมีประวัติที่มั่นคงมากว่า "รู้เรื่องของพวกเขา"

  2. คอมไพเลอร์ / เฟรมเวิร์กทำสิ่ง 'เวทมนตร์' อื่น ๆ ขึ้นอยู่กับอินเทอร์เฟซที่คุณใช้ (เช่น foreach, extension method, serialization ตามคุณสมบัติ ฯลฯ ) ดังนั้นจึงสมเหตุสมผลว่านี่อาจเป็น 'magic' ด้วย

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

คำตอบ:


122

Net Garbage Collector เรียกเมธอด Object.Finalize ของอ็อบเจ็กต์ในการรวบรวมขยะ โดยค่าเริ่มต้นสิ่งนี้จะไม่ทำอะไรเลยและต้องถูกแทนที่หากคุณต้องการเพิ่มทรัพยากรเพิ่มเติม

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

ดูhttp://msdn.microsoft.com/en-us/library/system.object.finalize.aspxสำหรับข้อมูลเพิ่มเติม


36
อันที่จริงฉันไม่เชื่อว่า GC เรียก Object สุดท้ายเลยถ้ามันไม่ถูกแทนที่ ออบเจ็กต์ถูกกำหนดให้ไม่มีโปรแกรม Finalizer อย่างมีประสิทธิภาพและการปิดท้ายจะถูกระงับ - ซึ่งทำให้มีประสิทธิภาพมากขึ้นเนื่องจากอ็อบเจ็กต์ไม่จำเป็นต้องอยู่ในคิวการสรุป / แบบอิสระ
Jon Skeet

8
ตาม MSDN: msdn.microsoft.com/en-us/library/…คุณไม่สามารถ "แทนที่" เมธอด Object.Finalize ใน C # ได้คอมไพลเลอร์จะสร้างข้อผิดพลาด: อย่าแทนที่ object.Finalize ให้ระบุผู้ทำลายแทน ; กล่าวคือคุณต้องใช้ตัวทำลายที่ทำหน้าที่เป็น Finalizer ได้อย่างมีประสิทธิภาพ [เพิ่มที่นี่เพื่อความสมบูรณ์เนื่องจากนี่เป็นคำตอบที่ยอมรับและมีแนวโน้มว่าจะถูกอ่านมากที่สุด]
Sudhanshu Mishra

1
GC ไม่ทำอะไรกับออบเจ็กต์ที่ไม่ได้แทนที่ Finalizer ไม่ได้อยู่ในคิว Finalization - และไม่มีการเรียก Finalizer
Dave Black

1
@dotnetguy - แม้ว่าข้อกำหนด C # ดั้งเดิมจะกล่าวถึง "ตัวทำลาย" แต่จริงๆแล้วมันเรียกว่า Finalizer - และกลไกของมันก็แตกต่างไปจากที่ "ตัวทำลาย" ที่แท้จริงทำงานสำหรับภาษาที่ไม่มีการจัดการ
Dave Black

68

ฉันต้องการเน้นประเด็นของ Brian ในความคิดเห็นของเขาเพราะมันสำคัญ

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

แต่สิ่งที่ไม่ดีเกี่ยวกับ Finalizers ก็คืออย่างที่ Brian กล่าวไว้มันทำให้วัตถุของคุณรอดจากการเก็บขยะ สิ่งนี้อาจไม่ดี ทำไม?

อย่างที่คุณอาจทราบหรือไม่ก็ตาม GC แบ่งออกเป็นรุ่น ๆ - Gen 0, 1 และ 2 รวมทั้ง Large Object Heap Split เป็นคำศัพท์ที่หลวม - คุณจะได้รับหน่วยความจำหนึ่งบล็อก แต่มีตัวชี้ตำแหน่งที่วัตถุ Gen 0 เริ่มต้นและสิ้นสุด

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

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

ซึ่งหมายความว่าหากคุณทำสิ่งนี้:

~MyClass() { }

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

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


8
ฉันยอมรับว่าคุณต้องการใช้ IDisposable ทุกครั้งที่ทำได้ แต่คุณควรมี Finalizer ที่เรียกใช้วิธีการกำจัด คุณสามารถเรียก GC.SuppressFinalize () ใน IDispose.Dispose หลังจากเรียกวิธีการกำจัดของคุณเพื่อให้แน่ใจว่าวัตถุของคุณจะไม่อยู่ในคิว Finalizer
jColeson

2
รุ่นต่างๆมีหมายเลข 0-2 ไม่ใช่ 1-3 แต่โพสต์ของคุณก็ดี แม้ว่าฉันจะเพิ่มมันว่าวัตถุใด ๆ ที่อ้างถึงโดยวัตถุของคุณหรือวัตถุใด ๆ ที่อ้างถึงโดยสิ่งเหล่านั้น ฯลฯ จะได้รับการป้องกันจากการรวบรวมขยะ (แม้ว่าจะไม่ใช่การสรุปผล) สำหรับรุ่นอื่น ดังนั้นออบเจ็กต์ที่มีโปรแกรมสุดท้ายไม่ควรอ้างอิงถึงสิ่งที่ไม่จำเป็นสำหรับการสรุปผล
supercat


3
เกี่ยวกับ "วัตถุของคุณไม่ว่าอะไรจะอยู่ถึงรุ่นที่ 2" นี่คือข้อมูลพื้นฐานมาก! ช่วยประหยัดเวลาในการดีบักระบบได้มากซึ่งมีวัตถุ Gen2 ที่มีอายุสั้นจำนวนมาก "เตรียมพร้อม" สำหรับการสรุปผล แต่ไม่เคยสรุปสาเหตุ OutOfMemoryException เนื่องจากการใช้งานฮีปจำนวนมาก การลบ Finalizer (แม้ว่างเปล่า) และย้าย (ทำงาน) รหัสไปที่อื่นปัญหาก็หายไปและ GC สามารถจัดการกับโหลด
เครื่องเหลา

@CoryFoy "วัตถุของคุณไม่ว่าอะไรจะอยู่ถึงเจเนอเรชั่น 2" มีเอกสารประกอบเรื่องนี้หรือไม่?
Ashish Negi

33

มีการพูดคุยที่ดีมากมายที่นี่และฉันไปงานปาร์ตี้ช้าไปหน่อย แต่ฉันต้องการเพิ่มคะแนนให้ตัวเอง

  • เครื่องเก็บขยะจะไม่ใช้วิธีการกำจัดให้คุณโดยตรง
  • GC จะดำเนินการขั้นสุดท้ายเมื่อรู้สึกเช่นนั้น
  • รูปแบบทั่วไปอย่างหนึ่งที่ใช้สำหรับออบเจ็กต์ที่มี Finalizer คือการเรียกใช้เมธอดซึ่งตามแบบแผนกำหนดว่า Dispose (การกำจัดบูล) ส่งผ่านเท็จเพื่อระบุว่าการเรียกนั้นเกิดจากการสรุปผลแทนที่จะเป็นการเรียก Dispose อย่างชัดเจน
  • เนื่องจากไม่ปลอดภัยที่จะตั้งสมมติฐานเกี่ยวกับออบเจ็กต์ที่มีการจัดการอื่น ๆ ในขณะที่สรุปออบเจ็กต์ (อาจสรุปไปแล้ว)

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

นั่นเป็นเวอร์ชันที่เรียบง่าย แต่มีความแตกต่างมากมายที่สามารถนำคุณไปสู่รูปแบบนี้ได้

  • สัญญาสำหรับ IDisposable.Dispose ระบุว่าต้องปลอดภัยในการโทรหลายครั้ง (การเรียก Dispose บนวัตถุที่ถูกกำจัดไปแล้วไม่ควรทำอะไรเลย)
  • อาจมีความซับซ้อนมากในการจัดการลำดับชั้นการสืบทอดของวัตถุที่ใช้แล้วทิ้งอย่างเหมาะสมโดยเฉพาะอย่างยิ่งหากเลเยอร์ต่างๆแนะนำทรัพยากรใหม่ที่ใช้แล้วทิ้งและไม่มีการจัดการ ในรูปแบบด้านบน Dispose (บูล) เป็นเสมือนที่อนุญาตให้ลบล้างเพื่อให้สามารถจัดการได้ แต่ฉันพบว่ามีข้อผิดพลาดได้ง่าย

ในความคิดของฉันเป็นการดีกว่ามากที่จะหลีกเลี่ยงการมีประเภทใด ๆ ที่มีทั้งข้อมูลอ้างอิงที่ใช้แล้วทิ้งและแหล่งข้อมูลดั้งเดิมที่อาจต้องมีการสรุปผลโดยตรง SafeHandles เป็นวิธีที่สะอาดมากในการทำเช่นนี้โดยการห่อหุ้มทรัพยากรดั้งเดิมลงในแบบใช้แล้วทิ้งซึ่งจัดเตรียมการสรุปผลภายในของตนเอง (พร้อมกับประโยชน์อื่น ๆ อีกมากมายเช่นการลบหน้าต่างในระหว่างการ P / Invoke ซึ่งอาจทำให้หมายเลขอ้างอิงดั้งเดิมสูญหายเนื่องจากข้อยกเว้นแบบอะซิงโครนัส) .

เพียงการกำหนด SafeHandle ทำให้สิ่งนี้เป็นเรื่องเล็กน้อย:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

ช่วยให้คุณลดความซับซ้อนของประเภทที่มีเป็น:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

1
คลาส SafeHandleZeroOrMinusOneIsInvalid มาจากไหน? เป็นประเภท. net ในตัวหรือไม่?
Orion Edwards

+1 สำหรับ // ในความคิดของฉันเป็นการดีกว่ามากที่จะหลีกเลี่ยงการมีประเภทใด ๆ ที่มีทั้งการอ้างอิงแบบใช้แล้วทิ้งและแหล่งข้อมูลดั้งเดิมที่อาจต้องมีการสรุปผลโดยตรง การสรุป
supercat

1
@OrionEdwards ใช่ดูmsdn.microsoft.com/en-us/library/…
Martin Capodici

1
เกี่ยวกับการโทรไปGC.SuppressFinalizeในตัวอย่างนี้ ในบริบทนี้ควรเรียก SuppressFinalize ก็ต่อเมื่อDispose(true)ดำเนินการสำเร็จ หากDispose(true)ล้มเหลวในบางจุดหลังจากการปิดท้ายถูกระงับ แต่ก่อนที่ทรัพยากรทั้งหมด (โดยเฉพาะอย่างยิ่งที่ไม่มีการจัดการ) จะถูกล้างคุณยังคงต้องการให้การสรุปเกิดขึ้นเพื่อทำการล้างข้อมูลให้มากที่สุด ดีกว่าที่จะย้ายGC.SuppressFinalizeโทรเข้าไปในวิธีการหลังจากที่โทรไปDispose() Dispose(true)ดูแนวทางการออกแบบกรอบและโพสต์นี้
BitMask777

6

ฉันไม่คิดอย่างนั้น คุณสามารถควบคุมได้ว่าจะเรียก Dispose เมื่อใดซึ่งหมายความว่าในทางทฤษฎีคุณสามารถเขียนรหัสการกำจัดที่ตั้งสมมติฐานเกี่ยวกับ (เช่น) การมีอยู่ของวัตถุอื่น คุณไม่สามารถควบคุมได้ว่าจะมีการเรียก Finalizer เมื่อใดดังนั้นจึงเป็นเรื่องที่ไม่ดีหากจะให้ Finalizer เรียก Dispose ในนามของคุณโดยอัตโนมัติ


แก้ไข: ฉันออกไปและทดสอบเพื่อให้แน่ใจว่า:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

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

3

ไม่ใช่ในกรณีที่คุณอธิบาย แต่ GC จะเรียกFinalizerให้คุณหากคุณมี

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

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


1

ไม่เรียกว่า.

แต่วิธีนี้ทำให้อย่าลืมทิ้งสิ่งของของคุณได้อย่างง่ายดาย เพียงแค่ใช้usingคีย์เวิร์ด

ฉันได้ทำการทดสอบต่อไปนี้สำหรับสิ่งนี้:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

1
นี่เป็นตัวอย่างว่าถ้าคุณไม่ใช้ <code> โดยใช้ </code> คำหลักมันจะไม่ถูกเรียก ... และตัวอย่างนี้มีอายุ 9 ปีสุขสันต์วันเกิด!
penyaskito

1

GC จะไม่เรียกทิ้ง มันอาจจะเรียก finalizer ของคุณ แต่แม้นี้ไม่สามารถรับประกันได้ในทุกสถานการณ์

ดูบทความนี้สำหรับการอภิปรายเกี่ยวกับวิธีจัดการที่ดีที่สุด


0

เอกสารเกี่ยวกับIDisposableให้คำอธิบายลักษณะการทำงานที่ชัดเจนและมีรายละเอียดรวมถึงโค้ดตัวอย่าง GC จะไม่เรียกใช้Dispose()เมธอดบนอินเทอร์เฟซ แต่จะเรียกตัวสุดท้ายสำหรับวัตถุของคุณ


0

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

ความล้มเหลวที่ปลอดภัยสำหรับรูปแบบคือการใช้โปรแกรมสุดท้ายที่เรียกใช้เมธอด Dispose () หากคุณไม่ทำเช่นนั้นคุณอาจสร้างการรั่วไหลของหน่วยความจำเช่น: หากคุณสร้าง COM wrapper และไม่เคยเรียกใช้ System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (ซึ่งจะอยู่ใน Dispose method)

ไม่มีเวทย์มนตร์ใดใน clr ที่จะเรียกใช้เมธอด Dispose โดยอัตโนมัตินอกเหนือจากการติดตามวัตถุที่มี Finalizer และจัดเก็บไว้ในตาราง Finalizer โดย GC และเรียกใช้เมื่อ GC บางตัวล้างข้อมูลโดย GC

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