Fonksiyonlar
Fonksiyonlar Rust kodunda son derece yaygındır. Dildeki en önemli fonksiyonlardan birini zaten gördünüz: pek çok programın giriş noktası olan main fonksiyonu. Ayrıca yeni fonksiyonlar bildirmenizi sağlayan fn anahtar kelimesiyle de karşılaştınız.
Rust kodunda fonksiyon ve değişken adları için geleneksel yazım biçimi olarak tüm harflerin küçük yazıldığı ve kelimelerin alt çizgiyle (_) ayrıldığı snake case (yilan_bicimi) stili kullanılır. İşte örnek bir fonksiyon tanımı içeren bir program:
Dosya adı: src/main.rs
fn main() {
println!("Merhaba, dünya!");
baska_bir_fonksiyon();
}
fn baska_bir_fonksiyon() {
println!("Başka bir fonksiyon.");
}
Rust’ta bir fonksiyonu fn anahtar kelimesini, ardından fonksiyon adını ve bir çift parantezi yazarak tanımlarız. Süslü parantezler ({}), derleyiciye fonksiyon gövdesinin nerede başlayıp nerede bittiğini bildirir.
Tanımladığımız herhangi bir fonksiyonu, adını ve ardından bir çift parantez yazarak çağırabiliriz. baska_bir_fonksiyon programda tanımlı olduğu için main fonksiyonunun içinden çağrılabilir. Kaynak kodda baska_bir_fonksiyon fonksiyonunu main fonksiyonundan sonra tanımladığımıza dikkat edin; ondan önce de tanımlayabilirdik. Rust, fonksiyonlarınızı dosyanın neresinde tanımladığınızla ilgilenmez; yalnızca çağıran kodun görebileceği bir kapsam (scope) içinde tanımlanmış olmaları yeterlidir.
Fonksiyonları daha yakından incelemek için fonksiyonlar adında yeni bir ikili (binary) proje başlatalım. baska_bir_fonksiyon örneğini src/main.rs dosyasına yerleştirip çalıştırdığınızda aşağıdaki çıktıyı görmelisiniz:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/fonksiyonlar`
Merhaba, dünya!
Başka bir fonksiyon.
Satırlar, main fonksiyonunda yer aldıkları sırayla çalıştırılır. Önce "Merhaba, dünya!" mesajı yazdırılır, ardından baska_bir_fonksiyon çağrılır ve kendi mesajı ekrana yazdırılır.
Parametreler
Fonksiyonları, fonksiyon imzasının (signature) bir parçası olan özel değişkenlere, yani parametrelere (parameters) sahip olacak şekilde tanımlayabiliriz. Bir fonksiyonun parametreleri olduğunda, bu parametreler için fonksiyona somut değerler iletebilirsiniz. Teknik olarak bu somut değerlere argüman (argument) denir; ancak günlük kullanımda geliştiriciler hem fonksiyon tanımındaki değişkenler hem de fonksiyon çağrılırken iletilen somut değerler için parametre ve argüman kelimelerini sıklıkla birbirinin yerine kullanır.
baska_bir_fonksiyon fonksiyonunun bu sürümünde bir parametre ekliyoruz:
Dosya adı: src/main.rs
fn main() {
baska_bir_fonksiyon(5);
}
fn baska_bir_fonksiyon(x: i32) {
println!("x'in değeri: {x}");
}
Bu programı çalıştırmayı deneyin; aşağıdaki çıktıyı almalısınız:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/fonksiyonlar`
x'in değeri: 5
baska_bir_fonksiyon bildiriminin x adında bir parametresi vardır ve x’in türü i32 olarak belirtilmiştir. baska_bir_fonksiyon fonksiyonuna 5 değerini ilettiğimizde, println! makrosu, biçimlendirme metnindeki (format string) içinde x bulunan süslü parantez çiftinin yerine 5 değerini koyar.
Fonksiyon imzalarında her parametrenin türünü mutlaka bildirmelisiniz. Bu, Rust’ın tasarımında bilinçli olarak alınmış bir karardır: Fonksiyon tanımlarında tür bildirimini zorunlu kılmak, derleyicinin kodun diğer kısımlarında hangi türü kastettiğinizi anlamak için sizden neredeyse hiçbir zaman tür bildirimi istememesi anlamına gelir. Ayrıca derleyici, fonksiyonun hangi türleri beklediğini bildiğinde çok daha açıklayıcı hata mesajları üretebilir.
Not: Bir fonksiyonun fn anahtar kelimesi, adı, parametre listesi (adları ve türleri) ile varsa dönüş türünden (-> Tür) oluşan başlık kısmına fonksiyon imzası denir. Rust derleyicisi fonksiyon gövdelerinin içinde güçlü bir tür çıkarımı (type inference) yaparken fonksiyon sınırlarında açık tür bildirimi ister. Böylece her fonksiyon imzası kendi başına okunabilen net bir sözleşme görevi görür; bir fonksiyonun gövdesinde yapılan değişiklikler imzayı sessizce değiştirip çağıran kodları beklenmedik biçimde bozamaz.
Birden fazla parametre tanımlarken parametre bildirimlerini aşağıdaki gibi virgülle ayırın:
Dosya adı: src/main.rs
fn main() {
etiketli_olcum_yazdir(5, 'h');
}
fn etiketli_olcum_yazdir(deger: i32, birim_etiketi: char) {
println!("Ölçüm değeri: {deger}{birim_etiketi}");
}
Bu örnek, iki parametre alan etiketli_olcum_yazdir adında bir fonksiyon oluşturur. İlk parametre deger adındadır ve i32 türündedir. İkinci parametre ise birim_etiketi adındadır ve char türündedir. Fonksiyon daha sonra hem deger hem de birim_etiketi değerlerini içeren bir metin yazdırır.
Gelin bu kodu çalıştırmayı deneyelim. fonksiyonlar projenizin src/main.rs dosyasındaki mevcut programı yukarıdaki örnekle değiştirin ve cargo run komutuyla çalıştırın:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/fonksiyonlar`
Ölçüm değeri: 5h
Fonksiyonu deger için 5 ve birim_etiketi için 'h' değerleriyle çağırdığımızdan program çıktısı bu değerleri içerir.
Deyimler ve İfadeler
Fonksiyon gövdeleri, isteğe bağlı olarak sonunda bir ifade (expression) bulunan bir dizi deyimden (statement) oluşur. Şimdiye kadar ele aldığımız fonksiyonların sonunda bir ifade yer almıyordu; ancak bir deyimin parçası olarak ifadelerle karşılaştınız. Rust ifade tabanlı (expression-based) bir dil olduğundan bu ayrımı anlamak son derece önemlidir. Diğer pek çok dilde aynı ayrım bulunmaz; bu nedenle deyimlerin ve ifadelerin ne olduğuna ve aralarındaki farkların fonksiyon gövdelerini nasıl etkilediğine yakından bakalım.
- Deyimler, belirli bir eylemi gerçekleştiren ve değer döndürmeyen komutlardır.
- İfadeler, değerlendirilerek bir sonuç değeri üreten kod parçalarıdır.
Gelin bazı örneklere bakalım.
Aslında deyimleri ve ifadeleri daha önce de kullandık. let anahtar kelimesiyle bir değişken oluşturup ona değer atamak bir deyimdir. Liste 3-1’de let y = 6; bir deyimdir.
fn main() {
let y = 6;
}
main fonksiyonu bildirimiFonksiyon tanımları da birer deyimdir; yukarıdaki örneğin tamamı başlı başına bir deyimdir. (Ancak birazdan göreceğimiz gibi bir fonksiyonu çağırmak deyim değildir.)
Deyimler değer döndürmez. Bu nedenle aşağıdaki kodun yapmaya çalıştığı gibi bir let deyimini başka bir değişkene atayamazsınız; derleme hatası alırsınız:
Dosya adı: src/main.rs
fn main() {
let x = (let y = 6);
}
Bu programı çalıştırdığınızda alacağınız hata şu şekilde görünür:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `fonksiyonlar` (bin "fonksiyonlar") generated 1 warning
error: could not compile `fonksiyonlar` (bin "fonksiyonlar") due to 1 previous error; 1 warning emitted
let y = 6 deyimi bir değer döndürmediğinden x değişkeninin bağlanabileceği herhangi bir değer yoktur (derleyici de expected expression, found `let` statement yani “ifade bekleniyordu, let deyimi bulundu” diyerek tam olarak bu noktaya işaret eder). Bu davranış, atama işleminin atanan değeri döndürdüğü C ve Ruby gibi dillerden farklıdır. O dillerde x = y = 6 yazarak hem x hem de y değişkenlerinin 6 değerini almasını sağlayabilirsiniz; Rust’ta ise bu geçerli değildir.
İfadeler değerlendirilerek bir değer üretir ve Rust’ta yazacağınız kodun geri kalanının büyük çoğunluğunu oluşturur. Örneğin 5 + 6 gibi matematiksel bir işlemi düşünün; bu işlem değerlendirildiğinde 11 değerini üreten bir ifadedir. İfadeler, deyimlerin bir parçası olabilir: Liste 3-1’deki let y = 6; deyiminde yer alan 6, değerlendirildiğinde 6 değerini üreten bir ifadedir. Bir fonksiyonu çağırmak bir ifadedir. Bir makroyu çağırmak bir ifadedir. Süslü parantezlerle oluşturulan yeni bir kapsam bloğu da bir ifadedir; örneğin:
Dosya adı: src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("y'nin değeri: {y}");
}
Buradaki ifade:
{
let x = 3;
x + 1
}
bu örnekte değerlendirilerek 4 değerini üreten bir bloktur. Bu değer, let deyiminin bir parçası olarak y değişkenine bağlanır. Şimdiye kadar gördüğünüz çoğu satırın aksine, x + 1 satırının sonunda noktalı virgül (;) bulunmadığına dikkat edin. İfadelerin sonunda noktalı virgül yer almaz. Bir ifadenin sonuna noktalı virgül eklerseniz onu bir deyime dönüştürürsünüz ve artık bir değer döndürmez. Şimdi fonksiyonların dönüş değerlerini ve ifadeleri incelerken bu kuralı aklınızda bulundurun.
Not: Yukarıdaki let y = { ... }; örneğinde noktalı virgüllerin konumuna dikkat edin: Bloğun içindeki x + 1 satırının sonunda noktalı virgül yoktur; çünkü bu satır bloğun üreteceği değeri belirleyen sonuç ifadesidir. Kapanan süslü parantezin (};) hemen sonundaki noktalı virgül ise dıştaki let y = ...; atama deyimini sonlandırır. Kısacası Rust’ta ; yalnızca görsel bir satır sonu işareti değil, bir ifadeyi değer döndürmeyen bir deyime dönüştüren (ve sonunda sonuç ifadesi kalmayan bloğun birim tür () üretmesine yol açan) sözdizimsel bir araçtır.
Dönüş Değerleri Olan Fonksiyonlar
Fonksiyonlar, kendilerini çağıran koda değer döndürebilir. Dönüş değerlerine isim vermeyiz ancak türlerini bir ok işaretinden (->) sonra mutlaka bildirmemiz gerekir. Rust’ta bir fonksiyonun dönüş değeri, fonksiyon gövdesi bloğundaki son ifadenin değeriyle aynıdır. return anahtar kelimesini kullanıp bir değer belirterek bir fonksiyondan erken dönebilirsiniz; ancak çoğu fonksiyon son ifadeyi örtük olarak (implicitly) döndürür. İşte değer döndüren bir fonksiyon örneği:
Dosya adı: src/main.rs
fn bes() -> i32 {
5
}
fn main() {
let x = bes();
println!("x'in değeri: {x}");
}
bes fonksiyonunun içinde hiçbir fonksiyon çağrısı, makro, hatta let deyimi bile yoktur; yalnızca tek başına 5 sayısı yer alır. Bu, Rust’ta tamamen geçerli bir fonksiyondur. Fonksiyonun dönüş türünün de -> i32 olarak belirtildiğine dikkat edin. Bu kodu çalıştırmayı deneyin; çıktı şöyle görünmelidir:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/fonksiyonlar`
x'in değeri: 5
bes fonksiyonundaki 5, fonksiyonun dönüş değeridir; bu yüzden dönüş türü i32 olarak tanımlanmıştır. Bunu biraz daha ayrıntılı inceleyelim. Burada iki önemli nokta vardır: İlk olarak, let x = bes(); satırı bir değişkene ilk değerini vermek için bir fonksiyonun dönüş değerini kullandığımızı gösterir. bes fonksiyonu 5 döndürdüğü için bu satır aşağıdakiyle birebir aynıdır:
#![allow(unused)]
fn main() {
let x = 5;
}
İkinci olarak, bes fonksiyonu hiçbir parametre almaz ve dönüş değerinin türünü tanımlar; ancak fonksiyonun gövdesi, değerini döndürmek istediğimiz bir ifade olduğu için sonunda noktalı virgül bulunmayan tek bir 5 sayısından oluşur.
Başka bir örneğe bakalım:
Dosya adı: src/main.rs
fn main() {
let x = arti_bir(5);
println!("x'in değeri: {x}");
}
fn arti_bir(x: i32) -> i32 {
x + 1
}
Bu kodu çalıştırmak ekrana x'in değeri: 6 yazdıracaktır. Peki x + 1 satırının sonuna bir noktalı virgül koyarak onu bir ifadeden bir deyime dönüştürürsek ne olur?
Dosya adı: src/main.rs
fn main() {
let x = arti_bir(5);
println!("x'in değeri: {x}");
}
fn arti_bir(x: i32) -> i32 {
x + 1;
}
Bu kodu derlemek aşağıdaki gibi bir hata üretecektir:
$ cargo run
Compiling fonksiyonlar v0.1.0 (file:///projects/fonksiyonlar)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn arti_bir(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `fonksiyonlar` (bin "fonksiyonlar") due to 1 previous error
Ana hata mesajı olan error[E0308]: mismatched types (uyuşmayan türler), bu koddaki temel sorunu açıkça ortaya koyar. arti_bir fonksiyonunun tanımı bir i32 döndüreceğini bildirir; ancak deyimler bir değer üretmez ve bu durum boş demet olan birim tür () ile temsil edilir (expected `i32`, found `()`). Derleyicinin belirttiği gibi, fonksiyon gövdesinde noktalı virgülsüz bir sonuç ifadesi (tail expression) veya return bulunmadığından fonksiyon örtük olarak () döndürür; bu da fonksiyon imzasındaki -> i32 tanımıyla çelişerek derleme hatasına yol açar. Çıktının sonundaki help: remove this semicolon to return this value (bu değeri döndürmek için bu noktalı virgülü kaldırın) satırı ise hatayı nasıl düzelteceğimizi doğrudan gösterir.
Not: İdiyomatik Rust kodunda return anahtar kelimesi genellikle yalnızca bir koşul sağlandığında fonksiyonun sonuna gelmeden erken çıkış yapmak için kullanılır. Fonksiyonun son satırında return x + 1; yazmak derlense de Rust topluluğunda gereksiz kabul edilir (hatta cargo clippy aracı bu kullanımda uyarı verir); bunun yerine son satırı x + 1 biçiminde noktalı virgülsüz bir sonuç ifadesi olarak bırakmak standart yaklaşımdır.