Rust'ta bir dosyayı okumanın en kısa yolu std::fs::read_to_string("yol"), yazmanın en kısa yolu ise std::fs::write("yol", içerik) çağrısıdır. İkisi de io::Result döndürür; yani dosya yoksa, izin yoksa ya da içerik geçerli UTF-8 değilse program çökmez, size bir hata değeri verilir ve bunu ? ile yukarı taşıyabilir ya da match ile ele alabilirsiniz. Satır satır okumak gerektiğinde BufReader, çok sayıda küçük yazma yapılacaksa BufWriter, dosya yollarını kurmak için de Path ve PathBuf kullanılır.

Bu yazıda standart kütüphanenin üç modülünü birlikte ele alıyoruz: std::fs dosya sistemi işlemlerini, std::io okuma ve yazma trait'lerini, std::path ise platformdan bağımsız yol işlemeyi sağlar. Örneklerin hepsi yalnızca standart kütüphaneyi kullanır ve Rust 1.99.1 ile derlenip çalıştırılmıştır. Hata türlerini kendi tipinizde toplamak isterseniz özel hata tipi yazısına, Result ve ? temelleri için Result ve hata yönetimi yazısına bakabilirsiniz.

std::fs, std::io ve std::path ne işe yarar?

std::fs, std::io ve std::path ne işe yarar?

std::fs yerel dosya sistemini değiştiren işlemleri içerir: dosya açma, oluşturma, silme, yeniden adlandırma, klasör oluşturma, klasör içeriğini listeleme ve meta veri okuma. Belgelere göre bu modüldeki işlemler platformlar arası çalışacak şekilde tasarlanmıştır; Unix veya Windows'a özgü ek yetenekler std::os altındaki uzantı trait'lerinde durur.

std::io modülünün kalbi Read ve Write trait'leridir. File, ağ soketleri, standart girdi ve hatta bellekteki bir Vec<u8> bu trait'leri uygular. Belgeler bu tipleri kısaca okuyucu (reader) ve yazıcı (writer) diye adlandırır. Aynı modülde konumu değiştirmeye yarayan Seek, tamponla satır okumayı sağlayan BufRead ve birçok fonksiyonun dönüş tipi olan io::Result<T> da bulunur.

std::path ise Path ve PathBuf tiplerini verir. Belgelerin benzetmesiyle bunlar str ve String ikilisine benzer: Path ödünç alınan bir yol dilimidir, PathBuf ise sahip olunan ve büyütülebilen yoldur. İkisi de işletim sisteminin yol sözdizimine göre çalışan OsStr ve OsString üzerine ince birer sarmalayıcıdır.

Tek seferde okuma ve yazma

Küçük ve orta boy dosyalar için en pratik yol, içeriği tek çağrıda belleğe almaktır. fs::read_to_string dosyayı açar, tamamını okur ve bir String döndürür; fs::read aynı işi bayt vektörü (Vec<u8>) olarak yapar. fs::write ise dosya yoksa oluşturur, varsa içeriğini tamamen değiştirir. Belgeler bu fonksiyonları, File::open ile read_to_string ya da File::create ile write_all ikililerinin daha az import ve ara değişkenle yazılmış kısayolları olarak tanımlar.

rust
use std::fs;
use std::io;

fn main() -> io::Result<()> {
    fs::write("not.txt", "merhaba\nçalışma dosyası\n")?;

    let metin = fs::read_to_string("not.txt")?;
    print!("{metin}");

    let baytlar = fs::read("not.txt")?;
    println!("{} karakter, {} bayt", metin.chars().count(), baytlar.len());
    Ok(())
}
// çıktı:
// merhaba
// çalışma dosyası
// 24 karakter, 28 bayt

Çıktıdaki fark Türkçe karakterlerden gelir: ç, ı ve ş UTF-8'de ikişer bayt tuttuğu için karakter sayısı ile bayt sayısı eşit değildir. Bu ayrımın ayrıntısı String ve &str yazısında anlatılıyor. Fonksiyonların yol parametresi AsRef<Path> olduğu için &str, String, &Path veya PathBuf verebilirsiniz; fs::write içeriği de AsRef<[u8]> olarak alır, bu yüzden hem metin hem bayt dizisi kabul eder.

