Rust, daha çok öğrenme eğrisinin zorluğu ile tanınan bir sistem programlama dilidir desek sanırım yanlış olmaz. Ownership, borrow-checker, lifetimes, macro'lar, mutex vs derken managed ortamlarda (.Net, Java, Go gibi) geliştirme yapan programcıları epeyce zorlayan konu başlıklarına sahiptir. Şahsen, aynı öğrenme eğrisi zorluğunu yaşamış birisi olarak kodladıkça daha fazla tutulacağınız bir dil olduğunu da belirtmek isterim.
Buna rağmen son dönemlerde özellikle github copilot gibi asistanlar veya kodlama üzerine uzmanlaşmış yapay zeka ajanları yaygın olarak kullanılmakta ve kod satırlarını otomatik olarak neredeyse tam da düşündüğümüz gibi tamamlamakta.
Çok basit bir örnek vererek devam edelim; Söz gelimi bir sayının faktöryelini hesaplayan metodu recursive modada yazacaksınız ama nasıl olduğunu pek de hatırlamıyorsunuz. Ya da öğrenmekte olduğunuz dilde bu size biraz tuhaf geliyor. Editöre, Todo başlıklı yorum satırını bırakın asistan sizin için tamamlasın. Biraz daha deneyimliyseniz ve memoization kullanmanın daha doğru olacağını düşündünüz. Onu da not olarak yazın asistan çat diye tamamlasın.
Bu yaklaşımın avantajları olduğu kadar dezavantajları olduğu da pekala aşikar. Öncelikle kod yazma pratikliğimizi olumsuz yönde etkileyebilir. Bir programlama diline iyi seviyede hakim birisi için kodun otomatik tamamlanması verimliliği artıran bir özellik olsa dahi zamanla "düşünerek kod yazma" yetkinliğini köreltebilir. Çünkü beyin bir süre sonra recursive faktöryel hesaplama kodunun Rust veya C# ile nasıl yazıldığını unutmaya başlayacaktır. Ancak bu benim kişisel fikrim zira bilimsel bir dayanağım yok. İnanıyorum ki yapay zeka araçları etrafımızı sarmış ve kodlamacının hayatını kolaylaştırmak adına önemli mesafe kat etmiş olsa da bu tip araçların yazdığı kodları denetleyebilmek, ideal olup olmadığına karar verebilmek kısacası Code Review'unu yapmak için de iyi seviyede bilgiye sahip olmamız gerektiğini düşünüyorum.
Bu nedenle rust dili ile ilgili kodlama bilgimizi pekiştirebileceğimiz ya da bir okusak da önemli noktaları hatırlasak diyebileceğimiz bir idman programı hazırlayabiliriz düşüncesindeyim. Bu amaçla bir süredir takip ettiğim bazı kaynaklardaki örnekleri çeşitli seviyelerde ayrıştırarak ilerlemeye karar verdim. İlk etapta başlangıç seviyesinden birkaç maddeyi ele alalım. Yazının devam eden kısmında konu başlıkları altına serpiştirilmiş örnek kod parçaları bulacaksınız. Eğer Visual Studio Code veya IntelliJ RustRover ile geliştirme yapacaksanız kod asistanlarını kapatarak ilerlemenizi öneririm. Bu ve devam yazısında kullandığım orijinal referanslar için kaynaklar bölümüne bakabilirsiniz.
Unwrap/Expect Tuzaklarından Kaçınmak
Rust'ın güçlü yönlerinden birisi generic Option<T> ve Result<T, E> tipleri ile hata yönetimidir. Option tipi ile Some ve None kurguları oluşturarak mutlak değer dönüşü sağlayabiliriz. Rust dilinde null diye bir kavram olmadığını hatırlayalım. Result türü ise çok daha güçlüdür ve olası panik noktalarını kontrol altına almamızda bize yardımcı olur. Ancak özellikle deneysel kodlamalarda ya da birşeyler öğrenirken unwrap ve expect kullanarak ilerleriz zira match veya if let kullanarak kodu daha da uzatmak istemeyiz. Sonuçarın döneceği değerlerden eminizdir. Ancak bu yaklaşım üretim kodunda ciddi problemlere yol açabilir. O nedenle prensibe baştan sahip olmak ve o alışkanlığı kazanmak önemlidir.
Örneğin bir sistemin açılırken kritik bir yapılandırma dosyasını okumaya çalıştığını düşünelim. Birçok sistem çalışmak için ihtiyaç duyduğu parametreleri konfigurasyon dosyalarından, environment değişkenlerinden ya da uç senaryo vault gibi servislerden karşılıyor. Ancak burada basit anlamda sadece bir dosyadan okunduğunu varsayalım. Dosyanın bulunamaması veya okuma sırasında bir hata alınması halinde programın paniklemesi yerine kullanıcıya anlamlı bir hata mesajı döndürmek veya izlenebilir ve dolayısıyla tedbir alınabilir bir makine logu bırakmak daha sağlıklı olacaktır. Aşağıdaki örnek kod parçasında bu durum ele alınmakta ve hem kötü kodlama pratiği hem de ideal yöntem sunulmaktadır.
use std::fs;
// Kötü pratik: unwrap ve expect kullanımı
#[allow(dead_code)]
fn read_file(path: &str) -> String {
fs::read_to_string(path).unwrap()
}
// İyi pratik: Hata yönetimi ile dosya okuma
fn read_file_safely(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path)
}
fn main() {
// let content = read_file("appSettings.json");
// println!("{}", content);
match read_file_safely("appSettings.json") {
Ok(content) => println!("{}", content),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
println!("Dosya bulunamadı: {}", e);
} else {
println!("Dosya okunurken bir hata oluştu: {}", e);
}
}
}
println!("Paniksiz günler dilerim!");
}

