Pola sekali pakai (Prinsip Desain Sekali Pakai) hal 1


Pola sekali pakai (Prinsip Desain Sekali Pakai)


Saya kira hampir semua programmer yang menggunakan .NET sekarang akan mengatakan pola ini adalah sepotong kue. Itu adalah pola paling terkenal yang digunakan pada platform. Namun, bahkan domain masalah yang paling sederhana dan terkenal akan memiliki area rahasia yang belum pernah Anda lihat. Jadi, mari kita gambarkan semuanya dari awal untuk pemula dan sisanya (sehingga Anda masing-masing dapat mengingat dasar-dasarnya). Jangan lewati paragraf ini - saya melihat Anda!


Jika saya bertanya apa IDisposable, Anda pasti akan mengatakan itu


public interface IDisposable { void Dispose(); } 

Apa tujuan dari antarmuka? Maksud saya, mengapa kita perlu menghapus memori sama sekali jika kita memiliki Pengumpul Sampah yang cerdas yang membersihkan memori alih-alih kita, jadi kita bahkan tidak perlu memikirkannya. Namun, ada beberapa detail kecil.


Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.

Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu github / sidristij / dotnetbook .

Ada kesalahpahaman bahwa IDisposable berfungsi untuk melepaskan sumber daya yang tidak dikelola. Ini hanya sebagian benar dan untuk memahaminya, Anda hanya perlu mengingat contoh sumber daya yang tidak dikelola. Apakah kelas File sumber daya yang tidak dikelola? Tidak. Mungkin DbContext adalah sumber daya yang tidak dikelola? Tidak lagi Sumber daya yang tidak dikelola adalah sesuatu yang bukan milik sistem tipe .NET. Sesuatu yang tidak diciptakan platform, sesuatu yang ada di luar jangkauannya. Contoh sederhana adalah pegangan file yang dibuka di sistem operasi. Pegangan adalah angka yang secara unik mengidentifikasi file yang dibuka - tidak, bukan oleh Anda - oleh sistem operasi. Yaitu, semua struktur kontrol (mis. Posisi file dalam sistem file, fragmen file dalam kasus fragmentasi dan informasi layanan lainnya, jumlah silinder, kepala atau sektor HDD) berada di dalam OS tetapi tidak Platform .NET. Satu-satunya sumber daya yang tidak dikelola yang diteruskan ke platform .NET adalah nomor IntPtr. Nomor ini dibungkus oleh FileSafeHandle, yang pada gilirannya dibungkus oleh kelas File. Ini berarti kelas File bukan sumber daya yang tidak dikelola sendiri, tetapi menggunakan lapisan tambahan dalam bentuk IntPtr untuk memasukkan sumber daya yang tidak dikelola - pegangan file yang dibuka. Bagaimana Anda membaca file itu? Menggunakan serangkaian metode di WinAPI atau Linux OS.


Sinkronisasi primitif dalam program multithreaded atau multiprosesor adalah contoh kedua dari sumber daya yang tidak dikelola. Di sini termasuk array data yang dilewatkan melalui P / Invoke dan juga mutex atau semaphore.


Perhatikan bahwa OS tidak hanya meneruskan pegangan sumber daya yang tidak dikelola ke suatu aplikasi. Ini juga menyimpan pegangan di tabel pegangan yang dibuka oleh proses. Dengan demikian, OS dapat menutup sumber daya dengan benar setelah penghentian aplikasi. Ini memastikan sumber daya akan tetap ditutup setelah Anda keluar dari aplikasi. Namun, waktu menjalankan aplikasi dapat berbeda yang dapat menyebabkan penguncian sumber daya yang lama.

Ok Sekarang kami membahas sumber daya yang tidak dikelola. Mengapa kita perlu menggunakan IDisposable dalam kasus ini? Karena .NET Framework tidak tahu apa yang terjadi di luar wilayahnya. Jika Anda membuka file menggunakan OS API, .NET tidak akan tahu apa-apa tentang itu. Jika Anda mengalokasikan rentang memori untuk kebutuhan Anda sendiri (misalnya menggunakan VirtualAlloc), .NET juga tidak akan tahu apa-apa. Jika tidak tahu, itu tidak akan melepaskan memori yang ditempati oleh panggilan VirtualAlloc. Atau, itu tidak akan menutup file yang dibuka langsung melalui panggilan OS API. Ini dapat menyebabkan konsekuensi yang berbeda dan tidak terduga. Anda bisa mendapatkan OutOfMemory jika Anda mengalokasikan terlalu banyak memori tanpa melepaskannya (misalnya hanya dengan menetapkan pointer ke nol). Atau, jika Anda membuka file pada berbagi file melalui OS tanpa menutupnya, Anda akan mengunci file pada file tersebut untuk waktu yang lama. Contoh berbagi file sangat baik karena kunci akan tetap berada di sisi IIS bahkan setelah Anda menutup koneksi dengan server. Anda tidak memiliki hak untuk melepaskan kunci dan Anda harus meminta administrator untuk melakukan iisreset atau untuk menutup sumber daya secara manual menggunakan perangkat lunak khusus.
Masalah ini pada server jauh dapat menjadi tugas yang rumit untuk dipecahkan.


