Pengetikan yang benar: aspek kode bersih yang diremehkan

Halo kolega.

Belum lama ini, perhatian kami tertarik pada buku yang hampir selesai oleh Manning Publishing House "Programming with types", yang merinci pentingnya mengetik yang tepat dan perannya dalam menulis kode yang bersih dan tahan lama.



Pada saat yang sama, di blog penulis, kami menemukan sebuah artikel yang ditulis, tampaknya, pada tahap awal pengerjaan buku dan memungkinkan untuk membuat kesan materi. Kami menyarankan untuk mendiskusikan seberapa menarik ide penulis dan, berpotensi, keseluruhan buku

Pengorbit iklim Mars

Pesawat ruang angkasa Mars Climate Orbiter jatuh selama pendaratan dan hancur berantakan di atmosfer Mars, karena komponen perangkat lunak Lockheed memberikan nilai momentum, diukur dalam pound-force dt., Sedangkan komponen lain yang dikembangkan oleh NASA mengambil nilai momentum dalam Newtons- detik

Anda dapat membayangkan komponen yang dikembangkan oleh NASA dalam kira-kira bentuk berikut:

//    ,  >= 2 N s void trajectory_correction(double momentum) { if (momentum < 2 /* N s */) { disintegrate(); } /* ... */ } 

Anda juga dapat membayangkan bahwa komponen Lockheed menyebut kode di atas seperti ini:

 void main() { trajectory_correction(1.5 /* lbf s */); } 

Pound-force-second (lbfs) adalah sekitar 4,448222 newton per detik (Ns). Dengan demikian, dari sudut pandang Lockheed, melewatkan 1,5 lbfs ke trajectory_correction harus normal: 1,5 lbfs sekitar 6,672333 Ns, jauh di atas ambang 2 Ns.

Masalahnya adalah interpretasi data. Akibatnya, komponen NASA membandingkan lbfs dengan Ns tanpa konversi dan secara keliru menginterpretasikan input ke lbfs sebagai input ke Ns. Karena 1,5 kurang dari 2, pengorbit runtuh. Ini adalah antipattern terkenal yang disebut obsesi primitif.

Obsesi dengan primitif

Fiksasi pada primitif memanifestasikan dirinya ketika kita menggunakan tipe data primitif untuk mewakili nilai dalam domain masalah dan memungkinkan situasi seperti yang dijelaskan di atas. Jika Anda menyatakan kode pos sebagai angka, nomor telepon sebagai string, Ns dan lbfs sebagai angka presisi ganda, inilah yang terjadi.

Akan jauh lebih aman untuk mendefinisikan tipe Ns sederhana:

 struct Ns { double value; }; bool operator<(const Ns& a, const Ns& b) { return a.value < b.value; } 

Demikian pula, Anda dapat mendefinisikan tipe lbfs sederhana:

 struct lbfs { double value; }; bool operator<(const lbfs& a, const lbfs& b) { return a.value < b.value; } 

Sekarang Anda dapat menerapkan varian trajectory_correction jenis-aman:

 //  ,   >= 2 N s void trajectory_correction(Ns momentum) { if (momentum < Ns{ 2 }) { disintegrate(); } /* ... */ } 

Jika Anda menyebutnya dengan lbfs , seperti pada contoh di atas, maka kode tersebut tidak dapat dikompilasi karena ketidakcocokan jenis:

 void main() { trajectory_correction(lbfs{ 1.5 }); } 

Perhatikan bagaimana informasi tipe nilai, yang biasanya ditunjukkan dalam komentar, ( 2 /*Ns */, /* lbfs */ ) sekarang ditarik ke dalam sistem tipe dan dinyatakan dalam kode: ( Ns{ 2 }, lbfs{ 1.5 } ) .

Tentu saja, dimungkinkan untuk memberikan pengurangan lbfs menjadi Ns dalam bentuk operator eksplisit:

 struct lbfs { double value; explicit operator Ns() { return value * 4.448222; } }; 

Berbekal teknik ini, Anda dapat memanggil trajectory_correction menggunakan gips statis:

 void main() { trajectory_correction(static_cast<Ns>(lbfs{ 1.5 })); } 