Gereksiz clone Çağrılarından Kaçınmak
Rust sahiplik(ownership) modeline göre Vector, String gibi heap bellek bölgesinde ele alınan veri yapıları kapsamlar(scopes) arasında taşınırken varsayılan olarak sahipliğin aktarımı söz konusudur. Eğer veri yapısı taşındığı fonksiyonda bir değişikliğe, başka bir deyişle mutasyona uğramayacaksa tüm veri yapısını klonlayarak göndermek yerine referans ile göndermek daha performanslı ve bellek dostu bir yaklaşımdır. Söz gelimi büyük bir sayı listesinin vektör veri yapısında ele alındığını ve matematiksel bir analiz fonksiyonu işleten bir metot tarafından kullanıldığını varsayalım. Analizi yapan fonksiyon veriyi değiştirmeyeceği için tüm vektörün klonlanması yerine referans ile gönderilmesi daha optimize bir çözüm olacaktır. Zira bu koca vektörün klonlanması bellek üzerinde maliyetli bir operasyondur. Aşağıdaki örnek kod parçasında bu durum ele alınıyor.
Kötü kotlama pratiğini ifade eden calculate_bad metodunda doğrudan vector kullanımı söz konusu. İlk kullanımda value moved here hatası alındığından clone çağrımına gidilmiştir. Oysa ki parametre olarak gelen vektor üzerinde hiçbir değişiklik yapılmayacaktır. Referans yolu ile kapsamlar arası transferi mümkündür.
// Kötü pratik: ownership alan fonksiyon kullanımı
#[allow(dead_code)]
fn calculate_bad(data: Vec<i32>) -> i32 {
let sum: i32 = data.iter().sum();
sum / (data.len() as i32)
}
// Tercih edilen pratik: referans ile veri geçme
fn calculate(data: &[i32]) -> i32 {
let sum: i32 = data.iter().sum();
sum / (data.len() as i32)
}
fn main() {
/*
Aşağıdaki kullanım value moved here hatası verir çünkü calculate fonksiyonu ownership'i alır ve data'yı kullanır.
Sık yapılan çözümlerden birisi vektörü klonlamaktır ancak bu performans açısından maliyetlidir.
Eğer veri değişmeyecekse, ownership almak yerine referans ile geçmek daha iyidir.
error[E0382]: borrow of moved value: `numbers`
--> exc01\src\main.rs:11:22
|
7 | let numbers = vec![10, 20, 30, 40, 50];
| ------- move occurs because `numbers` has type `Vec<i32>`, which does not implement the `Copy` trait
8 | let result = calculate(numbers);
| ------- value moved here
...
11 | println!("{:?}", numbers);
| ^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `calculate` to borrow instead if owning the value isn't necessary
--> exc01\src\main.rs:1:20
|
1 | fn calculate(data: Vec<i32>) -> i32 {
| --------- ^^^^^^^^ this parameter takes ownership of the value
| |
| in this function
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
8 | let result = calculate(numbers.clone());
| ++++++++
*/
let numbers = vec![10, 20, 30, 40, 50];
// Bad practice: ownership alan fonksiyon kullanımı
// // let result = calculate_bad(numbers);
// let result = calculate_bad(numbers.clone()); // Performans maliyeti var
// println!("Sonuç: {}", result);
// println!("{:?}", numbers);
// Good practice: referans ile veri geçme
let result = calculate(&numbers);
println!("Sonuç: {}", result);
println!("{:?}", numbers);
}
value moved hatası;

