การใช้วิธีจบ / กำจัดใน C #


381

C # 2008

ฉันทำงานนี้มาระยะหนึ่งแล้วและฉันยังสับสนเกี่ยวกับการใช้ขั้นตอนสุดท้ายและการกำจัดในโค้ด คำถามของฉันอยู่ด้านล่าง:

  1. ฉันรู้ว่าเราต้องการเครื่องมือขั้นสุดท้ายขณะที่ทิ้งทรัพยากรที่ไม่มีการจัดการ อย่างไรก็ตามหากมีการจัดการทรัพยากรที่โทรไปยังทรัพยากรที่ไม่มีการจัดการมันจะยังคงต้องใช้ตัวเลือกสุดท้าย

  2. อย่างไรก็ตามหากฉันพัฒนาคลาสที่ไม่ได้ใช้ทรัพยากรที่ไม่มีการจัดการไม่ว่าทางตรงหรือทางอ้อมฉันควรใช้IDisposableเพื่ออนุญาตให้ลูกค้าของคลาสนั้นใช้ 'การใช้คำสั่ง' หรือไม่

    เป็นไปได้ไหมที่จะใช้ IDisposable เพียงเพื่อให้ลูกค้าในชั้นเรียนของคุณใช้คำสั่ง use

    using(myClass objClass = new myClass())
    {
        // Do stuff here
    }
    
  3. ฉันได้พัฒนาโค้ดง่าย ๆ ด้านล่างนี้เพื่อสาธิตการใช้งานสุดท้าย / กำจัดทิ้ง:

    public class NoGateway : IDisposable
    {
        private WebClient wc = null;
    
        public NoGateway()
        {
            wc = new WebClient();
            wc.DownloadStringCompleted += wc_DownloadStringCompleted;
        }
    
    
        // Start the Async call to find if NoGateway is true or false
        public void NoGatewayStatus()
        {
            // Start the Async's download
                // Do other work here
            wc.DownloadStringAsync(new Uri(www.xxxx.xxx));
        }
    
        private void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            // Do work here
        }
    
        // Dispose of the NoGateway object
        public void Dispose()
        {
            wc.DownloadStringCompleted -= wc_DownloadStringCompleted;
            wc.Dispose();
            GC.SuppressFinalize(this);
        }
    }
    

คำถามเกี่ยวกับซอร์สโค้ด:

  1. ที่นี่ฉันยังไม่ได้เพิ่ม finalizer และโดยปกติแล้ว finalizer จะถูกเรียกโดย GC และ Finalizer จะเรียก Dispose เนื่องจากฉันไม่มีตัวปรับขั้นสุดท้ายฉันจะเรียกวิธีการทิ้งเมื่อใด เป็นลูกค้าของคลาสที่ต้องเรียกมันหรือไม่?

    ดังนั้นคลาสของฉันในตัวอย่างนี้เรียกว่า NoGateway และลูกค้าสามารถใช้และกำจัดคลาสแบบนี้ได้:

    using(NoGateway objNoGateway = new NoGateway())
    {
        // Do stuff here   
    }
    

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

    NoGateway objNoGateway = new NoGateway();
    // Do stuff with object
    objNoGateway.Dispose(); // finished with it
    
  2. ฉันกำลังใช้WebClientชั้นเรียนในNoGatewayชั้นเรียนของฉัน เนื่องจากWebClientใช้IDisposableอินเทอร์เฟซนี่หมายความว่าWebClientใช้ทรัพยากรที่ไม่มีการจัดการทางอ้อมหรือไม่ มีกฎที่ยากและรวดเร็วในการติดตามหรือไม่ ฉันจะรู้ได้อย่างไรว่าคลาสใช้ทรัพยากรที่ไม่มีการจัดการ


1
รูปแบบการออกแบบที่ซับซ้อนนี้จำเป็นต้องใช้เพื่อแก้ปัญหาการลดปริมาณทรัพยากรหรือไม่
zinking

คำตอบ:


422

รูปแบบ IDisposable แนะนำคือที่นี่ เมื่อเขียนโปรแกรมคลาสที่ใช้ IDisposable โดยทั่วไปคุณควรใช้สองรูปแบบ:

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

public sealed class A : IDisposable
{
    public void Dispose()
    {
        // get rid of managed resources, call Dispose on member variables...
    }
}

เมื่อใช้คลาสที่ไม่ได้ปิดผนึกให้ทำดังนี้:

public class B : IDisposable
{    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }   
        // get rid of unmanaged resources
    }

    // only if you use unmanaged resources directly in B
    //~B()
    //{
    //    Dispose(false);
    //}
}

ขอให้สังเกตว่าผมยังไม่ได้ประกาศให้เป็น finalizer ในB; คุณควรใช้เครื่องมือปรับขั้นสุดท้ายหากคุณมีทรัพยากรที่ไม่มีการจัดการที่แท้จริงในการกำจัด CLR เกี่ยวข้องกับวัตถุที่สามารถสรุปได้แตกต่างจากวัตถุที่ไม่สามารถสรุปได้แม้ว่าSuppressFinalizeจะถูกเรียก

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

public class C : B
{
    private IntPtr m_Handle;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // get rid of managed resources
        }
        ReleaseHandle(m_Handle);

        base.Dispose(disposing);
    }

    ~C() {
        Dispose(false);
    }
}

