Kode C# Buggy: 10 Kesalahan Paling Umum dalam Pemrograman C#
Diterbitkan: 2022-03-11Tentang C Sharp
C# adalah salah satu dari beberapa bahasa yang menargetkan Microsoft Common Language Runtime (CLR). Bahasa yang menargetkan CLR mendapat manfaat dari fitur-fitur seperti integrasi lintas bahasa dan penanganan pengecualian, keamanan yang ditingkatkan, model interaksi komponen yang disederhanakan, dan layanan debugging dan pembuatan profil. Dari bahasa CLR saat ini, C# adalah yang paling banyak digunakan untuk proyek pengembangan profesional yang kompleks yang menargetkan lingkungan desktop, seluler, atau server Windows.
C# adalah bahasa yang berorientasi objek dan diketik dengan kuat. Pengecekan tipe yang ketat dalam C#, baik pada waktu kompilasi dan run, menghasilkan sebagian besar kesalahan pemrograman C# yang umum dilaporkan sedini mungkin, dan lokasinya ditunjukkan dengan cukup akurat. Ini dapat menghemat banyak waktu dalam pemrograman C Sharp, dibandingkan dengan melacak penyebab kesalahan membingungkan yang dapat terjadi lama setelah operasi yang menyinggung terjadi dalam bahasa yang lebih liberal dengan penegakan keamanan jenisnya. Namun, banyak pembuat kode C# tanpa disadari (atau sembarangan) membuang manfaat dari deteksi ini, yang mengarah ke beberapa masalah yang dibahas dalam tutorial C# ini.
Tentang Tutorial Pemrograman C Sharp Ini
Tutorial ini menjelaskan 10 kesalahan pemrograman C# paling umum yang dibuat, atau masalah yang harus dihindari, oleh pemrogram C# dan memberi mereka bantuan.
Sementara sebagian besar kesalahan yang dibahas dalam artikel ini adalah khusus C#, beberapa juga relevan dengan bahasa lain yang menargetkan CLR atau menggunakan Framework Class Library (FCL).
Kesalahan Umum Pemrograman C# #1: Menggunakan referensi seperti nilai atau sebaliknya
Pemrogram C++, dan banyak bahasa lainnya, terbiasa mengendalikan apakah nilai yang mereka berikan ke variabel hanyalah nilai atau referensi ke objek yang ada. Dalam pemrograman C Sharp, bagaimanapun, keputusan itu dibuat oleh programmer yang menulis objek, bukan oleh programmer yang membuat objek dan menetapkannya ke variabel. Ini adalah "gotcha" umum bagi mereka yang mencoba mempelajari pemrograman C#.
Jika Anda tidak tahu apakah objek yang Anda gunakan adalah tipe nilai atau tipe referensi, Anda bisa mengalami beberapa kejutan. Sebagai contoh:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
Seperti yang Anda lihat, objek Point
dan Pen
dibuat dengan cara yang sama persis, tetapi nilai point1
tetap tidak berubah ketika nilai koordinat X
baru ditetapkan ke point2
, sedangkan nilai pen1
dimodifikasi ketika warna baru ditetapkan ke pen2
. Oleh karena itu kita dapat menyimpulkan bahwa point1
dan point2
masing-masing berisi salinan objek Point
mereka sendiri, sedangkan pen1
dan pen2
berisi referensi ke objek Pen
yang sama. Tapi bagaimana kita bisa mengetahuinya tanpa melakukan eksperimen ini?
Jawabannya adalah dengan melihat definisi tipe objek (yang dapat Anda lakukan dengan mudah di Visual Studio dengan menempatkan kursor di atas nama tipe objek dan menekan F12):
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
Seperti yang ditunjukkan di atas, dalam pemrograman C#, kata kunci struct
digunakan untuk mendefinisikan tipe nilai, sedangkan kata kunci class
digunakan untuk mendefinisikan tipe referensi. Bagi mereka yang memiliki latar belakang C++, yang terbuai dengan rasa aman yang salah dengan banyaknya kesamaan antara kata kunci C++ dan C#, perilaku ini mungkin mengejutkan sehingga Anda mungkin meminta bantuan dari tutorial C#.
Jika Anda akan bergantung pada beberapa perilaku yang berbeda antara nilai dan tipe referensi – seperti kemampuan untuk melewatkan objek sebagai parameter metode dan meminta metode tersebut mengubah status objek – pastikan Anda berurusan dengan jenis objek yang benar untuk menghindari masalah pemrograman C#.
Kesalahan Pemrograman C# Umum #2: Kesalahpahaman nilai default untuk variabel yang tidak diinisialisasi
Di C#, tipe nilai tidak boleh nol. Menurut definisi, tipe nilai memiliki nilai, dan bahkan variabel tipe nilai yang tidak diinisialisasi harus memiliki nilai. Ini disebut nilai default untuk tipe itu. Ini mengarah ke yang berikut, biasanya hasil yang tidak terduga saat memeriksa apakah suatu variabel tidak diinisialisasi:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
Mengapa point1
tidak nol? Jawabannya adalah Point
adalah tipe nilai, dan nilai default untuk Point
adalah (0,0), bukan null. Kegagalan untuk mengenali ini adalah kesalahan yang sangat mudah (dan umum) dilakukan di C#.
Banyak (tetapi tidak semua) tipe nilai memiliki properti IsEmpty
yang dapat Anda periksa untuk melihat apakah itu sama dengan nilai defaultnya:
Console.WriteLine(point1.IsEmpty); // True
Saat Anda memeriksa untuk melihat apakah suatu variabel telah diinisialisasi atau tidak, pastikan Anda mengetahui nilai apa yang akan dimiliki oleh variabel yang tidak diinisialisasi dari jenis itu secara default dan jangan mengandalkannya sebagai nol..
Kesalahan Pemrograman C# Umum #3: Menggunakan metode perbandingan string yang tidak tepat atau tidak ditentukan
Ada banyak cara berbeda untuk membandingkan string dalam C#.
Meskipun banyak programmer menggunakan operator ==
untuk perbandingan string, sebenarnya ini adalah salah satu metode yang paling tidak diinginkan untuk digunakan, terutama karena tidak menentukan secara eksplisit dalam kode jenis perbandingan yang diinginkan.
Sebaliknya, cara yang lebih disukai untuk menguji kesetaraan string dalam pemrograman C# adalah dengan metode Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
Tanda tangan metode pertama (yaitu, tanpa parameter comparisonType
), sebenarnya sama dengan menggunakan operator ==
, tetapi memiliki manfaat untuk diterapkan secara eksplisit ke string. Ia melakukan perbandingan ordinal dari string, yang pada dasarnya adalah perbandingan byte demi byte. Dalam banyak kasus, ini adalah jenis perbandingan yang Anda inginkan, terutama ketika membandingkan string yang nilainya diatur secara terprogram, seperti nama file, variabel lingkungan, atribut, dll. Dalam kasus ini, selama perbandingan ordinal memang jenis yang benar perbandingan untuk situasi itu, satu-satunya downside menggunakan metode Equals
tanpa comparisonType
adalah bahwa seseorang yang membaca kode mungkin tidak tahu jenis perbandingan apa yang Anda buat.
Namun, menggunakan tanda tangan metode Equals
yang menyertakan tipe comparisonType
setiap kali Anda membandingkan string, tidak hanya akan membuat kode Anda lebih jelas, tetapi juga akan membuat Anda secara eksplisit memikirkan jenis perbandingan yang perlu Anda buat. Ini adalah hal yang berharga untuk dilakukan, karena meskipun bahasa Inggris mungkin tidak memberikan banyak perbedaan antara perbandingan ordinal dan peka budaya, bahasa lain menyediakan banyak, dan mengabaikan kemungkinan bahasa lain membuka diri Anda terhadap banyak potensi untuk kesalahan di jalan. Sebagai contoh:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
Praktik paling aman adalah selalu memberikan parameter comparisonType
ke metode Equals
. Berikut adalah beberapa pedoman dasar:
- Saat membandingkan string yang dimasukkan oleh pengguna, atau akan ditampilkan kepada pengguna, gunakan perbandingan peka budaya (
CurrentCulture
atauCurrentCultureIgnoreCase
). - Saat membandingkan string terprogram, gunakan perbandingan ordinal (
Ordinal
atauOrdinalIgnoreCase
). -
InvariantCulture
danInvariantCultureIgnoreCase
umumnya tidak digunakan kecuali dalam keadaan yang sangat terbatas, karena perbandingan ordinal lebih efisien. Jika perbandingan budaya-sadar diperlukan, biasanya harus dilakukan terhadap budaya saat ini atau budaya tertentu lainnya.
Selain metode Equals
, string juga menyediakan metode Compare
, yang memberi Anda informasi tentang urutan relatif string, bukan hanya tes kesetaraan. Metode ini lebih disukai daripada operator <
, <=
, >
dan >=
, untuk alasan yang sama seperti yang dibahas di atas–untuk menghindari masalah C#.
Kesalahan Pemrograman C# yang Umum #4: Menggunakan pernyataan iteratif (bukan deklaratif) untuk memanipulasi koleksi
Di C# 3.0, penambahan Language-Integrated Query (LINQ) ke bahasa berubah selamanya cara koleksi ditanyai dan dimanipulasi. Sejak itu, jika Anda menggunakan pernyataan berulang untuk memanipulasi koleksi, Anda tidak menggunakan LINQ saat seharusnya.
Beberapa programmer C# bahkan tidak mengetahui keberadaan LINQ, tetapi untungnya jumlahnya semakin sedikit. Namun, banyak yang masih berpikir bahwa karena kesamaan antara kata kunci LINQ dan pernyataan SQL, penggunaannya hanya dalam kode yang menanyakan basis data.
Sementara query database adalah penggunaan yang sangat umum dari pernyataan LINQ, mereka benar-benar bekerja pada setiap koleksi enumerable (yaitu, objek apapun yang mengimplementasikan antarmuka IEnumerable). Jadi misalnya, jika Anda memiliki larik Akun, alih-alih menulis C# List foreach:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
Anda hanya bisa menulis:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Meskipun ini adalah contoh yang cukup sederhana tentang cara menghindari masalah pemrograman C# yang umum ini, ada kasus di mana satu pernyataan LINQ dapat dengan mudah mengganti lusinan pernyataan dalam loop berulang (atau loop bersarang) dalam kode Anda. Dan lebih sedikit kode umum berarti lebih sedikit peluang bagi bug untuk diperkenalkan. Perlu diingat, bagaimanapun, mungkin ada trade-off dalam hal kinerja. Dalam skenario kinerja-kritis, terutama di mana kode iteratif Anda dapat membuat asumsi tentang koleksi Anda yang LINQ tidak bisa, pastikan untuk melakukan perbandingan kinerja antara dua metode.
Kesalahan Umum Pemrograman C# #5: Gagal mempertimbangkan objek yang mendasarinya dalam pernyataan LINQ
LINQ sangat bagus untuk mengabstraksi tugas memanipulasi koleksi, apakah itu objek dalam memori, tabel database, atau dokumen XML. Di dunia yang sempurna, Anda tidak perlu tahu apa objek dasarnya. Tetapi kesalahan di sini adalah menganggap kita hidup di dunia yang sempurna. Faktanya, pernyataan LINQ yang identik dapat mengembalikan hasil yang berbeda ketika dieksekusi pada data yang sama persis, jika data tersebut berada dalam format yang berbeda.
Sebagai contoh, perhatikan pernyataan berikut:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Apa yang terjadi jika salah satu account.Status
objek. Statusnya sama dengan "Aktif" (perhatikan huruf besar A)? Nah, jika myAccounts
adalah objek DbSet
(yang diatur dengan konfigurasi case-insensitive default), ekspresi where
masih akan cocok dengan elemen itu. Namun, jika myAccounts
berada dalam array dalam memori, itu tidak akan cocok, dan karena itu akan menghasilkan hasil total yang berbeda.
Tapi tunggu sebentar. Ketika kita berbicara tentang perbandingan string sebelumnya, kita melihat bahwa operator ==
melakukan perbandingan string ordinal. Jadi mengapa dalam kasus ini operator ==
melakukan perbandingan case-insensitive?
Jawabannya adalah ketika objek yang mendasari dalam pernyataan LINQ adalah referensi ke data tabel SQL (seperti halnya dengan objek Entity Framework DbSet dalam contoh ini), pernyataan diubah menjadi pernyataan T-SQL. Operator kemudian mengikuti aturan pemrograman T-SQL, bukan aturan pemrograman C#, sehingga perbandingan dalam kasus di atas menjadi tidak peka huruf besar-kecil.
Secara umum, meskipun LINQ adalah cara yang membantu dan konsisten untuk menanyakan koleksi objek, pada kenyataannya Anda masih perlu tahu apakah pernyataan Anda akan diterjemahkan ke sesuatu selain C# di bawah tenda untuk memastikan bahwa perilaku kode Anda akan seperti yang diharapkan pada saat runtime.
Kesalahan Umum Pemrograman C# #6: Menjadi bingung atau dipalsukan dengan metode ekstensi
Seperti disebutkan sebelumnya, pernyataan LINQ bekerja pada objek apa pun yang mengimplementasikan IEnumerable. Misalnya, fungsi sederhana berikut akan menjumlahkan saldo pada setiap kumpulan akun:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
Dalam kode di atas, jenis parameter myAccounts dideklarasikan sebagai IEnumerable<Account>
. Karena myAccounts
mereferensikan metode Sum
(C# menggunakan "notasi titik" yang sudah dikenal untuk merujuk metode pada kelas atau antarmuka), kita akan mengharapkan untuk melihat metode yang disebut Sum()
pada definisi antarmuka IEnumerable<T>
. Namun, definisi IEnumerable<T>
, tidak merujuk ke metode Sum
apa pun dan hanya terlihat seperti ini:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Jadi di mana metode Sum()
didefinisikan? C# diketik dengan kuat, jadi jika referensi ke metode Sum
tidak valid, kompiler C# pasti akan menandainya sebagai kesalahan. Karena itu kita tahu bahwa itu pasti ada, tetapi di mana? Selain itu, di mana definisi dari semua metode lain yang disediakan LINQ untuk menanyakan atau menggabungkan koleksi ini?
Jawabannya adalah Sum()
bukan metode yang didefinisikan pada antarmuka IEnumerable
. Sebaliknya, ini adalah metode statis (disebut "metode ekstensi") yang didefinisikan pada kelas System.Linq.Enumerable
:
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
Jadi apa yang membuat metode ekstensi berbeda dari metode statis lainnya dan apa yang memungkinkan kita mengaksesnya di kelas lain?
Ciri khas dari metode ekstensi adalah pengubah this
pada parameter pertamanya. Ini adalah "keajaiban" yang mengidentifikasikannya ke kompiler sebagai metode ekstensi. Jenis parameter yang dimodifikasinya (dalam hal ini IEnumerable<TSource>
) menunjukkan kelas atau antarmuka yang kemudian akan muncul untuk mengimplementasikan metode ini.
(Sebagai poin tambahan, tidak ada yang ajaib tentang kesamaan antara nama antarmuka IEnumerable
dan nama kelas Enumerable
di mana metode ekstensi didefinisikan. Kesamaan ini hanyalah pilihan gaya yang berubah-ubah.)
Dengan pemahaman ini, kita juga dapat melihat bahwa fungsi sumAccounts
yang kita perkenalkan di atas dapat diimplementasikan sebagai berikut:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
Fakta bahwa kita bisa mengimplementasikannya dengan cara ini malah menimbulkan pertanyaan mengapa memiliki metode ekstensi sama sekali? Metode ekstensi pada dasarnya adalah kenyamanan bahasa pemrograman C# yang memungkinkan Anda untuk "menambahkan" metode ke tipe yang ada tanpa membuat tipe turunan baru, mengkompilasi ulang, atau memodifikasi tipe aslinya.

Metode ekstensi dimasukkan ke dalam ruang lingkup dengan menyertakan using [namespace];
pernyataan di bagian atas file. Anda perlu tahu namespace C# mana yang menyertakan metode ekstensi yang Anda cari, tetapi itu cukup mudah untuk ditentukan setelah Anda tahu apa yang Anda cari.
Ketika kompiler C# menemukan pemanggilan metode pada instance objek, dan tidak menemukan metode yang didefinisikan pada kelas objek yang direferensikan, ia kemudian melihat semua metode ekstensi yang berada dalam ruang lingkup untuk mencoba menemukan metode yang cocok dengan metode yang diperlukan. tanda tangan dan kelas. Jika ditemukan, ia akan meneruskan referensi instans sebagai argumen pertama ke metode ekstensi itu, lalu argumen lainnya, jika ada, akan diteruskan sebagai argumen berikutnya ke metode ekstensi. (Jika kompiler C# tidak menemukan metode ekstensi yang sesuai dalam ruang lingkup, itu akan menimbulkan kesalahan.)
Metode ekstensi adalah contoh "gula sintaksis" pada bagian dari kompiler C#, yang memungkinkan kita untuk menulis kode yang (biasanya) lebih jelas dan lebih mudah dipelihara. Lebih jelas, yaitu, jika Anda mengetahui penggunaannya. Jika tidak, ini bisa sedikit membingungkan, terutama pada awalnya.
Meskipun tentu saja ada keuntungan menggunakan metode ekstensi, mereka dapat menyebabkan masalah dan seruan untuk bantuan pemrograman C# bagi para pengembang yang tidak menyadarinya atau tidak memahaminya dengan benar. Ini terutama benar ketika melihat contoh kode online, atau kode pra-tertulis lainnya. Ketika kode tersebut menghasilkan kesalahan kompiler (karena memanggil metode yang secara jelas tidak didefinisikan pada kelas tempat mereka dipanggil), kecenderungannya adalah untuk berpikir bahwa kode tersebut berlaku untuk versi perpustakaan yang berbeda, atau ke perpustakaan yang berbeda sama sekali. Banyak waktu dapat dihabiskan untuk mencari versi baru, atau "perpustakaan yang hilang", yang tidak ada.
Bahkan pengembang yang akrab dengan metode ekstensi kadang-kadang masih terjebak, ketika ada metode dengan nama yang sama pada objek, tetapi tanda tangan metodenya berbeda secara halus dari metode ekstensi. Banyak waktu yang terbuang untuk mencari kesalahan ketik atau kesalahan yang sebenarnya tidak ada.
Penggunaan metode ekstensi di perpustakaan C# menjadi semakin lazim. Selain LINQ, Unity Application Block dan kerangka Web API adalah contoh dari dua perpustakaan modern yang banyak digunakan oleh Microsoft yang juga menggunakan metode ekstensi, dan masih banyak lagi lainnya. Semakin modern kerangka kerja, semakin besar kemungkinan ia akan menggabungkan metode ekstensi.
Tentu saja, Anda juga dapat menulis metode ekstensi Anda sendiri. Sadarilah, bagaimanapun, bahwa sementara metode ekstensi tampaknya dipanggil seperti metode instan biasa, ini benar-benar hanya ilusi. Secara khusus, metode ekstensi Anda tidak dapat mereferensikan anggota pribadi atau yang dilindungi dari kelas yang mereka perluas dan oleh karena itu tidak dapat berfungsi sebagai pengganti lengkap untuk pewarisan kelas yang lebih tradisional.
Kesalahan Umum Pemrograman C# #7: Menggunakan jenis koleksi yang salah untuk tugas yang ada
C# menyediakan berbagai macam objek koleksi, dengan yang berikut ini hanya sebagian daftar:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
.
Meskipun mungkin ada kasus di mana terlalu banyak pilihan sama buruknya dengan pilihan yang tidak cukup, tidak demikian halnya dengan objek koleksi. Jumlah opsi yang tersedia pasti dapat menguntungkan Anda. Luangkan sedikit waktu ekstra di muka untuk meneliti dan memilih jenis koleksi yang optimal untuk tujuan Anda. Ini kemungkinan akan menghasilkan kinerja yang lebih baik dan lebih sedikit ruang untuk kesalahan.
Jika ada jenis koleksi yang secara khusus ditargetkan pada jenis elemen yang Anda miliki (seperti string atau bit), condongkan untuk menggunakannya terlebih dahulu. Implementasi umumnya lebih efisien bila ditargetkan ke jenis elemen tertentu.
Untuk memanfaatkan keamanan tipe C#, Anda biasanya harus memilih antarmuka generik daripada antarmuka non-generik. Elemen antarmuka generik bertipe yang Anda tentukan saat mendeklarasikan objek, sedangkan elemen antarmuka non-generik bertipe objek. Saat menggunakan antarmuka non-generik, kompiler C# tidak dapat mengetik-memeriksa kode Anda. Juga, ketika berhadapan dengan koleksi tipe nilai primitif, menggunakan koleksi non-generik akan menghasilkan tinju/unboxing berulang dari tipe tersebut, yang dapat menghasilkan dampak kinerja negatif yang signifikan jika dibandingkan dengan koleksi generik dari tipe yang sesuai.
Masalah C# umum lainnya adalah menulis objek koleksi Anda sendiri. Itu tidak berarti itu tidak pernah tepat, tetapi dengan pilihan yang komprehensif seperti yang ditawarkan .NET, Anda mungkin dapat menghemat banyak waktu dengan menggunakan atau memperluas yang sudah ada, daripada menciptakan kembali roda. Secara khusus, Pustaka Koleksi Generik C5 untuk C# dan CLI menawarkan beragam koleksi tambahan “di luar kotak”, seperti struktur data pohon persisten, antrian prioritas berbasis tumpukan, daftar larik terindeks hash, daftar tertaut, dan banyak lagi.
Kesalahan Umum Pemrograman C# #8: Mengabaikan sumber daya gratis
Lingkungan CLR menggunakan pengumpul sampah, jadi Anda tidak perlu secara eksplisit mengosongkan memori yang dibuat untuk objek apa pun. Faktanya, Anda tidak bisa. Tidak ada yang setara dengan operator delete
C++ atau fungsi free()
di C . Tapi itu tidak berarti bahwa Anda bisa melupakan semua objek setelah Anda selesai menggunakannya. Banyak jenis objek merangkum beberapa jenis sumber daya sistem lainnya (misalnya, file disk, koneksi database, soket jaringan, dll.). Membiarkan sumber daya ini terbuka dapat dengan cepat menghabiskan jumlah total sumber daya sistem, menurunkan kinerja dan pada akhirnya menyebabkan kesalahan program.
Meskipun metode destruktor dapat didefinisikan pada kelas C# mana pun, masalah dengan destruktor (juga disebut finalizer dalam C#) adalah Anda tidak dapat mengetahui dengan pasti kapan mereka akan dipanggil. Mereka dipanggil oleh pengumpul sampah (pada utas terpisah, yang dapat menyebabkan komplikasi tambahan) pada waktu yang tidak ditentukan di masa mendatang. Mencoba mengatasi batasan ini dengan memaksa pengumpulan sampah dengan GC.Collect()
bukanlah praktik terbaik C#, karena hal itu akan memblokir utas untuk waktu yang tidak diketahui saat ia mengumpulkan semua objek yang memenuhi syarat untuk pengumpulan.
Ini bukan untuk mengatakan tidak ada kegunaan yang baik untuk finalizer, tetapi membebaskan sumber daya dengan cara yang deterministik bukanlah salah satunya. Sebaliknya, saat Anda mengoperasikan file, jaringan, atau koneksi database, Anda ingin secara eksplisit membebaskan sumber daya yang mendasarinya segera setelah Anda selesai menggunakannya.
Kebocoran sumber daya menjadi perhatian di hampir semua lingkungan. Namun, C# menyediakan mekanisme yang kuat dan mudah digunakan yang, jika digunakan, dapat membuat kebocoran menjadi lebih jarang terjadi. Kerangka kerja .NET mendefinisikan antarmuka IDisposable
, yang hanya terdiri dari metode Dispose()
. Objek apa pun yang mengimplementasikan IDisposable
mengharapkan metode itu dipanggil setiap kali konsumen objek selesai memanipulasinya. Ini menghasilkan pembebasan sumber daya yang eksplisit dan deterministik.
Jika Anda membuat dan membuang objek dalam konteks blok kode tunggal, pada dasarnya tidak dapat dimaafkan untuk lupa memanggil Dispose()
, karena C# memberikan pernyataan using
yang akan memastikan Dispose()
dipanggil tidak peduli bagaimana blok kode keluar (apakah itu pengecualian, pernyataan kembali, atau hanya penutupan blok). Dan ya, itu sama using
pernyataan yang disebutkan sebelumnya yang digunakan untuk memasukkan ruang nama C# di bagian atas file Anda. Ini memiliki tujuan kedua yang sama sekali tidak terkait, yang tidak disadari oleh banyak pengembang C#; yaitu, untuk memastikan bahwa Dispose()
dipanggil pada suatu objek saat blok kode keluar:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
Dengan membuat sebuah blok using
dalam contoh di atas, Anda tahu pasti bahwa myFile.Dispose()
akan dipanggil segera setelah Anda selesai dengan file tersebut, baik Read()
melontarkan pengecualian atau tidak.
Kesalahan Umum Pemrograman C# #9: Menghindari pengecualian
C# melanjutkan penegakan keamanan tipenya ke dalam runtime. Ini memungkinkan Anda untuk menunjukkan dengan tepat banyak jenis kesalahan dalam C# jauh lebih cepat daripada dalam bahasa seperti C++, di mana konversi jenis yang salah dapat menghasilkan nilai arbitrer yang ditetapkan ke bidang objek. Namun, sekali lagi, pemrogram dapat menyia-nyiakan fitur hebat ini, yang menyebabkan masalah C#. Mereka jatuh ke dalam jebakan ini karena C# menyediakan dua cara berbeda dalam melakukan sesuatu, satu yang dapat mengeluarkan pengecualian, dan yang lainnya tidak. Beberapa akan menghindar dari rute pengecualian, dengan pertimbangan bahwa tidak harus menulis blok coba/tangkap menghemat beberapa pengkodean.
Misalnya, berikut adalah dua cara berbeda untuk melakukan pemeran tipe eksplisit di C#:
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
Kesalahan paling jelas yang dapat terjadi dengan penggunaan Metode 2 adalah kegagalan untuk memeriksa nilai kembalian. Itu kemungkinan akan menghasilkan NullReferenceException akhirnya, yang mungkin bisa muncul di lain waktu, membuatnya lebih sulit untuk melacak sumber masalahnya. Sebaliknya, Metode 1 akan segera melontarkan InvalidCastException
yang membuat sumber masalahnya jauh lebih jelas.
Selain itu, bahkan jika Anda ingat untuk memeriksa nilai pengembalian di Metode 2, apa yang akan Anda lakukan jika ternyata nol? Apakah metode yang Anda tulis merupakan tempat yang tepat untuk melaporkan kesalahan? Apakah ada hal lain yang dapat Anda coba jika pemeran itu gagal? Jika tidak, maka melempar pengecualian adalah hal yang benar untuk dilakukan, jadi Anda sebaiknya membiarkannya terjadi sedekat mungkin dengan sumber masalah.
Berikut adalah beberapa contoh pasangan metode umum lainnya di mana yang satu melempar pengecualian dan yang lainnya tidak:
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
Beberapa pengembang C# begitu "merugikan pengecualian" sehingga mereka secara otomatis menganggap metode yang tidak mengeluarkan pengecualian lebih unggul. Meskipun ada beberapa kasus tertentu di mana ini mungkin benar, itu sama sekali tidak benar sebagai generalisasi.
Sebagai contoh spesifik, dalam kasus di mana Anda memiliki alternatif tindakan yang sah (misalnya, default) untuk diambil jika pengecualian akan dibuat, maka pendekatan non-pengecualian bisa menjadi pilihan yang sah. Dalam kasus seperti itu, mungkin memang lebih baik untuk menulis sesuatu seperti ini:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
dari pada:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
Namun, tidak benar untuk menganggap bahwa TryParse
karenanya merupakan metode yang "lebih baik". Kadang begitu, kadang tidak. Itu sebabnya ada dua cara untuk melakukannya. Gunakan yang benar untuk konteks tempat Anda berada, mengingat pengecualian pasti bisa menjadi teman Anda sebagai pengembang.
Kesalahan Pemrograman C# Umum #10: Mengizinkan peringatan kompiler menumpuk
Meskipun masalah ini jelas tidak spesifik C#, ini sangat mengerikan dalam pemrograman C# karena mengabaikan manfaat dari pemeriksaan tipe ketat yang ditawarkan oleh kompiler C#.
Peringatan dibuat karena suatu alasan. Sementara semua kesalahan kompiler C# menandakan cacat pada kode Anda, banyak peringatan juga menunjukkan hal itu. Apa yang membedakan keduanya adalah bahwa, dalam hal peringatan, kompiler tidak memiliki masalah dalam memancarkan instruksi yang diwakili oleh kode Anda. Meski begitu, kode Anda agak mencurigakan, dan ada kemungkinan yang masuk akal bahwa kode Anda tidak secara akurat mencerminkan maksud Anda.
Contoh sederhana yang umum untuk tutorial pemrograman C# ini adalah ketika Anda memodifikasi algoritme untuk menghilangkan penggunaan variabel yang Anda gunakan, tetapi Anda lupa menghapus deklarasi variabel. Program akan berjalan dengan sempurna, tetapi kompilator akan menandai deklarasi variabel yang tidak berguna. Fakta bahwa program berjalan dengan sempurna menyebabkan pemrogram lalai untuk memperbaiki penyebab peringatan tersebut. Selain itu, pembuat kode memanfaatkan fitur Visual Studio yang memudahkan mereka menyembunyikan peringatan di jendela "Daftar Kesalahan" sehingga mereka hanya dapat fokus pada kesalahan. Tidak butuh waktu lama sampai ada lusinan peringatan, semuanya diabaikan dengan senang hati (atau lebih buruk lagi, disembunyikan).
Tetapi jika Anda mengabaikan jenis peringatan ini, cepat atau lambat, sesuatu seperti ini mungkin akan menemukan jalannya ke dalam kode Anda:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
Dan pada kecepatan Intellisense yang memungkinkan kita menulis kode, kesalahan ini tidak semustahil kelihatannya.
Anda sekarang memiliki kesalahan serius dalam program Anda (walaupun kompiler hanya menandainya sebagai peringatan, untuk alasan yang telah dijelaskan), dan tergantung pada seberapa kompleks program Anda, Anda dapat membuang banyak waktu untuk melacak yang satu ini. Seandainya Anda memperhatikan peringatan ini sejak awal, Anda akan menghindari masalah ini dengan perbaikan sederhana lima detik.
Ingat, kompiler C Sharp memberi Anda banyak informasi berguna tentang kekokohan kode Anda… jika Anda mendengarkan. Jangan abaikan peringatan. Mereka biasanya hanya membutuhkan beberapa detik untuk memperbaikinya, dan memperbaiki yang baru ketika itu terjadi dapat menghemat waktu Anda. Latih diri Anda untuk mengharapkan jendela "Daftar Kesalahan" Visual Studio menampilkan "0 Kesalahan, 0 Peringatan", sehingga peringatan apa pun membuat Anda tidak nyaman untuk segera mengatasinya.
Tentu saja, ada pengecualian untuk setiap aturan. Oleh karena itu, ada kalanya kode Anda akan terlihat sedikit mencurigakan bagi kompiler, meskipun persis seperti yang Anda inginkan. Dalam kasus yang sangat jarang terjadi, gunakan #pragma warning disable [warning id]
hanya di sekitar kode yang memicu peringatan, dan hanya untuk ID peringatan yang dipicu. Ini akan menekan peringatan itu, dan peringatan itu saja, sehingga Anda masih bisa tetap waspada untuk yang baru.
Bungkus
C# adalah bahasa yang kuat dan fleksibel dengan banyak mekanisme dan paradigma yang dapat sangat meningkatkan produktivitas. Namun, seperti halnya alat perangkat lunak atau bahasa apa pun, memiliki pemahaman atau apresiasi yang terbatas atas kemampuannya terkadang bisa lebih menjadi hambatan daripada manfaat, membuat seseorang dalam kondisi pepatah "cukup tahu untuk menjadi berbahaya".
Menggunakan tutorial C Sharp seperti ini untuk membiasakan diri dengan nuansa utama C#, seperti (tetapi tidak terbatas pada) masalah yang diangkat dalam artikel ini, akan membantu dalam optimasi C# sambil menghindari beberapa jebakan yang lebih umum dari bahasa.
Bacaan Lebih Lanjut di Blog Teknik Toptal:
- Pertanyaan Wawancara C# Penting
- C# vs. C++: Apa Inti?