clone yerine referans kullanımı;

Mutasyon Kapsamını Sınırlamak
Rust programlama dilinde değişkenler varsayılan olarak immutable(değiştirilemez) olarak tanımlanır. Bir değişkenin atıfta bulunduğu veri değerini değiştirmek istediğimizde mut anahtar kelimesi ile değişkeni mutable(değiştirilebilir) olarak tanımlamamız gerekir. Mutasyonu mümkün olan en dar kapsamda kullanmak kod okunurluğu ve güvenliğini artıran bir pratik olarak değerlendirilmektedir.
Aşağıdaki kod parçasında bileşik faiz hesaplaması yapan bir muhasebe fonksiyonu bulunmaktadır. Bu fonksiyondaki döngü içinde güncellenen belli değişkenler vardır(current_amount ve total_interest) Bu değişken değerleri sadece döngü içinde güncellenir ve hesaplama için ihtiyaç duyulan ara değerler(bu örnekte sadece yearly_interest) değiştirilemez(immutable) kullanılabilir. Rust'ın değişkenleri varsayılan olarak immutable kabul etmesinin bir sebebini de, mutasyonları mümkün mertebe dar kapsamda ele almak istemesi olarak düşünebiliriz.
fn calculate_compound_interest(principal: f64, annual_rate: f64, years: u32) -> f64 {
let mut current_amount = principal;
let mut total_interest = 0.0;
for year in 1..=years {
let yearly_interest = current_amount * annual_rate / 100.0;
current_amount += yearly_interest;
total_interest += yearly_interest;
println!("Year {}: Interest earned: {:.2}, Total amount: {:.2}",
year, yearly_interest, current_amount);
}
total_interest
}
fn main() {
let principal = 1000.0;
let annual_rate = 4.5;
let years = 3;
let total_interest = calculate_compound_interest(principal, annual_rate, years);
let final_amount = principal + total_interest;
println!("\nSummary:");
println!("Principal amount: {:.2}", principal);
println!("Annual interest rate: {:.1}%", annual_rate);
println!("Time period: {} years", years);
println!("Total compound interest earned: {:.2}", total_interest);
println!("Final amount: {:.2}", final_amount);
}