หากคุณไม่ได้ใช้ทรัพยากรที่ไม่มีการจัดการโดยตรง ( SafeHandleและเพื่อนจะไม่นับตามที่พวกเขาประกาศผู้เข้ารอบสุดท้ายของตัวเอง) จากนั้นอย่าใช้เครื่องมือตัดปลายรอบเนื่องจาก GC เกี่ยวข้องกับคลาสที่สรุปได้แตกต่างกันแม้ว่าคุณจะระงับ Finalizer ในภายหลัง โปรดทราบด้วยว่าแม้ว่าBจะไม่มี Finalizer แต่ก็ยังคงเรียกร้องSuppressFinalizeให้จัดการกับคลาสย่อยใด ๆ ที่ใช้ finalizer ได้อย่างถูกต้อง

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


26
ฉันเห็นด้วยกับค็อป โปรดทราบว่าคุณไม่จำเป็นต้องมีเครื่องมือปรับขั้นสุดท้ายหากคุณจัดการกับทรัพยากรที่มีการจัดการเท่านั้น (อันที่จริงแล้วคุณไม่ควรพยายามเข้าถึงวัตถุที่ได้รับการจัดการจากภายในเครื่องสุดท้ายของคุณ (นอกเหนือจาก "นี่") เนื่องจากไม่มีคำสั่งรับประกัน GC จะทำความสะอาดวัตถุ. นอกจากนี้ถ้าคุณกำลังใช้ NET 2.0 หรือดีกว่าที่คุณสามารถ (และควร) SafeHandles ใช้กับ wrapper จับ unmanaged. Safehandles ช่วยลดความต้องการของคุณเขียน finalizers สำหรับการจัดการเรียนของคุณที่ทั้งหมด. blogs.msdn com / bclteam / archive / 2005/03/16 / 396900.aspx
JMarsch

5
ฉันคิดว่าเป็นการดีที่จะโทรหา MessageBox.Show ("Error" + GetType (). ชื่อ + "not disosed") ใน finalizer เนื่องจากวัตถุที่ใช้แล้วทิ้งควรถูกกำจัดอยู่เสมอและหากคุณไม่ทำสิ่งนี้ ดีที่สุดที่จะได้รับการแจ้งเตือนถึงความจริงโดยเร็วที่สุด
erikkallen

95
@erikkallen เป็นเรื่องตลกหรือเปล่า? :)
Alex Norcliffe

2
เนื่องจากต้องใช้ความพยายามในการคำนวณเพิ่มเติมใน CLR เพื่อติดตามคลาสที่มี finalizers ที่ใช้งานอยู่ - การใช้งาน finalizer ทำให้สิ่งนี้เกิดขึ้น การเรียก GC.SuppressFinalize หมายความว่า Finalizer ไม่ควรถูกเรียกใช้โดยรันไทม์ มันยังคงเป็น Gen2 โดยไม่คำนึงถึง อย่าเพิ่มตัวเลือกสุดท้ายหากคุณไม่ได้จัดการกับทรัพยากรที่มีการจัดการ ตัวดัดแปลงคลาสที่ปิดผนึกหรือที่ปิดผนึกนั้นไม่เกี่ยวข้องกับจุดนั้น
Ritch Melton

3
@ ริทช์: การอ้างอิง? นั่นไม่จำเป็นต้องเป็นสิ่งที่เลวร้าย หากคุณกำลังใช้งานIDisposableโอกาสที่มันจะถูกนำไปใช้เป็นระยะเวลาหนึ่ง คุณกำลังบันทึก CLR โดยไม่ต้องคัดลอกจาก Gen0 -> Gen1 -> Gen2
thecoop

123

รูปแบบที่เป็นทางการที่จะใช้IDisposableนั้นยากที่จะเข้าใจ ฉันเชื่อว่าอันนี้ดีกว่า :

public class BetterDisposableClass : IDisposable {

  public void Dispose() {
    CleanUpManagedResources();
    CleanUpNativeResources();
    GC.SuppressFinalize(this);
  }

  protected virtual void CleanUpManagedResources() { 
    // ...
  }
  protected virtual void CleanUpNativeResources() {
    // ...
  }

  ~BetterDisposableClass() {
    CleanUpNativeResources();
  }

}

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

public class NativeDisposable : IDisposable {

  public void Dispose() {
    CleanUpNativeResource();
    GC.SuppressFinalize(this);
  }

  protected virtual void CleanUpNativeResource() {
    // ...
  }

  ~NativeDisposable() {
    CleanUpNativeResource();
  }

}

ด้วยSafeHandleและอนุพันธ์ชั้นเรียนเหล่านี้ควรจะหายากมาก

ผลการเรียนที่ใช้แล้วทิ้งที่ไม่ได้จัดการโดยตรงกับทรัพยากรที่ไม่มีการจัดการแม้ในที่ที่เป็นมรดกที่มีประสิทธิภาพ: พวกเขาไม่จำเป็นต้องกังวลกับทรัพยากรที่ไม่มีการจัดการอีกต่อไป พวกเขาจะง่ายต่อการใช้และเข้าใจ:

public class ManagedDisposable : IDisposable {

  public virtual void Dispose() {
    // dispose of managed resources
  }

}