main fonksiyonunun io::Result<()> döndürmesi, içeride ? kullanmanızı sağlar. Bir hata oluşursa main bu hatayı döndürür, program sıfır olmayan bir çıkış koduyla biter ve hata standart hataya yazılır. Bu davranışın komut satırı araçlarındaki kullanımı komut satırı argümanları yazısında ele alınıyor.

Hataları türüne göre ele almak

Dosya işlemlerinde hata kaçınılmazdır: dosya silinmiş olabilir, kullanıcı yanlış yol yazmış olabilir, disk dolmuş olabilir. io::Error değerinin kind() metodu bir io::ErrorKind döndürür ve bununla hatanın sınıfına göre farklı davranabilirsiniz. Aşağıdaki örnekte olmayan bir dosya açılıyor, ardından geçersiz UTF-8 içeren bir dosya metin olarak okunmaya çalışılıyor.

rust
use std::fs::{self, File};
use std::io::ErrorKind;

fn main() {
    match File::open("olmayan.txt") {
        Ok(_) => println!("açıldı"),
        Err(e) if e.kind() == ErrorKind::NotFound => println!("dosya yok"),
        Err(e) => println!("başka hata: {e}"),
    }

    fs::write("bozuk.txt", [0x66, 0xff, 0x6f]).unwrap();
    let hata = fs::read_to_string("bozuk.txt").unwrap_err();
    println!("{:?}", hata.kind());
    println!("{hata}");
}
// çıktı:
// dosya yok
// InvalidData
// stream did not contain valid UTF-8

0xff baytı hiçbir geçerli UTF-8 dizisinin parçası olamaz; bu yüzden read_to_string InvalidData türünde hata döndürür. Böyle dosyaları okumanız gerekiyorsa fs::read ile baytları alıp kendiniz yorumlayabilirsiniz. İşletim sisteminden gelen hata mesajlarının metni platforma ve sistem diline göre değişir; bu nedenle programın mantığını mesaj metnine değil ErrorKind değerine dayandırın. OpenOptions belgesi ayrıca hangi durumun hangi ErrorKind ile eşlendiğinin fonksiyonun uyumluluk sözleşmesinin parçası olmadığını, bazı dosya sistemi hatalarının da şu an hiçbir türle eşleşmediğini belirtir. Dolayısıyla match ifadelerinizde her zaman genel bir Err(e) kolu bırakın.

File ve OpenOptions ile ayrıntılı kontrol

File ve OpenOptions ile ayrıntılı kontrol: read(true), write(true), append(true), truncate(true)

Tek seferlik fonksiyonlar yetmediğinde File tipini doğrudan kullanırsınız. File::open dosyayı salt okunur açar. File::create salt yazılır açar, dosya yoksa oluşturur, varsa uzunluğunu sıfıra indirir; yani eski içerik silinir. File::create_new ise dosya zaten varsa hata verir ve bunu tek bir atomik işlemle yapar. Daha ince ayar için OpenOptions kurucusu vardır: OpenOptions::new() ile tüm seçenekler false olarak başlar, zincirleme metotlarla istediklerinizi açar ve en sonda open çağırırsınız. File::options() aynı nesneyi import gerektirmeden verir.

rust
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};

fn main() -> io::Result<()> {
    fs::write("gunluk.log", "ilk satır\n")?;

    let mut f = OpenOptions::new().append(true).open("gunluk.log")?;
    writeln!(f, "ikinci satır")?;
    writeln!(f, "üçüncü satır")?;
    print!("{}", fs::read_to_string("gunluk.log")?);

    match File::create_new("gunluk.log") {
        Ok(_) => println!("yeni dosya oluşturuldu"),
        Err(e) => println!("create_new: {:?}", e.kind()),
    }

    let sonuc = OpenOptions::new().create(true).open("x.txt");
    println!("yalnızca create: {:?}", sonuc.unwrap_err().kind());
    Ok(())
}
// çıktı:
// ilk satır
// ikinci satır
// üçüncü satır
// create_new: AlreadyExists
// yalnızca create: InvalidInput