Dangling Referanslardan Kaçınmak
Rust'ın güçlü sahiplik(ownership) ve borçlanma(borrowing) modeli, dangling(Sarkmış) referansların oluşmasını derleme zamanında engeller. Dangling referanslar, bir değişken kapsam dışına çıktıktan sonra dahi ona erişmeye çalıştığımızda ortaya çıkar ve bu durum bellek güvenliği sorunlarına yol açar. Zira bellekten düşürdüğümüz bir veri bütünün bir parçası halen daha referans edilebilir ve kötü niyetli kişilerin erişimine açık pozisyonda kalabilir. Rust, bu tür hataların oluşmasını önlemek için katı kurallar uygular. Borrow Checker prensiplerine göre bir referansın atıfta bulunduğu değerden daha uzun süre yaşaması da mümkün değildir. Aslında Dangling(Sarkmış) referanslar genelde bir fonksiyonun local bir değere referans döndürmeye çalışması sırasında ortaya çıkan kritik bir bellek güvenliği hatasıdır.
N sayıda cümleyi literal string olarak tutan bir dizideki en uzun cümleyi bulmaya çalışan bir fonksiyon yazdığımızı düşünelim. En uzun cümleyi referans olarak döndürmeye çalışırsak, fonksiyonun kapsamı sona erdiğinde taşınan dizinin bellekten silinmesiyle birlikte döndürdüğümüz referansın geçersiz hale gelmesi söz konusu olur ve sorunu çözmek için karmaşık lifetime annotasyonları kullanmamız gerekir. Bunun yerine en uzun cümleyi sahiplenen bir String değişkeni fonksiyondan geriye döndürmek daha doğru bir yaklaşımdır.
Aşağıdaki kod parçasında bu senaryo ele alınmaktadır. Esasında ne kadar uğraşırsak uğraşalım zaten dangling referans oluşmayacaktır. Zira ısrarla &str döndürmek istediğimizde Rust bunu derleme zamanında anlayıp dönüş referansına ait yaşam ömrünü kontrol etmemizi isteyen bir hata mesajı yayınlayacaktır. Dolayısıyla &str kullanmaya lifetime annotation kullanımı ile devam da edebiliriz. Lakin maliyet çok yüksek değilse doğrudan bir String döndürmek kod okunurluğunu artırmak, çok sayıda katmandan oluşan daha büyük bir kod tabanında lifetime karmaşıklığı ile uğraşmamak adına daha iyidir.
// // Kötü pratik: Dangling referans sorunu oluşması ve lifetime kullanma gerekliliği
// fn find_longest_sentence_badly(lines: &[&str]) -> &str {
// let mut longest: &str = "";
// for &line in lines {
// if line.len() > longest.len() {
// longest = line;
// }
// }
// longest
// }
// Doğru pratik: String döndürme
fn find_longest_sentence_safely(lines: &[&str]) -> String {
let mut longest = String::new();
for line in lines {
if line.len() > longest.len() {
longest = line.to_string();
}
}
longest
}
fn main() {
let lines = vec![
"Rust is a systems programming language.",
"It is designed for performance and safety.",
"Ownership and borrowing are key concepts in Rust.",
];
/*
Bu fonksiyon dangling referans hatasına neden olur ve ayrıca derleme zamanında 'expected named lifetime parameter' hatası verir.
Sorunu çözmek için fonksiyon imzasına yaşam süresi parametreleri eklemek gerekir.
Bunun yerine en uzun cümleyi String olarak döndürmek daha güvenlidir.
error[E0106]: missing lifetime specifier
--> exc03\src\main.rs:2:51
|
2 | fn find_longest_sentence_badly(lines: &[&str]) -> &str {
| ------- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say which one of `lines`'s 2 lifetimes it is borrowed from
help: consider introducing a named lifetime parameter
|
2 | fn find_longest_sentence_badly<'a>(lines: &'a [&'a str]) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
*/
// let longest_sentence = find_longest_sentence_badly(&lines);
// println!("En uzun cümle (kötü pratik): {}", longest_sentence);
let longest_sentence = find_longest_sentence_safely(&lines);
println!("En uzun cümle (iyi pratik): {}", longest_sentence);
}
lifetime hatası;

String döndürdüğümüz senaryo;

