32 puan yazan GN⁺ 2025-12-07 | 1 yorum | WhatsApp'ta paylaş
  • 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ı, eksik match, 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
  • TryFrom kullanı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 happen yorumunun 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
    Reklam

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 PartialEq implementasyonunda let Self { size, toppings, .. } = self;
  • extra_cheese gibi yeni bir alan eklendiğinde karşılaştırma mantığının yeniden gözden geçirilmesi zorunlu hale gelir
  • Aynı ilke Hash, Debug, Clone gibi diğer trait'lere de uygulanabilir

Code Smell: From yerine TryFrom gerektiğinde

  • Dönüşüm her zaman başarılı olmuyorsa From yerine başarısızlık ihtimalini açık eden TryFrom kullanılmalıdır
  • unwrap_or_else kullanı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 | Variant4 biç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 };
    Reklam
  • 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
    • Seal yalnızca içeride var olduğundan new() 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: _private ya da #[non_exhaustive]
    • İç kodu engelleme: özel modül + Seal
    • Doğrulama mantığını derleyici düzeyinde bir güvenceye dönüştürme

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"]
    Reklam
  • Kullanıcı dönüş değerini yok sayarsa derleyici uyarısı oluşur
  • Result gibi 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şır
  • enum Compression, enum Encryption gibi 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 yasaklar
    • fallible_impl_from: From yerine TryFrom önerir
    • wildcard_enum_match_arm: _ pattern'ini yasaklar
    • fn_params_excessive_bools: aşırı boolean parametre kullanımına uyarı verir
    • must_use_candidate: #[must_use] adayı önerir
  • #![deny(clippy::...)] ya da Cargo.toml ayarları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

 
GN⁺ 2025-12-07
Hacker News yorumu
  • Yazı güzeldi. Ancak PizzaOrder örneği, çok fazla sorumluluğu tek bir struct içinde toplamış gibi hissettiriyor
    Amaç ordered_at alanını karşılaştırmanın dışında bırakmaksa, bunu PizzaDetails ve PizzaOrder diye iki struct'a ayırmanın daha iyi olduğunu düşünüyorum
    Böylece PartialEq uygulanırken yalnızca details alanının karşılaştırılacağı açıkça ifade edilebilir

    • İyi bir nokta. Ama yine de mantıksal olarak hatalı bir modelleme olduğunu düşünüyorum
      Sipariş zamanı farklıysa aynı sipariş değildir; bu yüzden tür seviyesinde eşit olduklarını tanımlamak risklidir
      PizzaDetails için PartialEq olması sorun değil, ama sipariş karşılaştırma mantığı ayrı bir iş kuralı fonksiyonunda durmalı
    • Yapıyı bölme yaklaşımı iyi, ama PizzaDetails üzerinde yapılacak değişikliklerin pizza tekrarlarını ayıklama mantığını etkileyebilmesi sorun
      Struct'ları ideal olarak yalnızca veriyi gruplamak için kullanmak gerekir
      Değişikliklerin başka yerleri etkilememesi için PizzaComparator veya PizzaFlavor gibi ayrı türler düşünmek de mümkün
      Protobuf'taki gibi alanlara {important_to_flavour=true} benzeri alan notları koyabilsek güzel olurdu
    • Sırf farklı bir karşılaştırma biçimi için yapıyı bölmek genellenebilir değil
      Ö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
    self sahipliğini devrederseniz sonrasında metot çağrısı yapılamayacağını da tür sistemi garanti ediyor
    Bu sayede thread güvenliği, ömürler ve kopyalanabilirlik gibi şeyler derleme zamanında küresel olarak doğrulanıyor

    • Ben de Rust'ın asıl gücünün “düşünmek zorunda olmadığınız şeylerde” olduğunu düşünüyorum
      Diğer dillerde ancak fonksiyonel tarzda değişmezliği koruyarak elde edilen faydaları Rust tür sistemiyle zorunlu kılıyor
    • Ama bu yorumun asıl makaleyle pek ilgili görünmediğini düşünüyorum
      Makalenin konusu borrow checker'ın bile yakalayamadığı mantıksal hatalar
    • Makale daha çok, programı tekrar tekrar iyileştirirken mantıksal hatalardan kaçınmaya yarayan kodlama kalıplarına odaklanıyordu
  • 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

    • unwrap olayını bir “kaza” olarak görmek gerekmediğini düşünüyorum
      Rust'taki unwrap, C'deki assert ile aynı şeydir. Başarısız olduğunda sadece sorunu görünür kılar
      Rust'ta da hâlâ hata yazabilirsiniz
    • Sonuçta aynı problem. Rust tarafı C'yi bırakmayı savunuyor ama C'de de indeks yerine handle kullanmak yaygın
  • 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 rand crate'inin kullanılması da böyle bir hava oluşturuyor
    Elbette 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

    • Ben de ilk başta o örnek yüzünden Rust'a mesafeli yaklaşmıştım
      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

    • Ben de neredeyse her zaman enum + match! yaklaşımını tercih ediyorum
      Gerekli mantığı bir Trait altında toplayabiliyor ya da impl <Enum> bloğuna ortak metotlar ekleyebiliyorsunuz
      Böylece hem okunabilirlik artıyor hem de her üyenin davranışı açıkça tanımlanabiliyor
    • impl Deref gibi bir şey denenebilir ama bunun iyi bir fikir olup olmadığından emin değilim
  • İlk örnekteki match ifadesi fazla abartılı geliyor
    Vec.first() veya Vec.iter().nth(0) daha açık ve niyete daha uygun

    • Katılıyorum. match kullanınca sorundan daha karmaşık bir çözüm ortaya çıkıyor
      if kaldırılabiliyorsa match de kaldırılabilir; güvenlik açısından arada fark yok
      first() çok daha kısa ve net
    • Aynı davranışı daha basit ifade etmek için itertools'ün exactly_one yöntemini de kullanabilirsiniz
    • Ama match, 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

    • Bizim şirkette (yaklaşık 300 kişi) bu rolü üstlenen bir teknik borç ekibi 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
    • Büyük teknoloji şirketlerinin çoğunda böyle ekipler var
      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

  • TryFrom trait'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

    • Rust'ı hâlâ öğreniyorum ama unwrap_or_else() adı kulağıma sanki “bilgisayarı tehdit ederek emir veriyormuşsunuz” gibi komik geliyor
  • Bu 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