Son satır sık yapılan bir hatayı gösteriyor: create(true) tek başına yetmez. Belgeye göre dosyanın oluşturulabilmesi için write veya append erişimi de açılmalıdır; aksi halde open InvalidInput döndürür. Benzer şekilde truncate(true) yazma erişimi olmadan çalışmaz. write(true) var olan dosyayı kesmeden baştan üzerine yazar; eski içeriğin sonu dosyada kalabilir. Ekleme modu ise her yazmanın o anki dosya sonuna yapılmasını garanti eder, ama farklı süreçlerin yazdığı verinin birbirine karışmayacağını garanti etmez.

SeçenekEtkisiDikkat
read(true)Okuma erişimiFile::open bunu kullanır
write(true)Yazma erişimiVar olan içeriği kesmez, üzerine yazar
append(true)Her yazma dosya sonuna giderwrite(true) ile birlikte vermek aynı etkiyi yapar
truncate(true)Var olan dosyayı sıfır uzunluğa indirirYazma erişimi gerekir
create(true)Yoksa oluşturur, varsa açarwrite veya append olmadan InvalidInput
create_new(true)Yalnızca yeni dosya oluştururAtomiktir; create ve truncate yok sayılır

File kapsam dışına çıktığında otomatik kapanır. Ancak belgeler, kapanış sırasında oluşan hataların Drop uygulamasında yok sayıldığını vurgular. Verinin diske ulaştığından emin olmanız ve bu aşamadaki hataları yakalamanız gerekiyorsa sync_all çağırın; bu çağrının dosyayı yalnızca kapatmaktan genelde daha pahalı olduğunu da unutmayın.

BufReader ve BufWriter ile tamponlu okuma ve yazma

File okuma ve yazmaları kendi başına tamponlamaz. Her küçük read veya write çağrısı işletim sistemine ayrı bir çağrı olarak gidebilir. BufReader alttaki okuyucudan büyük parçalar halinde okuyup bellekte tutar; BufWriter ise yazılanları biriktirip seyrek ve büyük parçalar halinde aktarır. Her ikisinin varsayılan tampon boyutu belgelere göre şu an 8 KiB'tır ve ileride değişebilir. Belgeler bu tiplerin küçük ve tekrarlı çağrılarda fayda sağladığını; tek seferde çok büyük okuma veya yazmada, ya da zaten bellekte olan bir kaynakta (örneğin Vec<u8>) bir kazanç getirmediğini açıkça söyler.

rust
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};

fn main() -> io::Result<()> {
    let dosya = File::create("sayilar.txt")?;
    let mut yazici = BufWriter::new(dosya);
    for i in 1..=3 {
        writeln!(yazici, "satır {i}")?;
    }
    yazici.flush()?;

    let okuyucu = BufReader::new(File::open("sayilar.txt")?);
    for (no, satir) in okuyucu.lines().enumerate() {
        let satir = satir?;
        println!("{}: {satir}", no + 1);
    }
    Ok(())
}
// çıktı:
// 1: satır 1
// 2: satır 2
// 3: satır 3

lines() metodu BufRead trait'inden gelir; bu yüzden use std::io::BufRead satırını unutursanız metot bulunamaz. Döndürdüğü yineleyicinin her elemanı io::Result<String> tipindedir ve satır sonundaki \n ya da \r\n çıkarılmış olur. Yineleyicilerle çalışmanın genel kuralları için iterator yazısına göz atabilirsiniz.