Di sini kebenaran kode dicapai dengan mengalikan dengan koefisien. Pemain juga dapat dilakukan secara implisit (menggunakan kata kunci implisit), dalam hal ini para pemain akan diterapkan secara otomatis. Sebagai aturan empiris, Anda bisa menggunakan salah satu dari Python coans di sini:
Eksplisit lebih baik daripada implisit
Moral dari cerita ini adalah bahwa, meskipun hari ini kita memiliki mekanisme pemeriksaan tipe yang sangat cerdas, mereka masih perlu memberikan informasi yang cukup untuk menangkap kesalahan jenis ini. Informasi ini masuk ke dalam program jika kami mendeklarasikan tipe-tipe dengan mempertimbangkan secara spesifik area subjek kami.

Ruang negara

Masalah terjadi ketika suatu program berakhir dalam keadaan buruk . Jenis membantu mempersempit bidang untuk terjadinya mereka. Mari kita coba memperlakukan jenis sebagai set nilai yang mungkin. Misalnya, bool adalah himpunan {true, false} , di mana variabel jenis ini dapat mengambil salah satu dari dua nilai ini. Demikian pula, uint32_t adalah himpunan {0 ...4294967295} . Mempertimbangkan tipe-tipe dengan cara ini, kita dapat mendefinisikan ruang keadaan dari program kita sebagai produk dari tipe semua variabel hidup pada titik waktu tertentu.

Jika kita memiliki variabel tipe bool dan variabel tipe uint32_t , maka ruang keadaan kita akan menjadi {true, false} X {0 ...4294967295} . Ini hanya berarti bahwa kedua variabel dapat dalam keadaan apa pun yang mungkin untuk mereka, dan karena kami memiliki dua variabel, program dapat berakhir dalam keadaan gabungan dari kedua jenis ini.

Semuanya menjadi jauh lebih menarik jika kita mempertimbangkan fungsi yang menginisialisasi nilai:

 bool get_momentum(Ns& momentum) { if (!some_condition()) return false; momentum = Ns{ 3 }; return true; } 

Dalam contoh di atas, kita mengambil Ns dengan referensi dan menginisialisasi jika beberapa kondisi terpenuhi. Fungsi mengembalikan true jika nilai diinisialisasi dengan benar. Jika fungsi karena alasan tertentu tidak dapat mengatur nilai, maka itu mengembalikan false .

Mempertimbangkan situasi ini dari sudut pandang ruang keadaan, kita dapat mengatakan bahwa ruang keadaan adalah produk bool X Ns . Jika fungsi mengembalikan true, itu berarti bahwa impuls telah diatur, dan merupakan salah satu nilai yang mungkin dari Ns . Masalahnya adalah ini: jika fungsi mengembalikan false , itu berarti bahwa impuls tidak disetel. Salah satu cara atau yang lain, momentum milik set nilai yang mungkin dari Ns, tetapi itu bukan nilai yang valid. Seringkali ada bug di mana keadaan yang tidak dapat diterima berikut secara tidak sengaja mulai menyebar:

 void example() { Ns momenum; get_momentum(momentum); trajectory_correction(momentum); } 

Sebaliknya, kita hanya perlu melakukan ini:

 void example() { Ns momentum; if (get_momentum(momentum)) { trajectory_correction(momentum); } } 

Namun, ada cara yang lebih baik untuk melakukan ini secara paksa:

 std::optional<Ns> get_momentum() { if (!some_condition()) return std::nullopt; return std::make_optional(Ns{ 3 }); } 

Jika Anda menggunakan optional , maka ruang status fungsi ini akan berkurang secara signifikan: alih-alih bool X Ns kami mendapatkan Ns + 1 . Fungsi ini akan mengembalikan nilai Ns atau nullopt valid untuk menunjukkan tidak ada nilai. Sekarang, kita tidak bisa memiliki Ns yang tidak valid yang akan menyebar di sistem. Juga sekarang menjadi tidak mungkin untuk lupa memeriksa nilai kembali, karena opsional tidak dapat secara implisit dikonversi ke Ns - kita perlu membongkar secara khusus:

 void example() { auto maybeMomentum = get_momentum(); if (maybeMomentum) { trajectory_correction(*maybeMomentum); } } 

