Yaygın Veri Türleri
Fonksiyon imzaları veya yapılar gibi öğeler için tanımlar oluşturmak için yaygınları kullanırız ve bunları daha sonra birçok farklı
somut veri türüyle kullanabiliriz. İlk olarak yaygınları kullanarak fonksiyonları, yapıları, enum
'ları ve metodları nasıl tanımlayacağımıza
bakalım. Daha sonra yaygınların kod performansını nasıl etkilediğini tartışacağız.
Fonksiyon Tanımlarında
Yaygın kullanan bir fonksiyon tanımlarken, yaygınları fonksiyonun imzasına, genellikle parametrelerin ve dönüş değerinin veri tiplerini belirttiğimiz yere yerleştiririz. Bunu yapmak kodumuzu daha esnek hale getirir ve kod tekrarını önlerken fonksiyonumuzu çağıranlara daha fazla işlevsellik sağlar.
largest
fonksiyonumuzla devam edersek, Liste 10-4'te her ikisi de bir dilimdeki en büyük değeri bulan iki fonksiyon gösterilmektedir.
Daha sonra bunları yaygın kullanan tek bir fonksiyonda birleştireceğiz.
Dosya adı: src/main.rs
fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {}", result); assert_eq!(result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {}", result); assert_eq!(result, 'y'); }
largest_i32
fonksiyonu, bir dilimdeki en büyük i32
'yi bulan Liste 10-3'te çıkardığımız fonksiyondur.
largest_char
fonksiyonu bir dilimdeki en büyük char
değerini bulur. Fonksiyon gövdeleri aynı koda sahiptir,
bu nedenle tek bir fonksiyona yaygın tür parametresi ekleyerek yinelemeyi ortadan kaldıralım.
Yeni bir tek fonksiyonda türleri parametrelendirmek için, tıpkı bir fonksiyonun değer parametreleri için yaptığımız gibi tür
parametresini adlandırmamız gerekir. Tür parametresi adı olarak herhangi bir tanımlayıcı kullanabilirsiniz.
Ancak biz T
kullanacağız çünkü Rust'ta parametre adları genellikle sadece bir harf olmak üzere kısadır ve Rust'ın tür adlandırma kuralı
CamelCase
'dir. “tür, type” kelimesinin kısaltması olan T
, çoğu Rust programcısının varsayılan tercihidir.
Fonksiyonun gövdesinde bir parametre kullandığımızda, parametre adını imzada bildirmemiz gerekir, böylece derleyici bu adın ne anlama geldiğini
bilir. Benzer şekilde, bir fonksiyon imzasında bir tür parametre adı kullandığımızda, kullanmadan önce tür parametre adını bildirmemiz gerekir.
Yaygın largest
fonksiyonunu tanımlamak için, tür adı bildirimlerini fonksiyonun adı ile parametre listesi arasına köşeli parantezler
(<>
) içinde yerleştirin, aşağıdaki gibi:
fn largest<T>(list: &[T]) -> T {
Bu tanımı şu şekilde okuyabiliriz: largest
fonksiyonu bazı T
türleri üzerinde yaygındır. Bu fonksiyonun list
adında bir
parametresi vardır ve bu parametre T
türünde bir değer dilimidir. largest
fonksiyonu aynı T
türünde bir değer döndürecektir.
Liste 10-5, imzasında yaygın veri tipini kullanan birleşik largest
fonksiyon tanımını gösterir.
Liste ayrıca, fonksiyonu i32
değerlerinden oluşan bir dilim ya da char
değerleriyle nasıl çağırabileceğimizi de gösterir.
Bu kodun henüz derlenmeyeceğini unutmayın, ancak bu bölümün ilerleyen kısımlarında bunu düzelteceğiz.
Dosya adı: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
Kodu şimdi derlemeye çalışırsak, şu hatayı alırız:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error
Notta bir tanım olan std::cmp::PartialOrd
'dan bahsedilmektedir. Tanımlar hakkında bir sonraki bölümde konuşacağız.
Şimdilik, bu hatanın largest
'in gövdesinin T
'nin olabileceği tüm olası türler için çalışmayacağını belirttiğini bilin.
Gövdede T
türündeki değerleri karşılaştırmak istediğimiz için, yalnızca değerleri sıralanabilen türleri kullanabiliriz.
Karşılaştırmaları etkinleştirmek için, standart kütüphanede türler üzerinde uygulayabileceğiniz std::cmp::PartialOrd
tanımı vardır
(bu tanım hakkında daha fazla bilgi için Ekleme C'ye bakın). Yaygın bir türün belirli bir tanıma sahip olduğunu nasıl belirteceğinizi
“Parametre Olarak Tanımlar” bölümünde öğreneceksiniz.
Bu kodu düzeltmeden önce (“Tanım Sınırları ile Fonksiyonu Düzeltme” bölümünde), yaygın tür parametrelerini
kullanmanın diğer yollarını inceleyelim.
Struct Tanımlarında
Ayrıca, <>
söz dizimini kullanarak bir veya daha fazla alanda yaygın tür parametresi kullanmak için
struct
'ları tanımlayabiliriz. Liste 10-6, herhangi bir türdeki x
ve y
koordinat değerlerini tutmak için bir
Point<T>
struct
'ı tanımlar.
Dosya adı: src/main.rs
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
Yapı tanımlarında yaygın türlerin kullanımı için söz dizimi, fonksiyon tanımlarında kullanılan söz dizimine benzer.
İlk olarak, struct
adından hemen sonra köşeli parantezler içinde tür parametresinin adını bildiririz.
Ardından, struct
tanımında somut veri türlerini belirteceğimiz yerde yaygın türü kullanırız.
Point<T>
'yi tanımlamak için yalnızca bir yaygın tür kullandığımızdan, bu tanımın Point<T>
yapısının bazı T
türleri üzerinde
yaygın olduğunu ve x
ve y
üyelerinin her ikisinin de, bu tür ne olursa olsun, aynı tür olduğunu söylediğine dikkat edin.
Liste 10-7'de olduğu gibi, farklı türlerde değerlere sahip bir Point<T>
tanımı oluşturursak, kodumuz derlenmeyecektir.
Dosya adı: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
Bu örnekte, x
'e 5
tam sayı değerini atadığımızda, derleyiciye T
yaygın türünün bu Point<T>
tanımı için bir tam sayı olacağını bildiririz.
Daha sonra, x
ile aynı türe sahip olacak şekilde tanımladığımız y
için 4.0
değerini belirttiğimizde, aşağıdaki gibi tür
uyuşmazlığı hatası alırız:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error
x
ve y
'nin her ikisinin de yaygın olduğu ancak farklı türlere sahip olabileceği bir Point
yapısını tanımlamak için birden fazla
yaygın tür parametresi kullanabiliriz. Örneğin, Liste 10-8'de, Point
tanımını T
ve U
türleri üzerinde yaygın olacak şekilde
değiştiririz; burada x
T
tipinde ve y
U
tipindedir.
Dosya adı: src/main.rs
struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; }
Artık gösterilen tüm Point
tanımlarına izin verilmektedir! Bir tanımda istediğiniz kadar yaygın tür parametresi kullanabilirsiniz,
ancak birkaç taneden fazla kullanmak kodunuzun okunmasını zorlaştırır. Kodunuzda çok sayıda yaygın türe ihtiyaç duyuyorsanız,
bu kodunuzun daha küçük parçalar halinde yeniden yapılandırılması gerektiğini gösterebilir.
enum
Tanımlarında
Yapılarda yaptığımız gibi, yaygın veri türlerini varyantlarında tutmak için enum
'ları tanımlayabiliriz.
Standart kütüphanenin sağladığı ve Bölüm 6'da kullandığımız Option<T>
enum
'una bir kez daha göz atalım:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Bu tanım şimdi size daha anlamlı gelecektir. Gördüğünüz gibi Option<T>
enum
'u T
türü üzerinde yaygındır ve iki çeşidi vardır:
T
türünde bir değer tutan Some
ve herhangi bir değer tutmayan None
varyantı. Option<T>
enum
'unu kullanarak,
isteğe bağlı bir değerin soyut kavramını ifade edebiliriz ve Option<T>
yaygın olduğu için, isteğe bağlı değerin türü ne olursa
olsun bu soyutlamayı kullanabiliriz.
enum
'lar birden fazla yaygın tür de kullanabilir. Bölüm 9'da kullandığımız Result
enum
tanımı buna bir örnektir:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Result
enum
'u, T
ve E
olmak üzere iki tür üzerinde yaygındır ve iki çeşidi vardır: T
türünde bir değer tutan Ok
ve
E
türünde bir değer tutan Err
. Bu tanım, başarılı (T
türünde bir değer döndüren) veya başarısız (E
türünde bir hata döndüren)
olabilecek bir işlemimiz olan her yerde Result
enum
'unu kullanmayı kolaylaştırır. Aslında, Liste 9-3'te bir dosyayı açmak
için kullandığımız şey buydu; dosya başarıyla açıldığında T
, std::fs::File
türüyle atandı ve dosyanın açılmasında sorun olduğunda
E
, std::io::Error
türüyle atandı.
Kodunuzda, yalnızca tuttukları değerlerin türlerinde farklılık gösteren birden fazla struct
veya enum
tanımının bulunduğu
durumları fark ettiğinizde, bunun yerine yaygın türleri kullanarak yinelemeyi önleyebilirsiniz.
Metod Tanımlarında
Yapılar ve enum
'lar üzerinde metodlar uygulayabilir (Bölüm 5'te yaptığımız gibi) ve tanımlarında yaygın türleri kullanabiliriz.
Liste 10-9, Liste 10-6'da tanımladığımız Point<T>
yapısını ve üzerinde uygulanan x
adlı bir metodu göstermektedir.
Dosya adı: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
Burada, x
üyesindeki verilere bir referans döndüren Point<T>
üzerinde x
adında bir metod tanımladık.
T
'yi impl
'den hemen sonra bildirmemiz gerektiğine dikkat edin, böylece T
'yi Point<T>
türünde metodlar tanımladığımızı belirtmek için
kullanabiliriz. T
'yi impl
'den sonra yaygın bir tür olarak bildirerek, Rust, Point
'teki köşeli parantez içindeki türün somut bir tür
yerine yaygın bir tür olduğunu belirleyebilir. Bu yaygın parametre için struct
tanımında bildirilen yaygın parametreden farklı bir
isim seçebilirdik, ancak aynı ismi kullanmak gelenekseldir. Yaygın türü bildiren bir impl
içinde yazılan metodlar,
yaygın türün yerine hangi somut tür geçerse geçsin, türün herhangi bir tanımı üzerinde tanımlanacaktır.
Tür üzerinde metod tanımlarken yaygın türler üzerinde kısıtlamalar da belirtebiliriz.
Örneğin, herhangi bir yaygın türe sahip Point<T>
tanımları yerine yalnızca Point<f32>
tanımları üzerinde metodlar uygulayabiliriz.
Liste 10-10'da somut f32
türünü kullanıyoruz, yani impl
'den sonra herhangi bir tür bildirmiyoruz.
Dosya adı: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
Bu kod, Point<f32>
türünün bir distance_from_origin
metoduna sahip olacağı anlamına gelir;
T
'nin f32
türünde olmadığı diğer Point<T>
örneklerinde bu metod tanımlı olmayacaktır. Metod, noktamızın (0.0, 0.0)
koordinatlarındaki noktadan ne kadar uzakta olduğunu ölçer ve yalnızca kayan nokta türleri için kullanılabilen matematiksel işlemleri kullanır.
Bir struct
tanımındaki yaygın tür parametreleri her zaman aynı struct
'ın metod imzalarında kullandıklarınızla aynı değildir.
Liste 10-11, örneği daha açık hale getirmek için Point
struct
'ı için X1
ve Y1
yaygın türlerini ve mixup
metod imzası için X2
Y2
'yi
kullanır. Metod, kendi Point
'inden (X1
türünde) alınan x
değeri ve aktarılan Point
'ten (Y2
türünde) alınan y
değeriyle yeni bir
Point
tanımı oluşturur.
Dosya adı: src/main.rs
struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
main
'de, x
için bir i32
(değeri 5
) ve y
için bir f64
(değeri 10,4
) olan bir Point
tanımladık. p2
değişkeni,
x
için bir dizgi dilimine ("Hello"
değeriyle) ve y
için bir char
değerine (c
değeriyle) sahip bir Point
struct
'tır.
p1
üzerinde p2
argümanıyla mixup
çağrıldığında, x
p1
'den geldiği için x
için bir i32
'ye sahip olan p3
elde edilir.
p3
değişkeninde y
için bir char
olacaktır, çünkü y
p2
'den gelmiştir. println!
makro çağrısı p3.x = 5, p3.y = c
yazdıracaktır.
Bu örneğin amacı, bazı yaygın parametrelerin impl
ile bildirildiği ve bazılarının metod tanımıyla bildirildiği bir durumu göstermektir.
Burada, X1
ve Y1
yaygın parametreleri impl
'den sonra bildirilir, çünkü bunlar struct
tanımıyla birlikte tanımlanmıştır.
X2
ve Y2
yaygın parametreleri fn mixup
'tan sonra bildirilir, çünkü bunlar yalnızca metodla ilgilidir.
Yaygınları Kullanan Kodun Performansı
Yaygın tür parametrelerini kullanırken bir çalışma zamanı maliyeti olup olmadığını merak ediyor olabilirsiniz. İyi haber şu ki, yaygın türleri kullanmak çalışmanızı somut tiplere göre daha yavaş hale getirmeyecektir.
Rust bunu, derleme zamanında yaygınları kullanarak kodun monomorfizasyonunu gerçekleştirerek başarır. Monomorfizasyon, derlendiğinde kullanılan somut tiplerin içini doldurarak genel kodu belirli bir koda dönüştürme işlemidir. Bu süreçte derleyici, Liste 10-5'teki yaygın fonksiyonu oluşturmak için kullandığımız adımların tersini yapar: derleyici, genel kodun çağrıldığı tüm yerlere bakar ve genel kodun çağrıldığı somut türler için kod oluşturur.
Standart kütüphanenin yaygın Option<T>
enum
'unu kullanarak bunun nasıl çalıştığına bakalım:
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
Rust bu kodu derlediğinde, monomorflaştırma gerçekleştirir. Bu işlem sırasında, derleyici Option<T>
tanımlarında kullanılan değerleri
okur ve iki tür Option<T>
tanımlar: biri i32
ve diğeri f64
. Bu nedenle, Option<T>
'nin yaygın tanımını Option_i32
ve
Option_f64
olarak genişletir, böylece genel tanımı özel olanlarla değiştirir.
Kodun monomorfize edilmiş versiyonu aşağıdaki gibi görünür:
Dosya adı: src/main.rs
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
Yaygın Option<T>
, derleyici tarafından oluşturulan özel tanımlarla değiştirilir.
Rust, yaygın kodu her örnekte türü belirten koda derlediğinden, yaygınları kullanmak için çalışma zamanı maliyeti ödemeyiz.
Kod çalıştığında, her bir tanımı elle çoğaltmış olsaydık nasıl çalışacaksa öyle çalışır. Monomorfizasyon süreci,
Rust'ın yaygınlarını çalışma zamanında son derece verimli hale getirir.