Rust'ta Savunmacı Programlama Kalıpları
(corrode.dev)- Rust'ın tip sistemini ve derleyicisini etkin biçimde kullanarak hataları önceden engelleyen kodlama alışkanlıkları tanıtılıyor
- Vektör indeksleme,
Default'un aşırı kullanımı, eksikmatch, gereksiz boolean parametreler gibi kırılgan kod kokusu (Code Smell) örnekleri sunuluyor ve alternatifleri açıklanıyor - Temel ilke, yapıyı derleyicinin invariant'ları zorunlu kılacağı şekilde tasarlamak; bunun için pattern matching, özel alanlar ve
#[must_use]niteliği gibi araçlar kullanılıyor TryFromkullanımı, struct'ı tamamen parçalara ayırma, geçici değiştirilebilirlik, constructor doğrulaması gibi gerçek kod düzeyinde savunma teknikleri somut biçimde gösteriliyor- Bu kalıplar, refactor sırasında kararlılığı korumak ve uzun vadeli bakım yapılabilirliği artırmak için kritik önem taşıyor
Savunmacı programlamaya genel bakış
// this should never happenyorumunun bulunduğu yerler, örtük invariant'ların bozulduğu noktaları gösterir- Çoğu durumda geliştirici tüm sınır durumlarını ya da gelecekteki kod değişikliklerini hesaba katmaz
- Rust derleyicisi bellek güvenliğini garanti eder, ancak iş mantığı hataları yine de ortaya çıkabilir
- Yıllara yayılan pratik deneyimle edinilen küçük alışkanlık kalıpları (idiom) kod kalitesini büyük ölçüde artırır
Code Smell: vektör indeksleme
if !vec.is_empty() { let x = &vec[0]; }biçimi, uzunluk kontrolü ile indekslemeyi ayırdığı için çalışma zamanında panic riski taşır- Slice pattern matching (
match vec.as_slice()) kullanıldığında derleyici tüm durumların kontrol edilmesini zorunlu kılar- Boş vektör, tek öğe, yinelenen öğeler gibi tüm durumlar açıkça ele alınabilir
- Bu, derleyicinin invariant'ları garanti etmesini sağlayacak şekilde tasarlamanın tipik bir örneğidir
Code Smell: Default'un ölçüsüz kullanımı
..Default::default(), yeni alan eklendiğinde gözden kaçırma riski ve örtük değer atama sorunları doğurur- Tüm alanları açıkça ilklendirmek, derleyicinin yeni alanların ayarlanmasını zorunlu kılmasını sağlar
let Foo { field1, field2, .. } = Foo::default();biçimiyle varsayılan struct'ı parçalayıp seçici override yapmak mümkündür- Varsayılanı koruma ile açık override arasında denge kurulur
Code Smell: kırılgan trait implementasyonu
- Struct alanlarını tamamen parçalayıp karşılaştırmak, yeni alan eklendiğinde derleme hatasıyla uyarı verilmesini sağlar
- Örneğin
PartialEqimplementasyonundalet Self { size, toppings, .. } = self;
- Örneğin
extra_cheesegibi yeni bir alan eklendiğinde karşılaştırma mantığının yeniden gözden geçirilmesi zorunlu hale gelir- Aynı ilke
Hash,Debug,Clonegibi diğer trait'lere de uygulanabilir
Code Smell: From yerine TryFrom gerektiğinde
- Dönüşüm her zaman başarılı olmuyorsa
Fromyerine başarısızlık ihtimalini açık edenTryFromkullanılmalıdır unwrap_or_elsekullanımı, potansiyel başarısızlığı gizleyen bir işaret olduğundan erken başarısızlık (fail fast) yaklaşımı daha güvenlidir
Code Smell: eksik match
_ => {}gibi catch-all pattern'ler, yeni variant eklendiğinde bir durumun atlanması riskini taşır- Tüm variant'ları açıkça sıralamak, derleyicinin yeni case'lerin ele alınmadığını bildirmesini sağlar
- Aynı mantık
Variant3 | Variant4biçiminde gruplanarak da kullanılabilir
Code Smell: _ placeholder'ının aşırı kullanımı
- Yalnızca
_kullanmak, hangi değişkenin atlandığını belirsiz bırakır has_fuel: _, has_crew: _gibi açık adlarla okunabilirlik artırılabilir
Pattern: geçici değiştirilebilirlik (Temporary Mutability)
- Veri yalnızca ilklendirme sırasında değiştirilebilir olmalıysa
let mut data = ...; data.sort(); let data = data;biçimi kullanılabilir - Blok scope kullanmak, geçici değişkenin dışarı sızmasını engeller
- Örneğin
let data = { let mut d = get_vec(); d.sort(); d };
- Örneğin
- Birden fazla geçici değişken kullanılan ilklendirme sürecinde kapsamları net biçimde ayırmak mümkün olur
Pattern: constructor doğrulamasını zorunlu kılma
- Struct oluşturulurken doğrulama mantığından mutlaka geçilmesi sağlanır
_private: ()alanı eklenirse dışarıdan doğrudan oluşturma mümkün olmaz#[non_exhaustive]niteliği, crate dışından oluşturmayı engeller ve gelecekte genişleyebileceğine işaret eder
- İç modüllerde de bunu zorlamak için özel tip (Seal) içeren iç içe modül yapısı kullanılabilir
Sealyalnızca içeride var olduğundannew()dışında doğrudan oluşturma mümkün olmaz
- Alanları özel tutup getter sağlamak, değişmez durumun korunmasına yardımcı olur
- Uygulama ölçütleri
- Dış kodu engelleme:
_privateya da#[non_exhaustive] - İç kodu engelleme: özel modül +
Seal - Doğrulama mantığını derleyici düzeyinde bir güvenceye dönüştürme
- Dış kodu engelleme:
Pattern: #[must_use] niteliğinden yararlanma
#[must_use], önemli dönüş değerlerinin göz ardı edilmesini önler- Örneğin
#[must_use = "Configuration must be applied to take effect"]
- Örneğin
- Kullanıcı dönüş değerini yok sayarsa derleyici uyarısı oluşur
Resultgibi standart kütüphane türlerinde de yaygın kullanılan, basit ama güçlü bir savunma aracıdır
Code Smell: boolean parametreler
fn process_data(..., compress: bool, encrypt: bool, validate: bool)biçimi anlam belirsizliği ve sıra hatası riski taşırenum Compression,enum Encryptiongibi yapılarla niyet açık biçimde ifade edilir- Birden çok seçenek varsa parametre struct'ı (Params struct) kullanılabilir
ProcessDataParams::production()gibi ön ayar metotlarıyla yeniden kullanılabilirlik artar
- Yeni seçenek eklendiğinde mevcut çağrı noktaları en az düzeyde etkilenir
Clippy lint'leriyle otomasyon
- Temel savunmacı kalıplar Clippy lint'leriyle otomatik olarak denetlenebilir
indexing_slicing: doğrudan indekslemeyi yasaklarfallible_impl_from:FromyerineTryFromönerirwildcard_enum_match_arm:_pattern'ini yasaklarfn_params_excessive_bools: aşırı boolean parametre kullanımına uyarı verirmust_use_candidate:#[must_use]adayı önerir
#![deny(clippy::...)]ya daCargo.tomlayarlarıyla proje genelinde uygulanabilir
Sonuç
- Rust'ın tip sistemini ve derleyicisini etkin biçimde kullanarak invariant'ları açık, doğrulanabilir hale getirmek savunmacı programlamanın özüdür
- Bu kalıplar, refactor sırasında kararlılığı korumaya, hata olasılığını en aza indirmeye ve uzun vadeli bakım yapılabilirliği güçlendirmeye katkı sağlar
- Bu yaklaşım, “derlenmeyen bug en iyi bug'dır” ilkesini hayata geçirir
1 yorum
Hacker News yorumu
Yazı güzeldi. Ancak PizzaOrder örneği, çok fazla sorumluluğu tek bir struct içinde toplamış gibi hissettiriyor
Amaç
ordered_atalanını karşılaştırmanın dışında bırakmaksa, bunuPizzaDetailsvePizzaOrderdiye iki struct'a ayırmanın daha iyi olduğunu düşünüyorumBöylece
PartialEquygulanırken yalnızcadetailsalanının karşılaştırılacağı açıkça ifade edilebilirSipariş zamanı farklıysa aynı sipariş değildir; bu yüzden tür seviyesinde eşit olduklarını tanımlamak risklidir
PizzaDetailsiçinPartialEqolması sorun değil, ama sipariş karşılaştırma mantığı ayrı bir iş kuralı fonksiyonunda durmalıPizzaDetailsüzerinde yapılacak değişikliklerin pizza tekrarlarını ayıklama mantığını etkileyebilmesi sorunStruct'ları ideal olarak yalnızca veriyi gruplamak için kullanmak gerekir
Değişikliklerin başka yerleri etkilememesi için
PizzaComparatorveyaPizzaFlavorgibi ayrı türler düşünmek de mümkünProtobuf'taki gibi alanlara
{important_to_flavour=true}benzeri alan notları koyabilsek güzel olurduÖrneğin bir string'i büyük/küçük harf duyarsız karşılaştırmak istesek bunu nasıl ayıracaksınız?
Rust'taki gerçekten harika şey, çoğu durumda savunmacı programlamaya ihtiyaç olmaması
Sahiplik ve referans kuralları sayesinde, belirli bir nesneye erişimin program genelinde tekil olduğu garanti edilebiliyor
Referanslar null olamaz, akıllı işaretçiler de null olamaz
selfsahipliğini devrederseniz sonrasında metot çağrısı yapılamayacağını da tür sistemi garanti ediyorBu sayede thread güvenliği, ömürler ve kopyalanabilirlik gibi şeyler derleme zamanında küresel olarak doğrulanıyor
Diğer dillerde ancak fonksiyonel tarzda değişmezliği koruyarak elde edilen faydaları Rust tür sistemiyle zorunlu kılıyor
Makalenin konusu borrow checker'ın bile yakalayamadığı mantıksal hatalardı
Dizi veya vektörlerde doğrudan indeksleme yapmaktan kaçınmanın akıllıca olduğunu düşünüyorum
Cloudflare'in unwrap olayı yaşandığı gün ben de bir slice'ın vektör sonunu aştığı bir hatayı bulmuştum
Sonrasında iterator tabanlı erişime geçtim ve çok daha güvenli hissettirdi
Rust'taki
unwrap, C'dekiassertile aynı şeydir. Başarısız olduğunda sadece sorunu görünür kılarRust'ta da hâlâ hata yazabilirsiniz
Rust geliştiricilerinin kaçınması gereken alışkanlıklardan biri de gereksiz crate bağımlılıkları eklemek
Rust'ın bunu teşvik etme eğilimi var. Örneğin Rust Book'ta temel örneklerde
randcrate'inin kullanılması da böyle bir hava oluşturuyorElbette bu, kriptoyla ilgili paketlerin kolayca değiştirilebilmesini sağlamak için alınmış stratejik bir karardı; ama yine de bunun alışkanlığa dönüşmesi sorun
Ama sonradan niyetini anlayınca fikrim değişti
Kısmi eşitlik uygulaması ilginçti
Merak ettiğim bir başka şey de boolean parametrelerden kaçınırken enum kullanma yaklaşımı
Ben bool'u saran bir struct kullanıyorum ama bunun normal bool gibi davranmaması biraz can sıkıcı
Enum'u bool gibi kullanmanın bir yolu var mı diye merak ediyorum
Gerekli mantığı bir Trait altında toplayabiliyor ya da
impl <Enum>bloğuna ortak metotlar ekleyebiliyorsunuzBöylece hem okunabilirlik artıyor hem de her üyenin davranışı açıkça tanımlanabiliyor
impl Derefgibi bir şey denenebilir ama bunun iyi bir fikir olup olmadığından emin değilimİlk örnekteki
matchifadesi fazla abartılı geliyorVec.first()veyaVec.iter().nth(0)daha açık ve niyete daha uygunmatchkullanınca sorundan daha karmaşık bir çözüm ortaya çıkıyorifkaldırılabiliyorsamatchde kaldırılabilir; güvenlik açısından arada fark yokfirst()çok daha kısa ve netmatch, en az bir öğe bulunması durumunu da ele almaya zorlaması açısından anlamlıYani kontrol ile ona bağımlı kodu ayırmaktan kaçının ilkesini görünür kılıyor
Böyle yazılar okudukça neden kod kalıplarını izleyen özel bir ekip olmadığını merak ediyorum
SOC veya QA gibi, kod tabanındaki kalıpları uzun vadede gözlemleyen bir ekip faydalı olurdu
Otomatik code smell tespit araçlarının sınırları var
Lint kurallarını yönetiyor, dokümantasyon hazırlıyor, geliştirici eğitimi veriyor ve ortak kütüphanelerin bakımını yapıyorlar
Birden fazla ekip aynı sorunu tekrar ettiğinde bunu birleştirecek çekirdek API tasarlıyorlar
Ama kod tabanı milyonlarca satıra ulaşınca yönetmek gerçekten çok zorlaşıyor
Takım içinde bu tür iyi kodlama kalıplarını nasıl teşvik edebileceğimizi düşünüyorum
Kod incelemelerinde bu konu sık sık “stil tartışmasına” dönüşüp verimsizleşebiliyor
Ama ilginç biçimde, linter uyarı verdiğinde bu tartışmalar neredeyse tamamen ortadan kalkıyor
TryFromtrait'inin 1.34 sürümünde eklenmesi gerçekten çok faydalıydıMuhtemelen
unwrap_or_else()kullanan kodlar ondan önceki dönemin kalıntısıFrom trait belgeleri artık bunun ne zaman uygulanması gerektiğini çok net açıklıyor
unwrap_or_else()adı kulağıma sanki “bilgisayarı tehdit ederek emir veriyormuşsunuz” gibi komik geliyorBu tür savunmacı programlama kalıplarının, büyük ölçekli yapay zeka ile kod üretimi kalitesini artırmada da işe yarayacağını düşünüyorum
Clippy ve Rust derleyicisinin verdiği somut geri bildirimler, yapay zeka ajanlarının hata yapmasını azaltıp doğru yöne gitmesinde büyük rol oynayabilir