Semua kasus ini memerlukan protokol universal dan akrab untuk interaksi antara sistem tipe dan programmer. Ini harus secara jelas mengidentifikasi jenis-jenis yang memerlukan penutupan paksa. Antarmuka IDisposable melayani tujuan ini dengan tepat. Fungsinya sebagai berikut: jika suatu tipe berisi implementasi dari antarmuka IDisposable, Anda harus memanggil Buang () setelah Anda selesai bekerja dengan instance dari jenis itu.


Jadi, ada dua cara standar untuk menyebutnya. Biasanya Anda membuat instance entitas untuk menggunakannya dengan cepat dalam satu metode atau dalam masa pakai instance entitas.


Cara pertama adalah membungkus instance using(...){ ... } . Ini berarti Anda menginstruksikan untuk menghancurkan objek setelah blok terkait penggunaan selesai, yaitu untuk memanggil Buang (). Cara kedua adalah menghancurkan objek, ketika masa pakainya berakhir, dengan referensi ke objek yang ingin kita lepaskan. Tapi .NET tidak memiliki apa-apa selain metode finalisasi yang menyiratkan penghancuran objek secara otomatis, kan? Namun, finalisasi sama sekali tidak cocok karena kita tidak tahu kapan akan dipanggil. Sementara itu, kita perlu merilis objek pada waktu tertentu, misalnya tepat setelah kita selesai bekerja dengan file yang dibuka. Itu sebabnya kita juga perlu menerapkan IDisposable dan memanggil Buang untuk melepaskan semua sumber daya yang kita miliki. Jadi, kami mengikuti protokol , dan itu sangat penting. Karena jika seseorang mengikutinya, semua peserta harus melakukan hal yang sama untuk menghindari masalah.


Berbagai cara untuk mengimplementasikan IDisposable


Mari kita lihat implementasi IDisposable dari yang sederhana hingga yang rumit. Yang pertama dan paling sederhana adalah menggunakan IDisposable karena:


 public class ResourceHolder : IDisposable { DisposableResource _anotherResource = new DisposableResource(); public void Dispose() { _anotherResource.Dispose(); } } 

Di sini, kami membuat instance sumber daya yang dirilis lebih lanjut oleh Buang (). Satu-satunya hal yang membuat implementasi ini tidak konsisten adalah Anda masih dapat bekerja dengan instance setelah dihancurkan oleh Dispose() :


 public class ResourceHolder : IDisposable { private DisposableResource _anotherResource = new DisposableResource(); private bool _disposed; public void Dispose() { if(_disposed) return; _anotherResource.Dispose(); _disposed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } } 

CheckDisposed () harus dipanggil sebagai ekspresi pertama di semua metode publik kelas. Struktur kelas ResourceHolder diperoleh terlihat bagus untuk menghancurkan sumber daya yang tidak dikelola, yaitu DisposableResource . Namun, struktur ini tidak cocok untuk sumber daya yang tidak dikelola yang dibungkus. Mari kita lihat contoh dengan sumber daya yang tidak dikelola.


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { CloseHandle(_handle); } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr hObject); } 

Apa perbedaan perilaku dua contoh terakhir? Yang pertama menggambarkan interaksi dua sumber daya yang dikelola. Ini berarti bahwa jika suatu program bekerja dengan benar, sumber daya akan tetap dirilis. Karena DisposableResource dikelola, .NET CLR mengetahuinya dan akan melepaskan memori darinya jika perilakunya salah. Perhatikan bahwa saya secara sadar tidak menganggap tipe DisposableResource apa yang merangkum. Mungkin ada jenis logika dan struktur. Ini dapat berisi sumber daya yang dikelola dan tidak dikelola. Ini seharusnya tidak menjadi perhatian kita sama sekali . Tidak ada yang meminta kami untuk mendekompilasi perpustakaan pihak ketiga setiap kali dan melihat apakah mereka menggunakan sumber daya yang dikelola atau tidak dikelola. Dan jika jenis kami menggunakan sumber daya yang tidak dikelola, kami tidak dapat tidak mengetahui hal ini. Kami melakukan ini di kelas FileWrapper . Jadi, apa yang terjadi dalam kasus ini? Jika kami menggunakan sumber daya yang tidak dikelola, kami memiliki dua skenario. Yang pertama adalah ketika semuanya OK dan Buang dipanggil. Yang kedua adalah ketika ada masalah dan Buang gagal.