@ ไคล์: ขอบคุณ! ผมชอบมันมากเกินไป :-) มีตามขึ้นไปมันนี่
Jordão

4
แม้ว่าสิ่งหนึ่งที่ฉันต้องการทราบคือมันไม่ได้ป้องกันไม่ให้ถูกเรียกเป็นครั้งที่สอง
HuseyinUslu

5
@HuseyinUslu: นี่เป็นเพียงสาระสำคัญของรูปแบบ คุณสามารถเพิ่มการdisposedตั้งค่าสถานะและตรวจสอบได้อย่างแน่นอน
Jordão

2
@didibus: มันเป็นเรื่องง่าย ๆ ในการเพิ่มdisposedธงตรวจสอบก่อนทิ้งและตั้งค่าหลังจากการกำจัด ดูที่นี่สำหรับแนวคิด คุณควรตรวจสอบการตั้งค่าสถานะก่อนวิธีการใด ๆ ของชั้นเรียน มีเหตุผล? มันซับซ้อนหรือไม่
Jordão

1
+1 สำหรับ"วิธีการแก้ปัญหาที่ดียิ่งขึ้นคือการมีกฎที่คุณมักจะมีการสร้างชั้นเสื้อคลุมสำหรับทรัพยากรที่ไม่มีการจัดการใด ๆ ที่คุณต้องจัดการ" ฉันสะดุดในส่วนเสริมนี้สำหรับ VLC และฉันใช้มันตั้งแต่นั้นมา บันทึกอาการปวดหัวมากมาย ...
Franz B.

37