Public API'lerde Kapsamlı Dokümantasyon Kullanmak
Rust'ın güçlü yanlarından birisi de markdown formatını baz alan zengin yardım dokümantasyonu desteğidir. Özellikle public API olarak ifade edebileceğimiz genel açık her tür kütüphane geliştirirken kapsamlı dokümantasyon kullanmak, diğer geliştiricilerin fonksiyonların nasıl kullanılacağını ve ne işe yaradığını anlamalarına yardımcı olur. Özellikle pub erişim belirleyicisi ile işaretlenmiş tüm enstrümanlarda zengin dokümantasyon yorumları kullanmak gerekir. Dokümantasyon kendi deneysel projelerimizde de önemli bir pratiktir. Zira kodun ne yaptığının belli standartlar üzerine oturtulmuş titiz bir anlatımıdır.
/// Verilen bir fonksiyonun türevini yaklaşık olarak hesaplar.
///
/// # Argümanlar
/// * `f` - Türevini almak istediğimiz fonksiyon.
/// * `x` - Türevini hesaplamak istediğimiz nokta.
/// * `h` - Küçük bir değer, türev hesaplamasında kullanılır (varsayılan: 1e-7).
/// # Dönüş Değeri
/// * `f` fonksiyonunun `x` noktasındaki yaklaşık türevi.
pub fn derivative<F>(f: F, x: f64, h: f64) -> f64
where
F: Fn(f64) -> f64,
{
(f(x + h) - f(x - h)) / (2.0 * h)
}
/// Verilen bir fonksiyonun belirli bir aralıktaki integralini yaklaşık olarak hesaplar.
///
/// # Argümanlar
/// * `f` - İntegralini almak istediğimiz fonksiyon.
/// * `a` - İntegral başlangıç noktası.
/// * `b` - İntegral bitiş noktası.
/// * `n` - İntegral hesaplamasında kullanılacak dikdörtgen sayısı (varsayılan: 1000).
/// # Dönüş Değeri
/// * `f` fonksiyonunun `[a, b]` aralığındaki yaklaşık integrali.
pub fn integral<F>(f: F, a: f64, b: f64, n: usize) -> f64
where
F: Fn(f64) -> f64,
{
let width = (b - a) / (n as f64);
let mut total_area = 0.0;
for i in 0..n {
let x = a + (i as f64 + 0.5) * width;
total_area += f(x) * width;
}
total_area
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn test_derivative() {
let f = |x: f64| x.powi(2);
let deriv_at_3 = derivative(f, 3.0, 1e-7);
assert!((deriv_at_3 - 6.0).abs() < 1e-5);
}
#[test]
fn test_integral() {
let f = |x: f64| x;
let integral_result = integral(f, 0.0, 1.0, 1000);
assert!((integral_result - 0.5).abs() < 1e-5);
}
}
ve modül içinde aşağıdaki gibi ilerlenebilir.
//! # Calculus Modülü
//!
//! Bu modül, temel matematiksel işlemleri gerçekleştiren fonksiyonlar içerir.
//! Örnek olarak, türev ve integral hesaplamaları için fonksiyonlar sağlar.
//!
//! # Örnekler
//! ```rust
//! mod calculus;
//!
//! use calculus::{derivative, integral};
//! fn main() {
//! let f = |x: f64| x.powi(2);
//! let deriv_at_3 = derivative(f, 3.0, 1e-7);
//! println!("f'(3) yaklaşık olarak: {}", deriv_at_3); // Yaklaşık 6.0
//! let integral_result = integral(f, 0.0, 1.0, 1000);
//! println!("∫f(x)dx from 0 to 1 yaklaşık olarak: {}", integral_result); // Yaklaşık 0.3333
//! }
//! ```
pub mod calculus;
Komut satırından cargo doc komutu sonrası oluşan dokümantasyon içeriğini kontrol ettiğimizde daha profesyonel bir içerik oluştuğunu görebiliriz.
Modül tarafı;

Örnek fonksiyon tarafı;

veya örneğin RustRover IDE'sinde kod yazarken ve bu modüle ati bir fonksiyonu kullanırken;

Şimdilik bu kadar...
Bu pratik kod örneklerini deneyerek temel rust bilgilerimizden bazılarını yeniden hatırlayabiliriz. İlerleyen yazılarda farklı seviyelerden örneklere de yer vermeye çalışacağım. Bu bölümde yer alan kod parçalarına github reposu üzerinden de erişebilirsiniz. Ayrıca Ferris logosunu sevdiyseniz Maria Letta'nın reposunda daha fazlasını da bulabilirsiniz ;)
Kaynaklar:
Bir başka yazıda görüşünceye dek hepinize mutlu günler dilerim.