7 kesalahan berbahaya yang mudah dilakukan di C # /. NET

Terjemahan artikel disiapkan sebelum dimulainya kursus "C # ASP.NET Core Developer" .




C # adalah bahasa yang bagus , dan .NET Framework juga sangat bagus. Mengetik dengan kuat di C # membantu mengurangi jumlah kesalahan yang dapat Anda lakukan, dibandingkan dengan bahasa lain. Plus, desain intuitif umumnya juga banyak membantu, dibandingkan dengan sesuatu seperti JavaScript (di mana benar salah ). Namun, setiap bahasa memiliki rake sendiri yang mudah digunakan, bersama dengan ide-ide keliru tentang perilaku yang diharapkan dari bahasa dan infrastruktur. Saya akan mencoba menjelaskan beberapa kesalahan ini secara rinci.

1. Tidak mengerti eksekusi yang tertunda (malas)


Saya percaya bahwa pengembang yang berpengalaman menyadari mekanisme .NET ini, tetapi mungkin mengejutkan kolega yang kurang berpengetahuan. Singkatnya, metode / operator yang kembali IEnumerable<T>dan digunakan yielduntuk mengembalikan setiap hasil tidak dieksekusi dalam baris kode yang benar-benar memanggil mereka - mereka dieksekusi ketika koleksi yang dihasilkan diakses dalam beberapa cara *. Perhatikan bahwa sebagian besar ekspresi LINQ akhirnya mengembalikan hasilnya dengan hasil .

Sebagai contoh, pertimbangkan uji unit mengerikan di bawah ini.

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable<string> RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

Kedua tes ini akan gagal. Tes pertama akan gagal, karena hasilnya tidak digunakan di mana pun, sehingga tubuh metode tidak akan pernah dieksekusi. Tes kedua akan gagal karena alasan lain, sedikit lebih non-sepele. Sekarang kita mendapatkan hasil pertama memanggil metode kita untuk memastikan bahwa metode itu benar-benar berjalan. Namun, mekanisme eksekusi yang tertunda akan keluar dari metode secepatnya - dalam kasus ini, kami hanya menggunakan elemen pertama, oleh karena itu, segera setelah kami melewati iterasi pertama, metode tersebut menghentikan eksekusi (karena itu saya == 3 tidak akan pernah benar).

Eksekusi yang tertunda sebenarnya merupakan mekanisme yang menarik, terutama karena membuatnya mudah untuk mem-chain query LINQ, mengambil data hanya ketika kueri Anda siap digunakan.

2. , Dictionary ,


Ini sangat tidak menyenangkan, dan saya yakin di suatu tempat saya memiliki kode yang bergantung pada asumsi ini. Ketika Anda menambahkan item ke daftar List<T>, mereka disimpan dalam urutan yang sama di mana Anda menambahkannya - secara logis. Terkadang Anda perlu memiliki objek lain yang terkait dengan item dalam daftar, dan solusi yang jelas adalah menggunakan kamus Dictionary<TKey,TValue>yang memungkinkan Anda menentukan nilai terkait untuk kunci tersebut.

Kemudian Anda dapat beralih ke kamus menggunakan foreach, dan dalam kebanyakan kasus akan berperilaku seperti yang diharapkan - Anda akan mengakses elemen dalam urutan yang sama di mana mereka ditambahkan ke kamus. Namun, perilaku ini tidak terdefinisi - yaitu ini adalah kebetulan yang menyenangkan, bukan sesuatu yang bisa Anda andalkan dan selalu harapkan. Ini dijelaskan dalamDokumentasi Microsoft , tetapi saya pikir beberapa orang telah mempelajari halaman ini dengan seksama.

Untuk menggambarkan hal ini, dalam contoh di bawah ini, outputnya adalah sebagai berikut:

ketiga
detik


var dict = new Dictionary<string, object>();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

Jangan percaya padaku? Periksa sendiri di sini secara online .

3. Jangan memperhitungkan keamanan aliran akun


Multithreading bagus, jika diterapkan dengan benar, Anda dapat secara signifikan meningkatkan kinerja aplikasi Anda. Namun, segera setelah Anda memasukkan multithreading, Anda harus sangat, sangat berhati-hati dengan objek yang akan Anda modifikasi, karena Anda mungkin mulai menemukan kesalahan yang tampaknya acak jika Anda tidak cukup berhati-hati.

Sederhananya, banyak kelas dasar di perpustakaan .NET tidak aman utas.- Ini berarti bahwa Microsoft tidak membuat jaminan bahwa Anda dapat menggunakan kelas ini secara paralel menggunakan beberapa utas. Ini tidak akan menjadi masalah besar jika Anda dapat segera menemukan masalah yang terkait dengan ini, tetapi sifat multithreading menyiratkan bahwa setiap masalah yang muncul sangat tidak stabil dan tidak dapat diprediksi - kemungkinan besar, tidak ada dua eksekusi yang akan menghasilkan hasil yang sama.