read_line ise farklı davranır: satır sonu karakterini korur, okunanı verdiğiniz String değişkeninin sonuna ekler ve okunan bayt sayısını döndürür. Ok(0) dosya sonuna gelindiğini gösterir. Döngü içinde aynı tamponu kullanıyorsanız her turda clear() çağırmazsanız satırlar birikir. Belgeler ayrıca read_line çağrısının satır sonu veya dosya sonu gelene kadar bloklandığını, güvenilmeyen bir kaynaktan okurken take ile üst sınır koymanın akıllıca olduğunu belirtir.

Path ve PathBuf ile yol oluşturma

Yolları metin birleştirerek kurmak, ayırıcı karakterin / mı \ mı olacağı gibi sorunlar çıkarır. std::path modülü bu işi platforma bırakır. Path::new bir metni maliyetsiz biçimde yol dilimine çevirir; parent, file_name, file_stem ve extension gibi metotlar yolu parçalarına ayırır. Değiştirmeniz gerekiyorsa PathBuf kullanır, push ile parça ekler, set_extension ile uzantıyı değiştirirsiniz.

rust
use std::path::{Path, PathBuf};

fn main() {
    let yol = Path::new("veri/raporlar/ekim.csv");
    println!("{:?}", yol.parent());
    println!("{:?}", yol.file_name());
    println!("{:?}", yol.file_stem());
    println!("{:?}", yol.extension());

    let mut kok = PathBuf::from("veri");
    kok.push("yedek");
    kok.push("ekim.csv");
    kok.set_extension("bak");
    println!("{}", kok.display());

    let yeni = yol.with_extension("json");
    println!("{}", yeni.display());
    println!("{}", Path::new("veri").join("/mutlak").display());
}
// çıktı (Windows):
// Some("veri/raporlar")
// Some("ekim.csv")
// Some("ekim")
// Some("csv")
// veri\yedek\ekim.bak
// veri/raporlar/ekim.json
// /mutlak

Bu çıktı Windows'ta alındı; push parçaları platformun ayırıcısıyla eklediği için beşinci satırda ters bölü görünüyor, Linux'ta aynı satır veri/yedek/ekim.bak olur. Son satır önemli bir kuralı gösterir: push ve join metotlarına mutlak bir yol verirseniz mevcut yolun yerini alır. Kullanıcıdan gelen bir dosya adını bir kök klasöre eklerken bunu hesaba katın.

Yazdırmak için display() kullanılır, çünkü yollar geçerli Unicode olmayabilir ve bu metot gerekirse kayıplı dönüşüm yapar. to_str() yalnızca yol geçerli UTF-8 ise Some döndürür. Belgelere göre components, karşılaştırma ve has_root gibi metotlar tekrarlanan ayırıcıları ve baştaki olmayan . parçalarını yok sayan basit bir normalleştirme yapar, ama .. parçalarını ve sembolik bağlantıları çözmez. Tam çözüm için dosya sistemine erişen fs::canonicalize gerekir. Ayrıca starts_with gibi dosya sistemine dokunmayan metotlar, Windows sürücü harfleri dışında, platformdan bağımsız olarak büyük-küçük harfe duyarlıdır.

Klasör oluşturma, listeleme ve silme

fs::create_dir_all eksik olan tüm üst klasörleri de oluşturur. Belgeler bu fonksiyonun atomik olmadığını, hata verirse o ana kadar oluşturduğu klasörlerin yerinde kaldığını belirtir. fs::read_dir bir klasördeki girdileri io::Result<DirEntry> üreten bir yineleyici olarak verir; . ve .. atlanır. Sıralama platforma ve dosya sistemine bağlıdır, çağrıdan çağrıya bile değişebilir; tekrarlanabilir bir sıra istiyorsanız kendiniz sıralamalısınız.

rust
use std::fs;
use std::io;
use std::path::Path;