โปรดทราบว่าการใช้งาน IDisposable ควรทำตามรูปแบบด้านล่าง (IMHO) ฉันพัฒนารูปแบบนี้โดยยึดตามข้อมูลจาก. NET "gods" แนวทางการออกแบบ. NET Framework ที่ยอดเยี่ยมมากมาย(โปรดทราบว่า MSDN ไม่ปฏิบัติตามสิ่งนี้ด้วยเหตุผลบางประการ!) . NET Framework Design Guidelines ถูกเขียนโดย Krzysztof Cwalina (สถาปนิก CLR ในเวลานั้น) และ Brad Abrams (ฉันเชื่อว่า CLR Program Manager ในเวลานั้น) และ Bill Wagner ([Effective C #] และ [More Effective C #] (เพียงใช้เวลา มองหาสิ่งเหล่านี้ใน Amazon.com:

โปรดทราบว่าคุณไม่ควรใช้ Finalizer เว้นแต่ว่าคลาสของคุณจะมีทรัพยากรที่ไม่ได้รับการจัดการโดยตรง เมื่อคุณใช้งาน Finalizer ในชั้นเรียนแม้ว่าจะไม่เคยมีใครเรียกมันก็รับประกันได้ว่าจะมีชีวิตอยู่สำหรับคอลเลกชันพิเศษ มันถูกวางไว้โดยอัตโนมัติใน Finalization Queue (ซึ่งรันบนเธรดเดี่ยว) นอกจากนี้บันทึกย่อที่สำคัญอย่างหนึ่ง ... รหัสทั้งหมดที่ถูกเรียกใช้ภายใน Finalizer (คุณต้องดำเนินการอย่างใดอย่างหนึ่ง) จะต้องเป็น thread-safe และ exception-safe! สิ่งที่ไม่ดีจะเกิดขึ้นเป็นอย่างอื่น ... (เช่นพฤติกรรมที่บึกบึนและในกรณีของข้อยกเว้นแอปพลิเคชันที่ไม่สามารถกู้คืนได้ที่ร้ายแรงถึงตาย)

รูปแบบที่ฉันได้รวบรวม (และเขียนข้อมูลโค้ดไว้) ดังต่อไปนี้:

#region IDisposable implementation

//TODO remember to make this class inherit from IDisposable -> $className$ : IDisposable

// Default initialization for a bool is 'false'
private bool IsDisposed { get; set; }

/// <summary>
/// Implementation of Dispose according to .NET Framework Design Guidelines.
/// </summary>
/// <remarks>Do not make this method virtual.
/// A derived class should not be able to override this method.
/// </remarks>
public void Dispose()
{
    Dispose( true );

    // This object will be cleaned up by the Dispose method.
    // Therefore, you should call GC.SupressFinalize to
    // take this object off the finalization queue 
    // and prevent finalization code for this object
    // from executing a second time.

    // Always use SuppressFinalize() in case a subclass
    // of this type implements a finalizer.
    GC.SuppressFinalize( this );
}

/// <summary>
/// Overloaded Implementation of Dispose.
/// </summary>
/// <param name="isDisposing"></param>
/// <remarks>
/// <para><list type="bulleted">Dispose(bool isDisposing) executes in two distinct scenarios.
/// <item>If <paramref name="isDisposing"/> equals true, the method has been called directly
/// or indirectly by a user's code. Managed and unmanaged resources
/// can be disposed.</item>
/// <item>If <paramref name="isDisposing"/> equals false, the method has been called by the 
/// runtime from inside the finalizer and you should not reference 
/// other objects. Only unmanaged resources can be disposed.</item></list></para>
/// </remarks>
protected virtual void Dispose( bool isDisposing )
{
    // TODO If you need thread safety, use a lock around these 
    // operations, as well as in your methods that use the resource.
    try
    {
        if( !this.IsDisposed )
        {
            if( isDisposing )
            {
                // TODO Release all managed resources here

                $end$
            }

            // TODO Release all unmanaged resources here



            // TODO explicitly set root references to null to expressly tell the GarbageCollector
            // that the resources have been disposed of and its ok to release the memory allocated for them.


        }
    }
    finally
    {
        // explicitly call the base class Dispose implementation
        base.Dispose( isDisposing );

        this.IsDisposed = true;
    }
}

//TODO Uncomment this code if this class will contain members which are UNmanaged
// 
///// <summary>Finalizer for $className$</summary>
///// <remarks>This finalizer will run only if the Dispose method does not get called.
///// It gives your base class the opportunity to finalize.
///// DO NOT provide finalizers in types derived from this class.
///// All code executed within a Finalizer MUST be thread-safe!</remarks>
//  ~$className$()
//  {
//     Dispose( false );
//  }
#endregion IDisposable implementation

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

public DerivedClass : BaseClass, IDisposable (remove the IDisposable because it is inherited from BaseClass)


protected override void Dispose( bool isDisposing )
{
    try
    {
        if ( !this.IsDisposed )
        {
            if ( isDisposing )
            {
                // Release all managed resources here

            }
        }
    }
    finally
    {
        // explicitly call the base class Dispose implementation
        base.Dispose( isDisposing );
    }
}

ฉันโพสต์การนำไปใช้นี้ในบล็อกของฉันที่: วิธีการใช้รูปแบบการกำจัดอย่างเหมาะสม


คนที่สามารถยังเพิ่มรูปแบบสำหรับการเรียนมา (มาจากชั้นฐานนี้)
akjoshi

3
@akjoshi - ฉันได้อัปเดตรูปแบบด้านบนเพื่อรวมรหัสสำหรับคลาสที่ทิ้งแล้ว นอกจากนี้โปรดทราบว่าไม่ควรใช้ Finalizer ในคลาสที่ได้รับ ...
เดฟแบล็ค

3
Microsoft ดูเหมือนจะชอบตั้งค่าสถานะ "จำหน่าย" ในตอนท้ายของวิธีการจำหน่าย แต่ดูเหมือนว่าฉันผิด การโทรซ้ำซ้อนเพื่อ "ทิ้ง" ควรจะไม่ทำอะไรเลย ในขณะที่ปกติจะไม่คาดหวังว่า Dispose จะถูกเรียกซ้ำสิ่งเหล่านี้อาจเกิดขึ้นได้หากมีใครพยายามที่จะกำจัดวัตถุที่ถูกทิ้งให้อยู่ในสถานะที่ไม่ถูกต้องโดยข้อยกเว้นที่เกิดขึ้นระหว่างการก่อสร้างหรือการดำเนินการอื่น ๆ ฉันคิดว่าการใช้เครื่องหมายInterlocked.Exchangeบนจำนวนเต็มIsDisposedในฟังก์ชัน wrapper ที่ไม่ใช่แบบเสมือนจะปลอดภัยกว่า
supercat

@DaveBlack: จะเกิดอะไรขึ้นถ้าคลาสพื้นฐานของคุณไม่ได้ใช้ทรัพยากรที่ไม่มีการจัดการ แต่คลาสที่ได้รับมาจะทำอย่างไร คุณต้องใช้งาน Finalizer ในคลาสที่ได้รับมาหรือไม่? และถ้าเป็นเช่นนั้นคุณจะรู้ได้อย่างไรว่าคลาสพื้นฐานนั้นยังไม่ได้ใช้งานหากคุณไม่มีสิทธิ์เข้าถึงแหล่งข้อมูล
Didier A.

@DaveBlack "ฉันพัฒนารูปแบบนี้ขึ้นอยู่กับข้อมูลจาก. NET" gods "" ถ้าหนึ่งในเทพคือ Jon Skeet ฉันจะทำตามคำแนะนำของคุณ
Elisabeth

23

ฉันเห็นด้วยกับ pm100 (และควรพูดอย่างนี้ในโพสต์ก่อนหน้าของฉัน)

คุณไม่ควรใช้ IDisposable ในชั้นเรียนเว้นแต่คุณต้องการ โดยเฉพาะอย่างยิ่งมีประมาณ 5 ครั้งเมื่อคุณต้องการ / ควรใช้ IDisposable:

  1. คลาสของคุณมีอย่างชัดเจน (เช่นไม่ผ่านการสืบทอด) ทรัพยากรที่มีการจัดการใด ๆ ที่ใช้ IDisposable และควรล้างข้อมูลเมื่อคลาสของคุณไม่ได้ใช้อีกต่อไป ตัวอย่างเช่นถ้าคลาสของคุณมีอินสแตนซ์ของสตรีม, DbCommand, DataTable เป็นต้น

  2. คลาสของคุณมีทรัพยากรที่มีการจัดการอย่างชัดเจนซึ่งใช้วิธีการปิด () เช่น IDataReader, IDbConnection เป็นต้นโปรดทราบว่าบางคลาสเหล่านี้ใช้ IDisposable โดยมี Dispose () และวิธีปิด ()

  3. คลาสของคุณมีทรัพยากรที่ไม่มีการจัดการอย่างชัดเจน - เช่นวัตถุ COM, พอยน์เตอร์ (ใช่คุณสามารถใช้พอยน์เตอร์ใน C # ที่ได้รับการจัดการ แต่จะต้องประกาศในบล็อก 'ไม่ปลอดภัย' ฯลฯ ในกรณีของทรัพยากรที่ไม่มีการจัดการ เรียก System.Runtime.InteropServices.Marshal.ReleaseComObject () บน RCW แม้ว่า RCW นั้นในทางทฤษฎีแล้ว wrapper ที่มีการจัดการ

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

  5. ชั้นเรียนของคุณมีการรวมกันดังกล่าวข้างต้น ...

ทางเลือกที่แนะนำสำหรับการทำงานกับวัตถุ COM และต้องใช้ Marshal.ReleaseComObject () คือการใช้คลาส System.Runtime.InteropServices.SafeHandle

BCL (ทีมห้องสมุดชั้นฐาน) มีบล็อกโพสต์ที่ดีเกี่ยวกับที่นี่http://blogs.msdn.com/bclteam/archive/2005/03/16/396900.aspx

สิ่งสำคัญอย่างหนึ่งที่ควรทำคือถ้าคุณทำงานกับ WCF และล้างทรัพยากรคุณควรหลีกเลี่ยงบล็อก 'ใช้' อยู่เสมอ มีบล็อกโพสต์มากมายและใน MSDN เกี่ยวกับสาเหตุที่เป็นความคิดที่ไม่ดี ฉันได้โพสต์เกี่ยวกับที่นี่ด้วย - อย่าใช้ 'using ()' กับพร็อกซี WCF


3
ฉันเชื่อว่ามีกรณีที่ 5: หากชั้นเรียนของคุณสมัครรับกิจกรรมโดยใช้การอ้างอิงที่แข็งแกร่งคุณควรใช้ IDisposable และยกเลิกการลงทะเบียนตัวคุณเองจากเหตุการณ์ในวิธีการกำจัด
Didier A.

สวัสดี Didibus ใช่คุณถูกต้อง. ฉันลืมเรื่องนั้นไป ฉันได้แก้ไขคำตอบเพื่อรวมไว้เป็นกรณี ๆ ขอบคุณ
เดฟสีดำ

เอกสารประกอบ MSDN สำหรับรูปแบบการกำจัดเพิ่มกรณีอื่น: "พิจารณาการใช้รูปแบบการทิ้งพื้นฐานในชั้นเรียนที่ตัวเองไม่ได้มีการจัดการทรัพยากรหรือวัตถุที่ใช้แล้วทิ้ง แต่มีแนวโน้มที่จะมีชนิดย่อยที่ทำตัวอย่างที่ดีของนี่คือ System.IO .Stream class ถึงแม้ว่ามันจะเป็นคลาสพื้นฐานที่เป็นนามธรรมซึ่งไม่ได้มีทรัพยากรใด ๆ แต่คลาสย่อยส่วนใหญ่จะทำและด้วยเหตุนี้มันจึงใช้รูปแบบนี้ "
Gonen I

12

การใช้ lambdas แทน IDisposable

ฉันไม่เคยตื่นเต้นกับการใช้ไอเดีย / IDisposable ทั้งหมด ปัญหาคือต้องการผู้โทรไปที่:

  • รู้ว่าพวกเขาจะต้องใช้ IDisposable
  • จำไว้ว่าให้ใช้ 'ใช้'

วิธีการใหม่ที่ฉันชอบคือใช้วิธีการจากโรงงานและแลมบ์ดาแทน

ลองนึกภาพฉันต้องการทำบางสิ่งด้วย SqlConnection (สิ่งที่ควรห่อหุ้มด้วยการใช้) คลาสสิกที่คุณจะทำ

using (Var conn = Factory.MakeConnection())
{
     conn.Query(....);
}

วิธีการใหม่

Factory.DoWithConnection((conn)=>
{
    conn.Query(...);
}

ในกรณีแรกผู้เรียกไม่สามารถใช้ไวยากรณ์ที่ใช้ ในกรณีที่สองผู้ใช้ไม่มีทางเลือก ไม่มีวิธีที่สร้างวัตถุ SqlConnection ผู้เรียกต้องเรียกใช้ DoWithConnection

DoWithConnection มีลักษณะเช่นนี้

void DoWithConnection(Action<SqlConnection> action)
{
   using (var conn = MakeConnection())
   {
       action(conn);
   }
}

MakeConnection เป็นส่วนตัวแล้ว


2
การห่อสิ่งต่าง ๆ ใน lambdas อาจเป็นวิธีที่ดี แต่มีข้อ จำกัด มันไม่ได้เลวร้ายเกินไปสำหรับสถานการณ์ที่ในความเป็นจริงผู้บริโภคทุกคนในชั้นเรียนจะใช้บล็อก "ใช้" แต่มันจะไม่อนุญาตให้สถานการณ์ที่วิธีการจะเก็บ IDisposable ในเขตเรียน (ทั้งโดยตรงหรือในบางอย่างเช่น iterator )
supercat

@supercat คุณสามารถยืนยันได้ว่าการไม่อนุญาตให้เก็บข้อมูลการใช้ทรัพยากร hogging เป็นสิ่งที่ดี รูปแบบการยืมที่ฉันเสนอที่นี่บังคับให้คุณต้องพึ่งพาการใช้ทรัพยากรของคุณ
pm100

มันอาจเป็นสิ่งที่ดี แต่ก็สามารถทำให้การดำเนินงานที่สมเหตุสมผลบางอย่างยากลำบากมาก ตัวอย่างเช่นสมมติว่าชนิดตัวอ่านฐานข้อมูลแทนที่จะใช้ IEnumerable <T> จะแสดงวิธีการDoForAll(Action<T>) where T:IComparable<T>เรียกผู้รับมอบสิทธิ์ที่ระบุในแต่ละระเบียน เมื่อได้รับวัตถุสองอย่างซึ่งทั้งสองอย่างนี้จะส่งคืนข้อมูลตามลำดับที่เรียงลำดับแล้วหนึ่งจะส่งออกรายการทั้งหมดที่มีอยู่ในคอลเลกชันหนึ่ง แต่ไม่ได้อื่น ๆ ได้อย่างไร ถ้าประเภทดำเนินการIEnumerable<T>อย่างใดอย่างหนึ่งจะทำการดำเนินการผสาน DoForAllแต่ที่จะไม่ทำงานกับ
supercat

วิธีเดียวที่ฉันสามารถรวมสองDoForAllคอลเลกชันโดยไม่ต้องคัดลอกครั้งแรกอย่างครบถ้วนลงในโครงสร้างอื่น ๆ จะใช้สองหัวข้อซึ่งจะค่อนข้าง hoggish ทรัพยากรมากกว่าเพียงใช้คู่ IEnumerable และระมัดระวัง เพื่อปล่อยพวกเขา
supercat

-1: คำตอบที่ดีสำหรับคำถามที่ไม่ได้ถาม นี่จะเป็นคำตอบที่ดีสำหรับ "ฉันจะทำให้การใช้วัตถุ IDisposable ง่ายขึ้นได้อย่างไร"
John Saunders

10

ไม่มีใครตอบคำถามเกี่ยวกับว่าคุณควรใช้ IDisposable หรือไม่แม้ว่าคุณไม่ต้องการใช้ก็ตาม

คำตอบสั้น ๆ : ไม่

คำตอบยาว:

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

  • มัน obviuos พวกเขาจากประสบการณ์ (ตัวอย่างซ็อกเก็ตชั้น)
  • เอกสารของมัน
  • พวกเขามีความระมัดระวังและสามารถเห็นได้ว่าคลาสใช้ IDisposable

ดังนั้นโดยการใช้ IDisposable คุณกำลังบอก devs (อย่างน้อยบางคน) ว่าคลาสนี้ล้อมรอบบางสิ่งที่ต้องถูกปล่อยออกมา พวกเขาจะใช้ 'ใช้' - แต่มีกรณีอื่น ๆ ที่ไม่สามารถใช้งานได้ (ขอบเขตของวัตถุไม่ได้อยู่ในเครื่อง); และพวกเขาจะต้องเริ่มกังวลเกี่ยวกับอายุการใช้งานของวัตถุในกรณีอื่น ๆ - ฉันคงจะกังวลอย่างแน่นอน แต่นี่ไม่จำเป็น

คุณใช้ Idisposable เพื่อให้พวกเขาใช้การใช้ แต่พวกเขาจะไม่ใช้การใช้จนกว่าคุณจะบอกพวกเขา

ดังนั้นอย่าทำ


1
ฉันไม่เข้าใจว่าทำไม dev ไม่ใช้การใช้ / กำจัดบนวัตถุที่ใช้ IDisposable (ยกเว้นว่าโปรแกรมกำลังจะออกจากระบบ)
adrianm

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

3
@ pm100 Re: การใช้ IDisposable โดยไม่จำเป็น - มีบทความโดยละเอียดที่codeproject.com/KB/dotnet/idisposable.aspxซึ่งกล่าวถึงกรณีที่หายากบางอย่างเมื่อคุณอาจต้องการคิดเกี่ยวกับเรื่องนี้ (หายากมากฉันแน่ใจ) กล่าวโดยย่อ: หากคุณสามารถคาดการณ์ความต้องการ IDisposable ในอนาคตหรือในวัตถุที่ได้รับคุณอาจคิดว่าการใช้ IDisposable เป็น "no-op" ในคลาสพื้นฐานของคุณเพื่อหลีกเลี่ยงปัญหา "การแบ่ง" ที่วัตถุบางอย่างต้องการ การกำจัดและอื่น ๆ ไม่ได้
Kevin P. Rice

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

  2. ถ้าคลาสของคุณไม่ได้ใช้ทรัพยากรที่หายากฉันไม่สามารถดูได้ว่าทำไมคุณถึงทำให้คลาสของคุณใช้ IDisposable คุณควรทำเช่นนั้นหากคุณ:

    • รู้ว่าคุณจะมีทรัพยากรที่หายากในวัตถุของคุณในไม่ช้าไม่ใช่ตอนนี้ (และฉันหมายความว่าเหมือนใน "เรายังคงพัฒนามันจะมาถึงก่อนที่เราจะทำ" ไม่ใช่ใน "ฉันคิดว่าเราต้องการสิ่งนี้ ")
    • การใช้ทรัพยากรที่หายาก
  3. ใช่รหัสที่ใช้รหัสของคุณต้องเรียกใช้วิธีการกำจัดวัตถุของคุณ และใช่รหัสที่ใช้วัตถุของคุณสามารถใช้usingตามที่คุณแสดง

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


4

รูปแบบการกำจัด:

public abstract class DisposableObject : IDisposable
{
    public bool Disposed { get; private set;}      

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~DisposableObject()
    {
        Dispose(false);
    }

    private void Dispose(bool disposing)
    {
        if (!Disposed)
        {
            if (disposing)
            {
                DisposeManagedResources();
            }

            DisposeUnmanagedResources();
            Disposed = true;
        }
    }

    protected virtual void DisposeManagedResources() { }
    protected virtual void DisposeUnmanagedResources() { }
}

ตัวอย่างของการสืบทอด:

public class A : DisposableObject
{
    public Component components_a { get; set; }
    private IntPtr handle_a;

    protected override void DisposeManagedResources()
    {
        try
        {
          Console.WriteLine("A_DisposeManagedResources");
          components_a.Dispose();
          components_a = null;
        }
        finally
        { 
          base.DisposeManagedResources();
        }
    }

    protected override void DisposeUnmanagedResources()
    {
        try
        {
          Console.WriteLine("A_DisposeUnmanagedResources");
          CloseHandle(handle_a);
          handle_a = IntPtr.Zero;
        }
        finally
        { 
          base.DisposeUnmanagedResources();
        }
    }
}

public class B : A
{
    public Component components_b { get; set; }
    private IntPtr handle_b;

    protected override void DisposeManagedResources()
    {
        try
        {
          Console.WriteLine("B_DisposeManagedResources");
          components_b.Dispose();
          components_b = null;
        }
        finally
        { 
          base.DisposeManagedResources();
        }
    }

    protected override void DisposeUnmanagedResources()
    {
        try
        {
          Console.WriteLine("B_DisposeUnmanagedResources");
          CloseHandle(handle_b);
          handle_b = IntPtr.Zero;
        }
        finally
        { 
          base.DisposeUnmanagedResources();
        }
    }
}

4

บางแง่มุมของคำตอบอื่นไม่ถูกต้องเล็กน้อยด้วยเหตุผล 2 ประการ:

ครั้งแรก

using(NoGateway objNoGateway = new NoGateway())

จริงๆแล้วเทียบเท่ากับ:

try
{
    NoGateway = new NoGateway();
}
finally
{
    if(NoGateway != null)
    {
        NoGateway.Dispose();
    }
}

สิ่งนี้อาจฟังดูไร้สาระเนื่องจากตัวดำเนินการ 'ใหม่' ไม่ควรส่งคืน 'ว่าง' เว้นแต่คุณมีข้อยกเว้น OutOfMemory แต่ให้พิจารณากรณีต่อไปนี้: 1. คุณเรียก FactoryClass ที่ส่งคืนทรัพยากร IDisposable หรือ 2 ถ้าคุณมีประเภทที่อาจหรือไม่สืบทอดจาก IDisposable ขึ้นอยู่กับการใช้งานของ - จำไว้ว่าฉันเคยเห็นรูปแบบ IDisposable ดำเนินการอย่างไม่ถูกต้อง จำนวนครั้งที่ไคลเอนต์จำนวนมากที่นักพัฒนาเพิ่งเพิ่มเมธอด Dispose () โดยไม่ต้องสืบทอดจาก IDisposable (แย่, แย่, แย่) คุณอาจมีกรณีของทรัพยากร IDisposable ถูกส่งกลับจากคุณสมบัติหรือวิธีการ (อีกครั้งเลวร้ายเลวเลว - อย่า 'แจกทรัพยากร IDisposable ของคุณ)

using(IDisposable objNoGateway = new NoGateway() as IDisposable)
{
    if (NoGateway != null)
    {
        ...

หากตัวดำเนินการ 'as' ส่งคืน null (หรือคุณสมบัติหรือวิธีการส่งคืนทรัพยากร) และรหัสของคุณในบล็อก 'using' ป้องกันการ 'null' รหัสของคุณจะไม่ระเบิดเมื่อพยายามเรียก Dispose บนวัตถุ null เนื่องจาก การตรวจสอบ null ในตัว

เหตุผลที่สองการตอบกลับของคุณไม่ถูกต้องเป็นเพราะสาเหตุต่อไปนี้:

Finalizer ถูกเรียกใช้เมื่อ GC ทำลายวัตถุของคุณ

ขั้นแรกการสรุป (เช่นเดียวกับ GC เอง) นั้นไม่สามารถกำหนดได้ CLR นี้จะกำหนดว่าจะเรียก finalizer เมื่อใด เช่นนักพัฒนา / รหัสไม่มีความคิด หากรูปแบบ IDisposable ถูกนำไปใช้อย่างถูกต้อง (ตามที่ฉันโพสต์ด้านบน) และ GC.SuppressFinalize () ได้รับการเรียก Finalizer จะไม่ถูกเรียก นี่คือหนึ่งในเหตุผลสำคัญที่ทำให้รูปแบบถูกต้อง เนื่องจากมีเธรด Finalizer เพียง 1 เธรดต่อกระบวนการที่ได้รับการจัดการโดยไม่คำนึงถึงจำนวนของตัวประมวลผลเชิงตรรกะคุณสามารถลดประสิทธิภาพได้อย่างง่ายดายโดยการสำรองข้อมูลหรือแม้แต่แขวนเธรด Finalizer ด้วยการลืมเรียก GC.SuppressFinalize ()

ฉันโพสต์รูปแบบการจัดการที่ถูกต้องในบล็อกของฉัน: วิธีการใช้รูปแบบการทิ้งอย่างเหมาะสม


2
คุณแน่ใจเกี่ยวกับการเขียนNoGateway = new NoGateway();และNoGateway != null?
Cœur

1
สิ่งนี้อ้างถึงstackoverflow.com/a/898856/3195477หรือไม่ ไม่มีคำตอบตอนนี้โพสต์โดยชื่อ 'Icey'
UuDdLrLrSs

@DaveInCaz ดูเหมือนว่าถูกต้อง ฉันไม่เห็น 'Icey' ที่ใดก็ได้ แต่บริบทของการตอบสนองของฉันดูเหมือนจะถูกนำไปตอบที่ลิงก์ของคุณด้านบน บางทีเขาอาจเปลี่ยนชื่อผู้ใช้ของเขา?
เดฟสีดำ

@DaveBlack เจ๋งขอบคุณ ฉันเพิ่งแก้ไขลงในข้อความ
UuDdLrLrSs

2

1) WebClient เป็นประเภทที่ได้รับการจัดการดังนั้นคุณไม่จำเป็นต้องมีเครื่องมือปรับแต่งขั้นสุดท้าย จำเป็นต้องใช้ตัวเลือกขั้นสุดท้ายในกรณีที่ผู้ใช้ของคุณไม่ได้กำจัด () ของคลาส NoGateway ของคุณและประเภทเนทีฟ (ซึ่ง GC ไม่ได้รวบรวมไว้) จะต้องกำจัดให้หมด ในกรณีนี้หากผู้ใช้ไม่ได้เรียก Dispose () WebClient ที่มีอยู่จะถูก GC ทิ้งทันทีหลังจาก NoGateway ทำ

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


2

รูปแบบจาก msdn

public class BaseResource: IDisposable
{
   private IntPtr handle;
   private Component Components;
   private bool disposed = false;
   public BaseResource()
   {
   }
   public void Dispose()
   {
      Dispose(true);      
      GC.SuppressFinalize(this);
   }
   protected virtual void Dispose(bool disposing)
   {
      if(!this.disposed)
      {        
         if(disposing)
         {
            Components.Dispose();
         }         
         CloseHandle(handle);
         handle = IntPtr.Zero;
       }
      disposed = true;         
   }
   ~BaseResource()      
   {      Dispose(false);
   }
   public void DoSomething()
   {
      if(this.disposed)
      {
         throw new ObjectDisposedException();
      }
   }
}
public class MyResourceWrapper: BaseResource
{
   private ManagedResource addedManaged;
   private NativeResource addedNative;
   private bool disposed = false;
   public MyResourceWrapper()
   {
   }
   protected override void Dispose(bool disposing)
   {
      if(!this.disposed)
      {
         try
         {
            if(disposing)
            {             
               addedManaged.Dispose();         
            }
            CloseHandle(addedNative);
            this.disposed = true;
         }
         finally
         {
            base.Dispose(disposing);
         }
      }
   }
}

1
using(NoGateway objNoGateway = new NoGateway())

เทียบเท่ากับ

try
{
    NoGateway = new NoGateway();
}

finally
{
    NoGateway.Dispose();
}

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


1
Finalizer ไม่ได้ถูกเรียกเมื่อ GC ทำลายวัตถุ หาก "จบ" ถูกแทนที่แล้วเมื่อ GC จะทำลายวัตถุนั้นจะถูกวางไว้ในคิวของวัตถุที่ต้องการการสรุปเพื่อสร้างการอ้างอิงที่แข็งแกร่งชั่วคราวและ - อย่างน้อยก็ชั่วคราว - "คืนชีพ" มัน
supercat

-5

จากสิ่งที่ฉันรู้ขอแนะนำไม่ให้ใช้ Finalizer / Destructor:

public ~MyClass() {
  //dont use this
}

ส่วนใหญ่เป็นเพราะไม่รู้ว่ามันจะถูกเรียกเมื่อไหร่หรือถ้า วิธีการกำจัดดีกว่ามากโดยเฉพาะถ้าคุณใช้หรือกำจัดโดยตรง

ใช้เป็นสิ่งที่ดี ใช้มัน :)


2
คุณควรไปตามลิงค์ในคำตอบของค็อป ใช่การใช้ / การกำจัดดีกว่า แต่คลาสแบบใช้ครั้งเดียวควรใช้ทั้งสองอย่างแน่นอน
Henk Holterman

ที่น่าสนใจคือเอกสารทั้งหมดที่ฉันได้อ่านจากไมโครซอฟท์ - เช่นแนวทางการออกแบบกรอบ - พูดว่าอย่าใช้ destructor ใช้ IDisposable เสมอ
Nic Wise

5
เพียงแค่แยกแยะความแตกต่างระหว่างการใช้คลาสและการเขียนคลาสการอ่านอีกครั้ง
Henk Holterman

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