Saya kira salah satu masalah paling penting dalam topik ini adalah membangun arsitektur penanganan pengecualian dalam aplikasi Anda. Ini menarik karena berbagai alasan. Dan alasan utama, saya pikir, adalah kesederhanaan yang jelas, yang Anda tidak selalu tahu harus melakukan apa. Semua konstruksi dasar seperti IEnumerable
, IDisposable
, IObservable
, dll. miliki properti ini dan gunakan di mana-mana. Di satu sisi, kesederhanaan mereka menggoda untuk menggunakan konstruksi ini dalam situasi yang berbeda. Di sisi lain, mereka penuh jebakan yang mungkin tidak Anda keluarkan. Ada kemungkinan bahwa dengan melihat jumlah informasi yang akan kami bahas, Anda punya pertanyaan: apa yang istimewa tentang situasi luar biasa?
Namun, untuk membuat kesimpulan tentang membangun arsitektur kelas pengecualian, kita harus mempelajari beberapa detail tentang klasifikasi mereka. Karena sebelum membangun sistem jenis yang akan jelas bagi pengguna kode, seorang programmer harus menentukan kapan harus memilih jenis kesalahan dan kapan harus menangkap atau melewatkan pengecualian. Jadi, mari kita mengklasifikasikan situasi luar biasa (bukan jenis pengecualian) berdasarkan berbagai fitur.
Berdasarkan kemungkinan teoritis untuk menangkap pengecualian di masa depan.
Berdasarkan fitur ini, kami dapat membagi pengecualian menjadi yang pasti ditangkap dan yang sangat mungkin tidak akan tertangkap. Mengapa saya mengatakan sangat mungkin ? Karena selalu ada seseorang yang akan mencoba menangkap pengecualian sementara ini tidak perlu.
Pertama, mari kita gambarkan kelompok pengecualian pertama - mereka yang harus ditangkap.
Dalam kasus pengecualian seperti itu, kami, di satu sisi, mengatakan kepada subsistem kami bahwa kami sampai pada keadaan ketika tidak ada gunanya tindakan lebih lanjut dengan data kami. Di sisi lain, kami maksudkan bahwa tidak ada bencana yang terjadi dan kami dapat menemukan jalan keluar dari situasi hanya dengan menangkap pengecualian. Properti ini sangat penting karena mendefinisikan kekritisan kesalahan dan memberikan keyakinan bahwa jika kita menangkap pengecualian dan sumber daya yang jelas, kita dapat melanjutkan dengan kode.
Kelompok kedua membahas pengecualian yang, meskipun mungkin terdengar aneh, tidak harus ditangkap. Mereka dapat digunakan hanya untuk logging kesalahan, tetapi tidak untuk memperbaiki situasi. Contoh paling sederhana adalah ArgumentException
dan NullReferenceException
. Bahkan, dalam situasi biasa Anda tidak perlu menangkap, misalnya, ArgumentNullException
karena dalam kasus ini sumber kesalahan adalah Anda. Jika Anda menangkap pengecualian seperti itu, Anda mengakui bahwa Anda membuat kesalahan dan mengirimkan sesuatu yang tidak dapat diterima ke metode:
void SomeMethod(object argument) { try { AnotherMethod(argument); } catch (ArgumentNullException exception) { // Log it } }
Dalam metode ini kami mencoba menangkap ArgumentNullException
. Tapi saya pikir ini aneh karena memberikan argumen yang benar pada suatu metode sepenuhnya menjadi perhatian kita. Bereaksi setelah peristiwa akan salah: hal terbaik yang dapat Anda lakukan dalam situasi seperti ini adalah memeriksa data yang dikirimkan terlebih dahulu sebelum memanggil metode atau bahkan membangun kode seperti itu di mana mendapatkan parameter yang salah tidak mungkin.
Kelompok lain dari situasi luar biasa adalah kesalahan fatal. Jika beberapa cache salah dan pekerjaan subsistem tidak benar, maka itu adalah kesalahan fatal dan kode terdekat pada stack tidak akan menangkapnya dengan pasti:
T GetFromCacheOrCalculate() { try { if(_cache.TryGetValue(Key, out var result)) { return result; } else { T res = Strategy(Key); _cache[Key] = res; return res; } } cache (CacheCorruptedException exception) { RecreateCache(); return GetFromCacheOrCalculate(); } }
CacheCorruptedException
adalah pengecualian yang berarti bahwa "cache hard drive tidak konsisten". Kemudian, jika penyebab kesalahan seperti itu fatal bagi subsistem cache (misalnya tidak ada hak akses file cache), kode berikut ini tidak dapat membuat kembali cache menggunakan instruksi RecreateCache
dan karenanya menangkap pengecualian ini adalah kesalahan itu sendiri.
Berdasarkan area di mana situasi luar biasa sebenarnya ditangkap
Masalah lainnya adalah apakah kita harus menangkap beberapa pengecualian atau memberikannya kepada seseorang yang memahami situasi dengan lebih baik. Dengan kata lain, kita harus menetapkan bidang tanggung jawab. Mari kita periksa kode berikut:
namespace JetFinance.Strategies { public class WildStrategy : StrategyBase { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { ?try? { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { } } } } using JetFinance.Strategies; using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); ?try? { boo.DoSomethingWild(); } catch(StrategyException exception) { } }
Manakah dari dua strategi yang lebih tepat? Area tanggung jawab sangat penting. Awalnya, mungkin terlihat bahwa kerja dan konsistensi WildInvestment
sepenuhnya bergantung pada WildStrategy
. Jadi, jika WildInvestment
mengabaikan pengecualian ini, itu akan naik ke level atas dan kita seharusnya tidak melakukan apa-apa. Namun, perhatikan bahwa dalam hal arsitektur, metode Main
menangkap pengecualian dari satu tingkat sambil memanggil metode dari yang lain. Bagaimana tampilannya dalam hal penggunaan? Begini tampilannya:
- tanggung jawab untuk pengecualian ini diserahkan kepada kami;
- pengguna kelas ini tidak yakin bahwa pengecualian ini sebelumnya melewati serangkaian metode yang disengaja;
- kita mulai membuat dependensi baru yang kita hilangkan dengan memanggil lapisan perantara.
Namun, ada kesimpulan lain yang dihasilkan dari ini: kita harus menggunakan catch
dalam metode DoSomethingWild
. Dan ini sedikit aneh bagi kami: WildInvestment
adalah semacam hampir tidak bergantung pada sesuatu. Maksud saya jika PlayRussianRoulette
tidak berfungsi, hal yang sama akan terjadi pada DoSomethingWild
: tidak memiliki kode pengembalian, tetapi harus memainkan roulette. Jadi, apa yang bisa kita lakukan dalam situasi yang tampaknya tanpa harapan? Jawabannya sebenarnya sederhana: berada di level lain DoSomethingWild
harus membuang pengecualiannya sendiri yang termasuk level ini dan membungkusnya dengan InnerException
sebagai sumber asli masalah:
namespace JetFinance.Strategies { pubilc class WildStrategy { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { try { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { throw new FailedInvestmentException("Oops", exception); } } } public class InvestmentException : Exception { /* .. */ } public class FailedInvestmentException : Exception { /* .. */ } } using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); try { boo.DoSomethingWild(); } catch(FailedInvestmentException exception) { } }
Dengan membungkus satu pengecualian di yang lain, kami mentransfer masalah dari satu tingkat aplikasi ke yang lain dan menjadikan pekerjaannya lebih mudah diprediksi dalam hal konsumen kelas ini: metode Main
.
Berdasarkan masalah penggunaan kembali
Seringkali kita merasa terlalu malas untuk membuat tipe pengecualian baru, tetapi ketika kita memutuskan untuk melakukannya, tidak selalu jelas tipe yang menjadi dasar. Namun justru keputusan inilah yang menentukan keseluruhan arsitektur situasi luar biasa. Mari kita lihat beberapa solusi populer dan buat beberapa kesimpulan.
Saat memilih jenis pengecualian, kita dapat menggunakan solusi yang dibuat sebelumnya, yaitu untuk menemukan pengecualian dengan nama yang mengandung arti yang sama dan menggunakannya. Misalnya, jika kami mendapat entitas melalui parameter dan kami tidak menyukai entitas ini, kami dapat melempar InvalidArgumentException
, yang mengindikasikan penyebab kesalahan dalam Pesan. Skenario ini terlihat bagus terutama karena InvalidArgumentException
termasuk dalam kelompok pengecualian yang mungkin tidak tertangkap. Namun, pilihan InvalidDataException
akan salah jika Anda bekerja dengan beberapa tipe data. Itu karena jenis ini ada di area System.IO
, yang mungkin bukan yang Anda hadapi. Dengan demikian, hampir selalu salah untuk mencari jenis yang sudah ada daripada mengembangkannya sendiri. Hampir tidak ada pengecualian untuk berbagai tugas umum. Hampir semuanya adalah untuk situasi tertentu dan jika Anda menggunakannya kembali dalam kasus lain, itu akan sangat melanggar arsitektur situasi luar biasa. Selain itu, pengecualian untuk tipe tertentu (misalnya, System.IO.InvalidDataException
) dapat membingungkan pengguna: di satu sisi, ia akan melihat bahwa pengecualian milik namespace System.IO
, sementara di sisi lain ia dilempar dari namespace yang sama sekali berbeda. Jika pengguna ini mulai berpikir tentang aturan melempar pengecualian ini, ia dapat pergi ke Referenceource.microsoft.com dan menemukan semua tempat di mana itu dilemparkan :
internal class System.IO.Compression.Inflater
Pengguna akan mengerti itu seseorang jempol jenis pengecualian ini membingungkannya karena metode yang melempar pengecualian ini tidak berurusan dengan kompresi.
Selain itu, dalam hal penggunaan kembali, Anda cukup membuat satu pengecualian dan mendeklarasikan bidang ErrorCode
di dalamnya. Itu sepertinya ide yang bagus. Anda hanya melempar pengecualian yang sama, mengatur kode, dan menggunakan hanya satu catch
untuk menangani pengecualian, meningkatkan stabilitas aplikasi, tidak lebih. Namun, saya yakin Anda harus memikirkan kembali posisi ini. Tentu saja, pendekatan ini membuat hidup lebih mudah di satu sisi. Namun, di sisi lain, Anda menolak kemungkinan untuk menangkap subkelompok pengecualian yang memiliki beberapa fitur umum. Misalnya, ArgumentException
yang menyatukan banyak pengecualian dengan warisan. Kerugian serius lainnya adalah kode yang terlalu besar dan tidak dapat dibaca yang harus mengatur penyaringan berdasarkan kode kesalahan. Namun, memperkenalkan tipe mencakup dengan kode kesalahan akan lebih tepat ketika pengguna tidak perlu peduli menentukan kesalahan.
public class ParserException { public ParserError ErrorCode { get; } public ParserException(ParserError errorCode) { ErrorCode = errorCode; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket, // ... } // Usage throw new ParserException(ParserError.MissingModifier);
Kode yang melindungi panggilan parser tidak peduli mengapa gagal parsing: tertarik pada kesalahan seperti itu. Namun, jika penyebab kegagalan menjadi penting, pengguna selalu bisa mendapatkan kode kesalahan dari properti ErrorCode
. Dan Anda benar-benar tidak perlu mencari kata-kata yang diperlukan dalam substring Message
.
Jika kami tidak memilih untuk menggunakan kembali, kami dapat membuat jenis pengecualian untuk setiap situasi. Di satu sisi, ini terdengar logis: satu jenis kesalahan - satu jenis pengecualian. Namun, jangan berlebihan: memiliki terlalu banyak jenis pengecualian akan menyebabkan masalah menangkapnya karena kode metode panggilan akan kelebihan beban dengan blok catch
. Karena itu perlu memproses semua jenis pengecualian yang ingin Anda sampaikan. Kerugian lain adalah murni arsitektur. Jika Anda tidak menggunakan pengecualian, Anda membingungkan mereka yang akan menggunakan pengecualian ini: mereka mungkin memiliki banyak kesamaan tetapi akan ditangkap secara terpisah.
Namun, ada skenario hebat untuk memperkenalkan tipe terpisah untuk situasi tertentu. Misalnya, ketika kesalahan mempengaruhi bukan seluruh entitas, tetapi metode tertentu. Maka jenis kesalahan ini harus mengambil tempat sedemikian dalam hierarki warisan sehingga tidak ada yang akan berpikir untuk menangkapnya bersama dengan sesuatu yang lain: misalnya, melalui cabang warisan yang terpisah.
Juga, jika Anda menggabungkan kedua pendekatan ini, Anda bisa mendapatkan seperangkat instrumen yang kuat untuk bekerja dengan sekelompok kesalahan: Anda dapat memperkenalkan jenis abstrak umum dan mewarisi kasus-kasus tertentu dari itu. Kelas dasar (tipe umum kami) harus mendapatkan properti abstrak, yang dirancang untuk menyimpan kode kesalahan sementara pewaris akan menentukan kode ini dengan menimpa properti ini.
public abstract class ParserException { public abstract ParserError ErrorCode { get; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket } public class MissingModifierParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingModifier; } public class MissingBracketParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingBracket; } // Usage throw new MissingModifierParserException(ParserError.MissingModifier);
Dengan menggunakan pendekatan ini, kami mendapatkan beberapa properti indah:
- di satu sisi, kami terus menangkap pengecualian menggunakan tipe dasar (umum);
- di sisi lain, bahkan menangkap pengecualian dengan tipe dasar ini kita masih dapat mengidentifikasi situasi tertentu;
- ditambah, kita dapat menangkap pengecualian melalui tipe tertentu dan bukan tipe dasar tanpa menggunakan struktur datar kelas.
Saya pikir ini sangat nyaman.
Didasarkan pada milik kelompok situasi perilaku tertentu
Kesimpulan apa yang bisa kita buat berdasarkan alasan sebelumnya? Mari kita coba mendefinisikannya.
Pertama-tama, mari kita putuskan apa artinya suatu situasi? Biasanya, kita berbicara tentang kelas dan objek dalam hal entitas dengan keadaan internal dan kita dapat melakukan tindakan pada entitas ini. Dengan demikian, jenis situasi perilaku pertama termasuk tindakan pada beberapa entitas. Selanjutnya, jika kita melihat pada grafik objek dari luar kita akan melihat bahwa itu secara logis direpresentasikan sebagai kombinasi dari kelompok-kelompok fungsional: kelompok pertama berurusan dengan caching, yang kedua bekerja dengan basis data, yang ketiga melakukan perhitungan matematis. Lapisan yang berbeda dapat melewati semua grup ini, misalnya lapisan status internal yang masuk, proses logging, dan penelusuran panggilan metode. Lapisan dapat mencakup beberapa kelompok fungsional. Misalnya, bisa ada lapisan model, lapisan pengontrol dan lapisan presentasi. Kelompok-kelompok ini dapat berada dalam satu majelis atau dalam majelis yang berbeda, tetapi masing-masing kelompok dapat menciptakan situasi yang luar biasa.
Jadi, kita dapat membangun hierarki untuk jenis situasi luar biasa berdasarkan pada kepemilikan jenis ini ke satu atau lain kelompok atau lapisan. Dengan demikian, kami memungkinkan kode tangkapan untuk dengan mudah menavigasi di antara jenis-jenis ini dalam hierarki.
Mari kita periksa kode berikut:
namespace JetFinance { namespace FinancialPipe { namespace Services { namespace XmlParserService { } namespace JsonCompilerService { } namespace TransactionalPostman { } } } namespace Accounting { /* ... */ } }
Seperti apa itu? Saya pikir namespace adalah cara sempurna untuk secara alami mengelompokkan jenis-jenis pengecualian berdasarkan situasi perilaku: segala sesuatu yang dimiliki kelompok tertentu harus tetap ada, termasuk pengecualian. Selain itu, ketika Anda mendapatkan pengecualian tertentu, Anda akan melihat nama jenisnya dan juga namespace-nya yang akan menentukan grup miliknya. Apakah Anda ingat penggunaan kembali yang buruk dari InvalidDataException
yang sebenarnya didefinisikan dalam System.IO
namespace? Fakta bahwa itu milik namespace ini berarti jenis pengecualian ini dapat dilempar dari kelas-kelas yang ada dalam System.IO
namespace atau yang lebih bersarang. Tetapi pengecualian yang sebenarnya dilemparkan dari ruang yang sama sekali berbeda, membingungkan seseorang yang menangani masalah ini. Namun, jika Anda menempatkan jenis pengecualian dan jenis yang membuang pengecualian ini di ruang nama yang sama, Anda menjaga arsitektur tipe konsisten dan memudahkan pengembang untuk memahami alasan apa yang terjadi.
Apa cara kedua untuk pengelompokan pada level kode? Warisan:
public abstract class LoggerExceptionBase : Exception { protected LoggerException(..); } public class IOLoggerException : LoggerExceptionBase { internal IOLoggerException(..); } public class ConfigLoggerException : LoggerExceptionBase { internal ConfigLoggerException(..); }
Perhatikan bahwa untuk entitas aplikasi biasa, mereka mewarisi perilaku dan data serta tipe grup yang dimiliki oleh satu grup entitas . Namun, sebagai pengecualian, mereka mewarisi dan dikelompokkan berdasarkan satu kelompok situasi , karena esensi pengecualian bukanlah suatu entitas tetapi masalah.
Menggabungkan dua metode pengelompokan ini kita dapat membuat kesimpulan berikut:
- harus ada jenis dasar pengecualian di dalam
Assembly
yang akan dilemparkan oleh majelis ini. Jenis pengecualian ini harus berada dalam root namespace majelis. Ini akan menjadi lapisan pertama pengelompokan. - lebih lanjut, bisa ada satu atau beberapa ruang nama di dalam sebuah majelis. Masing-masing dari mereka membagi rakitan menjadi zona fungsional, mendefinisikan kelompok-kelompok situasi, yang muncul dalam rakitan ini. Ini mungkin zona pengendali, entitas basis data, algoritma pemrosesan data, dll. Bagi kami, ruang nama ini berarti jenis pengelompokan berdasarkan fungsinya. Namun, dalam hal pengecualian, mereka dikelompokkan berdasarkan masalah dalam majelis yang sama;
- pengecualian harus diwarisi dari jenis di namespace tingkat atas yang sama. Ini memastikan bahwa pengguna akhir akan memahami situasi dengan jelas dan tidak akan menangkap pengecualian berbasis tipe yang salah . Akui, akan aneh untuk menangkap
global::Finiki.Logistics.OhMyException
dengan catch(global::Legacy.LoggerExeption exception)
, sedangkan kode berikut terlihat sangat sesuai:
namespace JetFinance.FinancialPipe { namespace Services.XmlParserService { public class XmlParserServiceException : FinancialPipeExceptionBase { // .. } public class Parser { public void Parse(string input) { // .. } } } public abstract class FinancialPipeExceptionBase : Exception { } } using JetFinance.FinancialPipe; using JetFinance.FinancialPipe.Services.XmlParserService; var parser = new Parser(); try { parser.Parse(); } catch (XmlParserServiceException exception) { // Something is wrong in the parser } catch (FinancialPipeExceptionBase exception) { // Something else is wrong. Looks critical because we don't know the real reason }
Di sini, kode pengguna memanggil metode pustaka yang, seperti kita ketahui, dapat melempar XmlParserServiceException
dalam beberapa situasi. Dan, seperti yang kita ketahui, pengecualian ini merujuk ke namespace yang diwariskan JetFinance.FinancialPipe.FinancialPipeExceptionBase
, yang berarti bahwa mungkin ada beberapa pengecualian lain - kali ini XmlParserService
hanya membuat satu pengecualian tetapi pengecualian lain mungkin muncul di masa depan. Karena kami memiliki konvensi untuk membuat jenis pengecualian, kami tahu dari entitas mana pengecualian baru ini akan diwarisi dan memasukkan catch
mencakup sebelumnya. Itu memungkinkan kita untuk melewati semua hal yang tidak relevan bagi kita.
Bagaimana cara membangun hierarki jenis seperti itu?
- Pertama-tama, kita harus membuat kelas dasar untuk domain. Sebut saja kelas basis domain. Dalam hal ini, domain adalah kata yang mencakup sejumlah majelis, menggabungkannya berdasarkan beberapa fitur: logging, business-logic, UI. Maksud saya zona fungsional aplikasi yang sebesar mungkin.
- Selanjutnya, kita harus memperkenalkan kelas dasar tambahan untuk pengecualian yang harus ditangkap: semua pengecualian yang akan ditangkap menggunakan kata kunci
catch
akan diwarisi dari kelas dasar ini; - Semua pengecualian yang mengindikasikan kesalahan fatal harus diwarisi langsung dari kelas basis domain. Dengan demikian kami akan memisahkan mereka dari yang tertangkap pada tingkat arsitektur;
- Membagi domain menjadi area fungsional berdasarkan ruang nama dan mendeklarasikan tipe dasar pengecualian yang akan dilempar dari setiap area. Di sini perlu untuk menggunakan akal sehat: jika aplikasi memiliki tingkat tinggi namespace nesting, Anda tidak harus melakukan tipe dasar untuk setiap level nesting. Namun, jika ada percabangan pada tingkat bersarang ketika satu kelompok pengecualian pergi ke satu namespace dan grup lain pergi ke namespace lain, perlu menggunakan dua tipe dasar untuk setiap subkelompok; - Pengecualian khusus harus diwarisi dari jenis pengecualian yang termasuk dalam area fungsional
- Jika sekelompok pengecualian khusus dapat digabungkan, Anda perlu melakukannya dalam satu tipe dasar lagi: sehingga Anda dapat menangkapnya dengan lebih mudah;
- Jika Anda mengira grup akan lebih sering tertangkap menggunakan kelas dasar, perkenalkan Mode Campuran dengan ErrorCode.
Berdasarkan sumber kesalahan
Sumber kesalahan dapat menjadi dasar lain untuk menggabungkan pengecualian dalam grup. Misalnya, jika Anda mendesain perpustakaan kelas, hal-hal berikut ini dapat membentuk kelompok sumber:
- panggilan kode tidak aman dengan kesalahan. Situasi ini dapat ditangani dengan membungkus pengecualian atau kode kesalahan dalam jenis pengecualian sendiri sambil menyimpan data yang dikembalikan (misalnya kode kesalahan asli) di properti publik pengecualian;
- panggilan kode oleh dependensi eksternal, yang telah melemparkan pengecualian yang tidak dapat ditangkap oleh perpustakaan kami karena mereka berada di luar tanggung jawabnya. Grup ini dapat menyertakan pengecualian dari metode entitas yang diterima sebagai parameter metode saat ini atau pengecualian dari konstruktor kelas yang metode ini disebut ketergantungan eksternal. Sebagai contoh, metode kelas kami telah memanggil metode kelas lain, contohnya dikembalikan melalui parameter metode lain. Jika pengecualian menunjukkan bahwa kami adalah sumber masalah, kami harus membuat pengecualian sendiri sambil mempertahankan yang asli di
InnerExcepton
. Namun, jika kita memahami bahwa masalahnya disebabkan oleh dependensi eksternal, kita mengabaikan pengecualian ini sebagai bagian dari grup dependensi eksternal di luar kendali kita; - kode kita sendiri yang secara tidak sengaja dimasukkan ke dalam kondisi yang tidak konsisten. Contoh yang baik adalah parsing teks - tidak ada dependensi eksternal, tidak ada transfer ke dunia yang
unsafe
, tetapi masalah parsing terjadi.
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 .