fn main() -> io::Result<()> {
    let kok = Path::new("deneme_klasor");
    fs::create_dir_all(kok.join("alt/derin"))?;
    fs::write(kok.join("b.txt"), "B")?;
    fs::write(kok.join("a.txt"), "A")?;

    let mut girdiler = fs::read_dir(kok)?
        .map(|g| g.map(|g| g.file_name()))
        .collect::<Result<Vec<_>, io::Error>>()?;
    girdiler.sort();
    println!("{girdiler:?}");

    let meta = fs::metadata(kok.join("a.txt"))?;
    println!("dosya mı: {}, boyut: {}", meta.is_file(), meta.len());

    fs::remove_dir_all(kok)?;
    println!("var mı: {:?}", fs::exists(kok));
    Ok(())
}
// çıktı:
// ["a.txt", "alt", "b.txt"]
// dosya mı: true, boyut: 1
// var mı: Ok(false)

collect::<Result<Vec<_>, io::Error>>() kalıbı, yineleyicideki ilk hatada durup o hatayı döndüren, hata yoksa tüm sonuçları bir vektöre toplayan kısa bir yoldur. remove_dir_all klasörü içeriğiyle birlikte siler; belgedeki uyarı dikkat çekicidir: dikkatli kullanın. Klasör yoksa hata verir, yani tekrar çağrılması güvenli değildir, sembolik bağlantıları izlemez ve bağlantının kendisini siler.

Varlık kontrolü için iki seçenek vardır. Path::exists bir bool döndürür ve meta veriye erişilemediği her durumda, örneğin izin hatasında, false der; belgeler bu yüzden onu hataya açık bulur. fs::exists ise io::Result<bool> döndürür ve var olup olmadığını doğrulayamadığında hata verir. Ancak ikisi de aşağıda anlatılan yarış durumunu çözmez.

Kontrol ile kullanım arasındaki yarış (TOCTOU)

Kontrol ile kullanım arasındaki yarış (TOCTOU): Mümkün olduğunda atomik işlem kullanın, Meta veri sorgularının başka süreçlerin değişikliklerinden etkilenebileceğini..., İşiniz...

std::fs modülü belgesi, bir koşulun kontrol edildiği an ile sonucun kullanıldığı an arasında başka bir sürecin durumu değiştirebileceği yarış durumuna özel bir bölüm ayırır. Örneğin önce dosyanın var olmadığını kontrol edip sonra oluşturmak bu yarışa açıktır: iki adım arasında başka bir süreç aynı dosyayı oluşturabilir. Önerilen çözümler şunlardır:

  • Mümkün olduğunda atomik işlem kullanın: varlık kontrolü yerine File::create_new ya da create_new(true) seçeneği.
  • Meta veri sorgularının başka süreçlerin değişikliklerinden etkilenebileceğini unutmayın.
  • İşiniz bitene kadar dosyayı açık tutun; aynı yolu tekrar tekrar açmak yerine elinizdeki File değerini kullanın.

Uygulamalı mini örnek: log dosyasından hata özeti

Şimdi parçaları birleştirelim. Aşağıdaki fonksiyon bir log dosyasını satır satır okur, HATA ile başlayan satırları satır numarasıyla ayrı bir dosyaya yazar ve toplam satır ile hata sayısını döndürür. Girdi dosyası yoksa hata ? ile çağırana taşınır ve main bunu türüne göre raporlar.

rust
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::Path;

fn ozetle(girdi: &Path, cikti: &Path) -> io::Result<(usize, usize)> {
    let okuyucu = BufReader::new(File::open(girdi)?);
    let mut yazici = BufWriter::new(File::create(cikti)?);
    let (mut toplam, mut hata) = (0, 0);
    for satir in okuyucu.lines() {
        let satir = satir?;
        toplam += 1;
        if satir.starts_with("HATA") {
            hata += 1;
            writeln!(yazici, "{toplam}: {satir}")?;
        }
    }
    yazici.flush()?;
    Ok((toplam, hata))
}

fn main() {
    fs::write("sunucu.log", "BİLGİ başladı\nHATA disk dolu\nBİLGİ istek\nHATA zaman aşımı\n").unwrap();
    match ozetle(Path::new("sunucu.log"), Path::new("hatalar.txt")) {
        Ok((t, h)) => println!("{t} satır, {h} hata"),
        Err(e) => eprintln!("okunamadı: {e}"),
    }
    print!("{}", fs::read_to_string("hatalar.txt").unwrap());
    if let Err(e) = ozetle(Path::new("yok.log"), Path::new("x.txt")) {
        println!("hata türü: {:?}", e.kind());
    }
}
// çıktı:
// 4 satır, 2 hata
// 2: HATA disk dolu
// 4: HATA zaman aşımı
// hata türü: NotFound