Pada dasarnya, kami berupaya agar fungsi kami mengembalikan hasil atau kesalahan, bukan hasil dan kesalahan. Dengan demikian, kami mengecualikan kondisi di mana kami memiliki kesalahan, dan juga kami aman dari hasil yang tidak dapat diterima, yang kemudian dapat bocor ke perhitungan lebih lanjut.

Dari sudut pandang ini, melempar pengecualian adalah normal, karena sesuai dengan prinsip yang dijelaskan di atas: suatu fungsi akan mengembalikan hasil atau melempar pengecualian.

RAII

RAII berarti Akuisisi Sumber Daya Adalah Inisialisasi, tetapi pada tingkat yang lebih besar prinsip ini dikaitkan dengan pelepasan sumber daya. Nama pertama kali muncul di C ++, namun pola ini dapat diimplementasikan dalam bahasa apa pun (lihat, misalnya, IDisposable dari .NET). RAII menyediakan pembersihan sumber daya otomatis.

Apa itu sumber daya? Berikut adalah beberapa contoh: memori dinamis, koneksi basis data, deskriptor OS. Pada prinsipnya, sumber daya adalah sesuatu yang diambil dari dunia luar dan dapat kembali setelah kita tidak lagi membutuhkannya. Kami mengembalikan sumber daya menggunakan operasi yang sesuai: lepaskan, hapus, tutup, dll.

Karena sumber daya ini bersifat eksternal, mereka tidak secara eksplisit dinyatakan dalam sistem tipe kami. Sebagai contoh, jika kita memilih sebuah fragmen memori dinamis, kita akan mendapatkan sebuah pointer dimana kita harus memanggil delete :

 struct Foo {}; void example() { Foo* foo = new Foo(); /*  foo */ delete foo; } 

Tetapi apa yang terjadi jika kita lupa melakukan ini, atau apakah ada yang menghentikan kita dari panggilan delete ?

 void example() { Foo* foo = new Foo(); throw std::exception(); delete foo; } 

Dalam hal ini, kami tidak lagi memanggil delete dan mendapatkan sumber kebocoran. Pada prinsipnya, pembersihan sumber daya secara manual seperti itu tidak diinginkan. Untuk memori dinamis, kami memiliki unique_ptr untuk membantu kami mengaturnya:

 void example() { auto foo = std::make_unique<Foo>(); throw std::exception(); } 

unique_ptr kami adalah objek tumpukan, oleh karena itu, jika ia unique_ptr ruang lingkup (ketika fungsi melempar pengecualian atau ketika tumpukan membongkar ketika pengecualian dilemparkan), destruktornya disebut. Destructor inilah yang mengimplementasikan panggilan delete . Karenanya, kami tidak lagi harus mengelola sumber daya memori - kami mentransfer karya ini ke pembungkus, yang memiliki dan bertanggung jawab atas pelepasannya.

Pembungkus serupa ada (atau dapat dibuat) untuk sumber daya lainnya (misalnya, OS HANDLE dari Windows dapat dibungkus dalam suatu jenis, dalam hal ini destruktornya akan memanggil CloseHandle ).

Kesimpulan utama dalam kasus ini adalah tidak pernah melakukan pembersihan sumber daya secara manual; Baik menggunakan pembungkus yang ada, atau jika tidak ada pembungkus yang cocok untuk skenario spesifik Anda, kami akan menerapkannya sendiri.

Kesimpulan

Kami memulai artikel ini dengan contoh terkenal yang menunjukkan pentingnya mengetik, dan kemudian memeriksa tiga aspek penting dari penggunaan jenis untuk membantu menulis kode yang lebih aman:

  • Mendeklarasikan dan menggunakan tipe yang lebih kuat (sebagai lawan obsesi dengan primitif).
  • Mengurangi ruang keadaan, mengembalikan hasil atau kesalahan, bukan hasil atau kesalahan.
  • RAII dan manajemen sumber daya otomatis.

Jadi, tipe sangat membantu untuk membuat kode lebih aman dan menyesuaikannya untuk digunakan kembali.

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


All Articles