Sebagai contoh, pertimbangkan blok kode ini yang menggunakan sederhana, tetapi tidak aman-thread List<T>,.

var items = new List<int>();
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

Jadi, kami menambahkan angka dari 0 hingga 4 ke daftar masing-masing 10.000 kali, yang berarti bahwa daftar tersebut pada akhirnya harus berisi 50.000 elemen. Haruskah saya? Ya, ada kemungkinan kecil bahwa pada akhirnya akan terjadi - tetapi di bawah ini adalah hasil dari 5 peluncuran saya yang berbeda:

28191
23536
44346
40007
40476

Anda dapat memeriksanya sendiri online di sini .

Faktanya, ini karena metode Add bukan atomik, yang menyiratkan bahwa utas dapat mengganggu metode tersebut, yang pada akhirnya dapat mengubah ukuran array sementara utas lain sedang dalam proses menambahkan, atau menambahkan elemen dengan indeks yang sama sebagai utas lainnya. Pengecualian IndexOutOfRange datang kepada saya beberapa kali, mungkin karena ukuran array berubah selama penambahan itu. Jadi apa yang kita lakukan di sini? Kami dapat menggunakan kata kunci kunci untuk memastikan bahwa hanya satu utas yang dapat menambahkan (Tambahkan) item ke daftar pada satu waktu, tetapi ini dapat secara signifikan mempengaruhi kinerja. Microsoft, sebagai orang baik, menyediakan beberapa koleksi hebat ituMereka aman dan sangat dioptimalkan dalam hal kinerja. Saya sudah menerbitkan artikel yang menjelaskan bagaimana Anda bisa menggunakannya .

4. Pelanggaran pemalas (ditangguhkan) memuat dalam LINQ


Pemuatan malas adalah fitur hebat untuk LINQ ke SQL dan LINQ ke Entitas (Kerangka Entitas), yang memungkinkan Anda memuat baris tabel terkait sesuai kebutuhan. Dalam salah satu proyek saya yang lain, saya memiliki tabel "Modul" dan tabel "Hasil" dengan hubungan satu-ke-banyak (modul dapat memiliki banyak hasil).



Ketika saya ingin mendapatkan modul tertentu, saya tentu tidak ingin Kerangka Entitas mengembalikan setiap Hasil yang dimiliki tabel Modul! Karena itu, ia cukup pintar untuk mengeksekusi kueri untuk mendapatkan hasil hanya ketika saya membutuhkannya. Dengan demikian, kode di bawah ini akan menjalankan 2 pertanyaan - satu untuk mendapatkan modul, dan yang lainnya untuk mendapatkan hasil (untuk setiap modul),

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

Namun, bagaimana jika saya memiliki ratusan modul? Ini berarti bahwa permintaan SQL terpisah untuk menerima catatan hasil akan dieksekusi untuk setiap modul! Jelas, ini akan membuat server Anda tegang dan secara signifikan memperlambat aplikasi Anda. Dalam Kerangka Entitas, jawabannya sangat sederhana - Anda bisa menentukan bahwa itu menyertakan serangkaian hasil tertentu dalam permintaan Anda. Lihat kode yang dimodifikasi di bawah ini, di mana hanya satu query SQL yang akan dieksekusi, yang akan mencakup setiap modul dan setiap hasil untuk modul ini (digabungkan menjadi satu query, yang ditampilkan oleh Entity Framework secara cerdas dalam model Anda),

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

5. Tidak mengerti bagaimana LINQ ke SQL / Entity Frameworks menerjemahkan kueri


Karena kami menyentuh pada topik LINQ, saya pikir layak menyebutkan betapa berbedanya kode Anda akan dieksekusi jika ada di dalam permintaan LINQ. Menjelaskan pada tingkat tinggi, semua kode Anda di dalam kueri LINQ diterjemahkan ke dalam SQL menggunakan ekspresi - ini tampak jelas, tetapi sangat, sangat mudah untuk melupakan konteks tempat Anda berada dan pada akhirnya menimbulkan masalah ke dalam basis kode Anda. Di bawah ini saya telah menyusun daftar untuk menggambarkan beberapa kendala khas yang mungkin Anda temui.

Sebagian besar panggilan metode tidak akan berfungsi.

Jadi, bayangkan Anda memiliki kueri di bawah ini untuk memisahkan nama semua modul dengan titik dua dan menangkap bagian kedua.

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

Anda akan mendapatkan pengecualian di sebagian besar penyedia LINQ - tidak ada terjemahan SQL untuk metode Split, beberapa metode mungkin didukung, misalnya, menambahkan hari ke tanggal, tetapi semuanya tergantung pada penyedia Anda.

Mereka yang bekerja mungkin menghasilkan hasil yang tidak terduga ...