Fonksiyon parametre olarak &Path alıyor; böylece çağıran taraf ister Path::new ister PathBuf versin, kopya oluşmaz. Okuma tamponlu olduğu için büyük log dosyalarında bile dosyanın tamamı belleğe alınmaz. Bu fonksiyonu test etmek isterseniz geçici dosyalar oluşturan bir birim testi yazabilirsiniz; ayrıntılar Rust test yazma yazısında.

Sık yapılan hatalar

  • lines() veya read_line kullanırken BufRead trait'ini kapsama almamak.
  • File::create çağrısının var olan dosyayı sıfırladığını unutmak; ekleme yapmak için append(true) gerekir.
  • create(true) seçeneğini write veya append olmadan vermek ve InvalidInput hatası almak.
  • BufWriter değerini flush() çağırmadan düşürmek ve yazma hatalarını kaçırmak.
  • read_dir sonucunun sıralı geleceğini varsaymak.
  • Önce exists() ile kontrol edip sonra dosya oluşturmak; bunun yerine create_new kullanın.
  • Kullanıcı girdisini join ile eklerken mutlak yolun mevcut yolu tamamen değiştirebileceğini gözden kaçırmak.
  • Hata mesajı metnine göre karar vermek; mesajlar platforma göre değişir, ErrorKind kullanın.

Hangi yöntemi ne zaman kullanmalı?

İhtiyaçÖnerilen araçNot
Küçük metin dosyasını okumakfs::read_to_stringGeçersiz UTF-8 ise InvalidData
İkili dosyayı okumakfs::readVec<u8> döner
Dosyayı baştan yazmakfs::writeYoksa oluşturur, varsa değiştirir
Büyük dosyayı satır satır işlemekBufReader ve lines()Tamamını belleğe almaz
Çok sayıda küçük yazmaBufWriterSonda flush() çağırın
Dosyaya ekleme yapmakOpenOptions append(true)Dosyayı kesmez
Yalnızca yeni dosya oluşturmakFile::create_newAtomik, yarışa karşı güvenli
Yol kurmakPathBuf ve join/pushAyırıcıyı platform belirler

Sık Sorulan Sorular

read_to_string ile BufReader arasında nasıl seçim yaparım?

Dosya küçükse ve içeriğin tamamına aynı anda ihtiyacınız varsa fs::read_to_string en sade çözümdür. Dosya büyükse, satır satır işleyebiliyorsanız ya da kaynağın ne kadar büyük olacağını bilmiyorsanız BufReader ile okumak bellek kullanımını sınırlı tutar.

Dosya kapatmak için ayrıca bir şey yapmam gerekir mi?

Hayır. File kapsam dışına çıktığında otomatik kapanır. Ancak kapanırken oluşan hatalar yok sayılır; verinin kalıcı olarak yazıldığından emin olmak istiyorsanız sync_all çağırıp sonucunu kontrol edin. Sahiplik ve düşürme mantığı için ownership yazısına bakabilirsiniz.

Windows ve Linux için ayrı yol kodu yazmam gerekir mi?

Genellikle hayır. Path ve PathBuf platformun yol sözdizimine göre çalışır; parçaları join veya push ile eklediğinizde doğru ayırıcı kullanılır. Yolu metin olarak elle birleştirmekten kaçının.

Dosya yoksa oluştur, varsa ekle davranışını nasıl elde ederim?

OpenOptions::new().append(true).create(true).open(yol) zinciri bu işi yapar. append yazma erişimini sağladığı için create da geçerli hale gelir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Rust nedir? · Result ve hata yönetimi · Özel hata tipi ve Error trait · Komut satırı argümanları · String ve &str farkı