Katakan langsung mengapa ini bisa salah:


  • Jika kami menggunakan using(obj) { ... } , pengecualian mungkin muncul di blok kode bagian dalam. Pengecualian ini ditangkap oleh blok finally , yang tidak bisa kita lihat (ini adalah sintaksis gula C #). Blokir ini memanggil Buang secara implisit. Namun, ada beberapa kasus ketika ini tidak terjadi. Misalnya, tidak catch atau finally menangkap StackOverflowException . Anda harus selalu mengingat ini. Karena jika beberapa utas menjadi rekursif dan StackOverflowException terjadi di beberapa titik, .NET akan melupakan sumber daya yang digunakan tetapi tidak dirilis. Tidak tahu cara melepaskan sumber daya yang tidak dikelola. Mereka akan tinggal di memori sampai OS melepaskannya, yaitu ketika Anda keluar dari sebuah program, atau bahkan beberapa saat setelah penghentian aplikasi.
  • Jika kita memanggil Buang () dari Buang lain (). Sekali lagi, kita mungkin gagal mencapai itu. Ini bukan kasus pengembang aplikasi yang linglung, yang lupa memanggil Buang (). Ini adalah pertanyaan tentang pengecualian. Namun, ini bukan hanya pengecualian yang menabrak utas aplikasi. Di sini kita berbicara tentang semua pengecualian yang akan mencegah algoritma memanggil Dispose () eksternal yang akan memanggil Dispose kami ().

Semua kasus ini akan menciptakan sumber daya yang tidak dikelola yang ditangguhkan. Itu karena Pengumpul Sampah tidak tahu itu harus mengumpulkan mereka. Yang dapat dilakukan pada pemeriksaan selanjutnya adalah menemukan bahwa referensi terakhir ke grafik objek dengan tipe FileWrapper kami hilang. Dalam hal ini, memori akan dialokasikan kembali untuk objek dengan referensi. Bagaimana kita bisa mencegahnya?


Kita harus mengimplementasikan finalizer dari suatu objek. The 'finalizer' dinamai seperti ini dengan sengaja. Ini bukan destruktor seperti yang terlihat karena cara yang mirip untuk memanggil finalizers di C # dan destruktor di C ++. Perbedaannya adalah bahwa finalizer akan dipanggil pula , bertentangan dengan destructor (dan juga Dispose() ). Seorang penyelesai disebut ketika Pengumpulan Sampah dimulai (sekarang sudah cukup untuk mengetahui hal ini, tetapi hal-hal sedikit lebih rumit). Ini digunakan untuk pelepasan sumber daya yang dijamin jika terjadi kesalahan . Kita harus menerapkan finalizer untuk mengeluarkan sumber daya yang tidak dikelola. Sekali lagi, karena finalizer dipanggil ketika GC dimulai, kita tidak tahu kapan ini terjadi secara umum.


Mari kembangkan kode kita:


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { InternalDispose(); GC.SuppressFinalize(this); } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

Kami meningkatkan contoh dengan pengetahuan tentang proses finalisasi dan mengamankan aplikasi terhadap kehilangan informasi sumber daya jika Buang () tidak dipanggil. Kami juga memanggil GC. SuppressFinalize untuk menonaktifkan finalisasi instance dari tipe jika Buang () berhasil dipanggil. Tidak perlu melepaskan sumber daya yang sama dua kali, kan? Dengan demikian, kami juga mengurangi antrian finalisasi dengan melepaskan wilayah kode acak yang kemungkinan akan berjalan dengan finalisasi secara paralel, beberapa waktu kemudian. Sekarang, mari kita tambahkan contoh lebih jauh.


 public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

Sekarang contoh kita dari tipe yang merangkum sumber daya yang tidak dikelola terlihat lengkap. Sayangnya, Dispose() kedua Dispose() sebenarnya merupakan standar platform dan kami mengizinkannya. Perhatikan bahwa orang sering mengizinkan panggilan kedua Dispose() untuk menghindari masalah dengan kode panggilan dan ini salah. Namun, pengguna perpustakaan Anda yang melihat dokumentasi MS mungkin tidak berpikiran seperti itu dan akan mengizinkan beberapa panggilan Buang (). Memanggil metode publik lainnya akan menghancurkan integritas suatu objek. Jika kita menghancurkan objek, kita tidak bisa bekerja dengannya lagi. Ini berarti kita harus memanggil CheckDisposed di awal setiap metode publik.


Namun, kode ini berisi masalah parah yang mencegahnya berfungsi seperti yang kita inginkan. Jika kita ingat cara kerja pengumpulan sampah, kita akan melihat satu fitur. Saat mengumpulkan sampah, GC terutama menyelesaikan semua yang diwarisi langsung dari Object . Selanjutnya berkaitan dengan objek yang mengimplementasikan CriticalFinalizerObject . Ini menjadi masalah karena kedua kelas yang kami desain mewarisi Object. Kami tidak tahu dalam urutan mana mereka akan datang ke "mil terakhir". Namun, objek tingkat yang lebih tinggi dapat menggunakan finalizer untuk menyelesaikan objek dengan sumber daya yang tidak dikelola. Meskipun, ini sepertinya bukan ide yang bagus. Urutan finalisasi akan sangat membantu di sini. Untuk mengaturnya, tipe tingkat bawah dengan sumber daya tidak terkelola yang dienkapsulasi harus diwarisi dari CriticalFinalizerObject .


Alasan kedua lebih mendalam. Bayangkan Anda berani menulis aplikasi yang tidak terlalu memusingkan memori. Ini mengalokasikan memori dalam jumlah besar, tanpa menguangkan dan kehalusan lainnya. Suatu hari aplikasi ini akan crash dengan OutOfMemoryException. Ketika itu terjadi, kode berjalan secara spesifik. Itu tidak dapat mengalokasikan apa pun, karena itu akan menyebabkan pengecualian berulang, bahkan jika yang pertama tertangkap. Ini tidak berarti kita tidak harus membuat instance objek baru. Bahkan panggilan metode sederhana dapat membuang pengecualian ini, misalnya finalisasi. Saya mengingatkan Anda bahwa metode dikompilasi ketika Anda memanggil mereka untuk pertama kalinya. Ini adalah perilaku yang biasa. Bagaimana kita bisa mencegah masalah ini? Cukup mudah. Jika objek Anda diwarisi dari CriticalFinalizerObject , maka semua metode jenis ini akan langsung dikompilasi setelah memuatnya dalam memori. Selain itu, jika Anda menandai metode dengan atribut [PrePrepareMethod] , mereka juga akan dikompilasi sebelumnya dan akan aman untuk memanggil dalam situasi sumber daya rendah.


Mengapa itu penting? Mengapa menghabiskan terlalu banyak upaya pada mereka yang meninggal? Karena sumber daya yang tidak dikelola dapat ditangguhkan dalam suatu sistem untuk waktu yang lama. Bahkan setelah Anda me-restart komputer. Jika pengguna membuka file dari berbagi file di aplikasi Anda, yang pertama akan dikunci oleh host jarak jauh dan dirilis pada batas waktu atau ketika Anda merilis sumber daya dengan menutup file. Jika aplikasi Anda macet ketika file dibuka, itu tidak akan dirilis bahkan setelah reboot. Anda harus menunggu lama sampai host jarak jauh melepaskannya. Selain itu, Anda tidak boleh mengizinkan pengecualian di finalizer. Ini menyebabkan crash CLR dan aplikasi yang dipercepat karena Anda tidak dapat membungkus panggilan finalizer dalam try ... catch . Maksud saya, ketika Anda mencoba untuk merilis sumber daya, Anda harus yakin itu dapat dirilis. Fakta terakhir tetapi tidak kalah pentingnya: jika CLR membongkar domain secara tidak normal, finalizer tipe, yang berasal dari CriticalFinalizerObject juga akan dipanggil, tidak seperti yang diwarisi langsung dari Object .


Charper ini diterjemahkan dari bahasa Rusia sebagai bahasa pengarang oleh penerjemah profesional . Anda dapat membantu kami membuat versi terjemahan teks ini ke bahasa lain termasuk Cina atau Jerman menggunakan versi Rusia dan Inggris teks sebagai sumber.

Juga, jika Anda ingin mengucapkan "terima kasih", cara terbaik yang dapat Anda pilih adalah memberi kami bintang di github atau repositori forking https://github.com/sidristij/dotnetbook

Source: https://habr.com/ru/post/id443958/


All Articles