Pola sekali pakai (Prinsip Desain Sekali Pakai) hal 2


Jenis SafeHandle / CriticalHandle / SafeBuffer / turunan


Saya merasa saya akan membuka kotak Pandora untuk Anda. Mari kita bicara tentang tipe khusus: SafeHandle, CriticalHandle dan tipe turunannya.


Ini adalah hal terakhir tentang pola tipe yang memberikan akses ke sumber daya yang tidak dikelola. Tapi pertama-tama, mari daftarkan semua yang biasanya kita dapatkan dari dunia yang tidak dikelola:


Yang pertama dan jelas adalah pegangan. Ini mungkin kata yang tidak berarti untuk pengembang .NET, tetapi ini adalah komponen yang sangat penting dari dunia sistem operasi. Pegangan pada dasarnya adalah nomor 32 atau 64-bit. Ini menunjuk sesi interaksi terbuka dengan sistem operasi. Misalnya, ketika Anda membuka file Anda mendapatkan pegangan dari fungsi WinApi. Kemudian Anda dapat bekerja dengannya dan melakukan pencarian, baca atau tulis operasi. Atau, Anda dapat membuka soket untuk akses jaringan. Sekali lagi sistem operasi akan memberi Anda pegangan. Dalam .NET handle disimpan sebagai tipe IntPtr ;


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 .

  • Yang kedua adalah array data. Anda dapat bekerja dengan array yang tidak dikelola baik melalui kode yang tidak aman (tidak aman adalah kata kunci di sini) atau menggunakan SafeBuffer yang akan membungkus buffer data ke dalam kelas .NET yang sesuai. Perhatikan bahwa cara pertama lebih cepat (misalnya Anda dapat mengoptimalkan lilitan sangat), tetapi yang kedua jauh lebih aman, karena didasarkan pada SafeHandle;
  • Lalu pergi string. String itu sederhana karena kita perlu menentukan format dan pengkodean dari string yang kita tangkap. Ini kemudian disalin untuk kita (string adalah kelas yang tidak dapat diubah) dan kita tidak khawatir lagi.
  • Yang terakhir adalah ValueTypes yang baru saja disalin sehingga kita tidak perlu memikirkannya sama sekali.

SafeHandle adalah kelas .NET CLR khusus yang mewarisi CriticalFinalizerObject dan harus membungkus pegangan sistem operasi dengan cara yang paling aman dan paling nyaman.


[SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { protected IntPtr handle; // The handle from OS private int _state; // State (validity, the reference counter) private bool _ownsHandle; // The flag for the possibility to release the handle. // It may happen that we wrap somebody else's handle // have no right to release. private bool _fullyInitialized; // The initialized instance [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { } // The finalizer calls Dispose(false) with a pattern [SecuritySafeCritical] ~SafeHandle() { Dispose(false); } // You can set a handle manually or automatically with p/invoke Marshal [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected void SetHandle(IntPtr handle) { this.handle = handle; } // This method is necessary to work with IntPtr directly. It is used to // determine if a handle was created by comparing it with one of the previously // determined known values. Pay attention that this method is dangerous because: // // – if a handle is marked as invalid by SetHandleasInvalid, DangerousGetHandle // it will anyway return the original value of the handle. // – you can reuse the returned handle at any place. This can at least // mean, that it will stop work without a feedback. In the worst case if // IntPtr is passed directly to another place, it can go to an unsafe code and become // a vector for application attack by resource substitution in one IntPtr [ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public IntPtr DangerousGetHandle() { return handle; } // The resource is closed (no more available for work) public bool IsClosed { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get { return (_state & 1) == 1; } } // The resource is not available for work. You can override the property by changing the logic. public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get; } // Closing the resource through Close() pattern [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Close() { Dispose(true); } // Closing the resource through Dispose() pattern [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Dispose() { Dispose(true); } [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Dispose(bool disposing) { // ... } // You should call this method every time when you understand that a handle is not operational anymore. // If you don't do it, you can get a leak. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void SetHandleAsInvalid(); // Override this method to point how to release // the resource. You should code carefully, as you cannot // call uncompiled methods, create new objects or produce exceptions from it. // A returned value shows if the resource was releases successfully. // If a returned value = false, SafeHandleCriticalFailure will occur // that will enter a breakpoint if SafeHandleCriticalFailure // Managed Debugger Assistant is activated. [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected abstract bool ReleaseHandle(); // Working with the reference counter. To be explained further. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void DangerousAddRef(ref bool success); public extern void DangerousRelease(); } 

Untuk memahami kegunaan kelas-kelas yang berasal dari SafeHandle, Anda perlu mengingat mengapa tipe .NET sangat bagus: GC dapat mengumpulkan instance mereka secara otomatis. Ketika SafeHandle dikelola, sumber daya yang tidak dikelola yang dibungkusnya mewarisi semua karakteristik dunia yang dikelola. Ini juga mengandung penghitung internal referensi eksternal yang tidak tersedia untuk CLR. Maksud saya referensi dari kode yang tidak aman. Anda tidak perlu menambah atau mengurangi penghitung secara manual sama sekali. Saat Anda mendeklarasikan tipe yang berasal dari SafeHandle sebagai parameter metode yang tidak aman, penghitung akan bertambah saat memasukkan metode tersebut atau dikurangi setelah keluar. Alasannya adalah bahwa ketika Anda pergi ke kode yang tidak aman dengan melewati pegangan di sana, Anda mungkin mengumpulkan SafeHandle ini oleh GC, dengan mengatur ulang referensi ke pegangan ini di utas lain (jika Anda berurusan dengan satu pegangan dari beberapa utas). Hal-hal bekerja lebih mudah dengan penghitung referensi: SafeHandle tidak akan dibuat sampai penghitung nol. Itu sebabnya Anda tidak perlu mengubah penghitung secara manual. Atau, Anda harus melakukannya dengan sangat hati-hati dengan mengembalikannya jika memungkinkan.


Tujuan kedua dari penghitung referensi adalah untuk mengatur urutan finalisasi CriticalFinalizerObject yang saling referensi. Jika salah satu tipe berbasis SafeHandle merujuk yang lain, maka Anda perlu menambah penghitung referensi di konstruktor dari tipe referensi dan mengurangi penghitung dalam metode ReleaseHandle. Dengan demikian, objek Anda akan ada sampai objek yang referensi objek Anda tidak dihancurkan. Namun, lebih baik untuk menghindari kebingungan seperti itu. Mari kita gunakan pengetahuan tentang SafeHandlers dan tulis varian terakhir dari kelas kami:


 public class FileWrapper : IDisposable { SafeFileHandle _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; _handle.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern SafeFileHandle CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); /// other methods } 

Apa bedanya? Jika Anda menetapkan tipe berbasis SafeHandle (termasuk milik Anda) sebagai nilai balik dalam metode DllImport, maka Marshal akan membuat dan menginisialisasi tipe ini dengan benar dan menetapkan penghitung ke 1. Mengetahui hal ini, kami menetapkan jenis SafeFileHandle sebagai tipe pengembalian untuk fungsi CreateFile kernel. Ketika kami mendapatkannya, kami akan menggunakannya tepat untuk memanggil ReadFile dan WriteFile (sebagai kenaikan nilai counter saat memanggil dan mengurangi ketika keluar akan memastikan bahwa pegangan masih ada selama membaca dari dan menulis ke file). Ini adalah tipe yang dirancang dengan benar dan akan dapat dipercaya menangani file jika utas dibatalkan. Ini berarti kita tidak perlu mengimplementasikan finalizer kita sendiri dan segala sesuatu yang berhubungan dengannya. Seluruh tipe disederhanakan.


Eksekusi finalizer ketika metode instance bekerja


Ada satu teknik optimasi yang digunakan selama pengumpulan sampah yang dirancang untuk mengumpulkan lebih banyak objek dalam waktu yang lebih singkat. Mari kita lihat kode berikut:


 public void SampleMethod() { var obj = new object(); obj.ToString(); // ... // If GC runs at this point, it may collect obj // as it is not used anymore // ... Console.ReadLine(); } 

Di satu sisi, kodenya terlihat aman, dan tidak jelas mengapa kita harus peduli. Namun, jika Anda ingat bahwa ada kelas yang membungkus sumber daya yang tidak dikelola, Anda akan memahami bahwa kelas yang dirancang secara tidak benar dapat menyebabkan pengecualian dari dunia yang tidak dikelola. Pengecualian ini akan melaporkan bahwa pegangan yang diperoleh sebelumnya tidak aktif:


 // The example of an absolutely incorrect implementation void Main() { var inst = new SampleClass(); inst.ReadData(); // inst is not used further } public sealed class SampleClass : CriticalFinalizerObject, IDisposable { private IntPtr _handle; public SampleClass() { _handle = CreateFile("test.txt", 0, 0, IntPtr.Zero, 0, 0, IntPtr.Zero); } public void Dispose() { if (_handle != IntPtr.Zero) { CloseHandle(_handle); _handle = IntPtr.Zero; } } ~SampleClass() { Console.WriteLine("Finalizing instance."); Dispose(); } public unsafe void ReadData() { Console.WriteLine("Calling GC.Collect..."); // I redirected it to the local variable not to // use this after GC.Collect(); var handle = _handle; // The imitation of full GC.Collect GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("Finished doing something."); var overlapped = new NativeOverlapped(); // it is not important what we do ReadFileEx(handle, new byte[] { }, 0, ref overlapped, (a, b, c) => {;}); } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)] static extern IntPtr CreateFile(String lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttrs, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadFileEx(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, [In] ref NativeOverlapped lpOverlapped, IOCompletionCallback lpCompletionRoutine); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hObject); } 

Akui bahwa kode ini terlihat layak atau kurang layak. Bagaimanapun, sepertinya tidak ada masalah. Padahal, ada masalah serius. Finalizer kelas dapat mencoba untuk menutup file saat membacanya, yang hampir pasti mengarah pada kesalahan. Karena dalam kasus ini kesalahan dikembalikan secara eksplisit ( IntPtr == -1 ) kita tidak akan melihat ini. _handle akan ditetapkan ke nol, Dispose berikut akan gagal untuk menutup file dan sumber daya akan bocor. Untuk mengatasi masalah ini, Anda harus menggunakan SafeHandle , CriticalHandle , SafeBuffer dan kelas turunannya. Selain bahwa kelas-kelas ini memiliki penghitung penggunaan dalam kode yang tidak dikelola, penghitung ini juga secara otomatis bertambah ketika meneruskan dengan parameter metode ke dunia yang tidak dikelola dan penurunan ketika meninggalkannya.


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/id443960/


All Articles