Ambil ungkapan LINQ di bawah ini (saya tidak tahu mengapa Anda akan melakukan ini dalam praktek, tapi tolong bayangkan bahwa ini adalah permintaan yang masuk akal).

int modules = db.Modules.Sum(a => a.ID);

Jika Anda memiliki baris dalam tabel modul, itu akan memberi Anda jumlah pengidentifikasi. Kedengarannya benar! Tapi bagaimana jika Anda menjalankannya menggunakan LINQ to Objects? Kita dapat melakukan ini dengan mengubah koleksi modul menjadi daftar sebelum kita menjalankan metode Sum kita.

int modules = db.Modules.ToList().Sum(a => a.ID);

Shock, horor - ini akan melakukan hal yang persis sama! Namun, bagaimana jika Anda tidak memiliki baris dalam tabel modul? LINQ ke Objects mengembalikan 0, dan Entity Framework / LINQ ke versi SQL melempar InvalidOperationException , yang mengatakan bahwa itu tidak dapat mengkonversi "int?" di "int" ... seperti itu. Ini karena ketika Anda menjalankan SUM dalam SQL untuk set kosong, NULL dikembalikan bukan 0 - karena itu, alih-alih mencoba mengembalikan int nullable. Berikut adalah beberapa tips untuk memperbaikinya jika Anda menghadapi masalah seperti itu .

Tahu kapan Anda hanya perlu menggunakan SQL lama yang bagus.

Jika Anda menjalankan kueri yang sangat kompleks, maka kueri yang diterjemahkan mungkin berakhir seperti sesuatu yang dimuntahkan, dimakan berulang-ulang. Sayangnya, saya tidak punya contoh untuk ditunjukkan, tetapi dilihat dari pendapat yang berlaku, saya sangat suka menggunakan pandangan bersarang, yang membuat pemeliharaan kode mimpi buruk.

Juga, jika Anda mengalami hambatan kinerja, akan sulit bagi Anda untuk memperbaikinya, karena Anda tidak memiliki kontrol langsung atas SQL yang dihasilkan. Baik melakukannya dalam SQL, atau mendelegasikannya ke administrator database, jika Anda atau perusahaan Anda memilikinya!

6. Pembulatan yang salah


Sekarang tentang sesuatu yang sedikit lebih sederhana dari paragraf sebelumnya, tetapi saya selalu lupa tentang hal itu, dan berakhir dengan kesalahan yang tidak menyenangkan (dan, jika itu terkait dengan keuangan, seorang direktur sirip / gen yang marah).

NET Framework. Termasuk metode statis yang sangat baik di kelas Matematika yang disebut Round , yang mengambil nilai numerik dan membulatkannya ke tempat desimal yang ditentukan. Ini berfungsi sempurna sebagian besar waktu, tetapi apa yang harus dilakukan ketika Anda mencoba untuk membulatkan 2,25 ke tempat desimal pertama? Saya berasumsi bahwa Anda mungkin berharap untuk membulatkan ke 2.3 - itulah yang kita semua terbiasa, kan? Nah, dalam prakteknya, ternyata .NET menggunakan pembulatan bankiryang membulatkan contoh yang diberikan ke 2.2! Ini disebabkan oleh fakta bahwa para bankir dibulatkan ke angka genap terdekat jika angka itu ada di “titik tengah”. Untungnya, ini dapat dengan mudah diganti dalam metode Math.Round.

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7. Kelas mengerikan 'DBNull'


Ini dapat menyebabkan kenangan yang tidak menyenangkan bagi sebagian orang - ORM menyembunyikan kekotoran ini dari kami, tetapi jika Anda mempelajari dunia telanjang ADO.NET (SqlDataReader dan sejenisnya) Anda akan bertemu dengan DBNull.Value.

Saya tidak 100% yakin alasan mengapaNilai NULL dari database diproses sebagai berikut (tolong beri komentar di bawah jika Anda tahu!), Tetapi Microsoft memutuskan untuk menyajikannya dengan tipe khusus DBNull (dengan Nilai bidang statis). Saya dapat memberikan salah satu kelebihannya - Anda tidak akan mendapatkan NullReferenceException yang tidak menyenangkan saat mengakses bidang database yang NULL. Namun, Anda seharusnya tidak hanya mendukung cara sekunder memeriksa nilai NULL (yang mudah dilupakan, yang dapat menyebabkan kesalahan serius), tetapi Anda kehilangan salah satu fitur hebat C # yang membantu Anda bekerja dengan nol. Apa yang bisa sesederhana itu

reader.GetString(0) ?? "NULL";

apa yang akhirnya menjadi ...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

Ugh.

Catatan


Ini hanya beberapa "garu" non-sepele yang saya temui di .NET - jika Anda tahu lebih banyak, saya ingin mendengar dari Anda di bawah ini.



ASP.NET Core:



All Articles