Numaralandırılmış Yapı Tanımlamak
Yapıların size width
ve height
üyeleri olan bir Rectangle
gibi ilgili üyeleri ve
verileri gruplandırmanın bir yolunu verdiği yerde, numaralandırmalar size bir değerin
olası bir değer kümesinden biri olduğunu söylemenin bir yolunu verir.
Örneğin, Rectangle
'ın Circle
ve Triangle
'ı da içeren olası şekillerden biri
olduğunu söylemek isteyebiliriz. Bunu yapmak için Rust, bu olasılıkları bir enum
olarak kodlamamıza izin verir.
Kodda ifade etmek isteyebileceğimiz bir duruma bakalım ve bu durumda numaralandırmaların neden yapılardan daha yararlı ve daha uygun olduğunu görelim. IP adresleriyle çalışmamız gerektiğini farz edin. Şu anda IP adresleri için iki ana standart kullanılmaktadır: V4 ve V6. Programımızın karşılaşacağı bir IP adresi için tek olasılık bunlar olduğundan, tüm olası değişkenleri sıralayabiliriz, bu da numaralandırmanın adını aldığı yerdir.
Herhangi bir IP adresi, V4 veya V6 adresi olabilir,
ancak ikisi aynı anda olamaz. IP adreslerinin bu özelliği, enum
veri yapısını
uygun hale getirir, çünkü bir enum
değeri onun türevlerinden yalnızca biri olabilir.
Hem V4 hem de V6 adresleri hala temelde IP adresleridir, bu nedenle kod herhangi bir
IP adresi için geçerli olan durumları işlerken aynı tür olarak ele alınmalıdır.
Bu kavramı bir IpAddrKind
numaralandırması tanımlayarak ve bir IP adresinin olabileceği
olası türleri V4 ve V6 olarak listeleyerek kodda ifade edebiliriz.
Bunlar, numaralandırmanın varyantlarıdır:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
IpAddrKind
artık kodumuzun başka bir yerinde kullanabileceğimiz özel bir veri türüdür.
enum
Değerleri
IpAddrKind
'in iki varyantının her birinin örneklerini şu şekilde oluşturabiliriz:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Numaralandırmanın türevlerinin, tanımlayıcısının altında ad alanlı olduğuna ve
ikisini ayırmak için çift iki nokta üst üste (:
) kullandığımıza dikkat edin.
Bu kullanışlıdır çünkü artık her iki IpAddrKind::V4
ve IpAddrKind::V6
değeri aynı
türdedir: IpAddrKind
. Daha sonra, örneğin, herhangi bir IpAddrKind
alan bir
fonksiyon tanımlayabiliriz:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Ve bu fonksiyonu her iki değişkenle de çağırabiliriz:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Numaralandırma kullanmanın daha da fazla avantajı vardır. IP adresi türümüz hakkında daha fazla düşünürsek, şu anda gerçek IP adresi verilerini saklamanın bir yolu yok; sadece ne tür olduğunu biliyoruz. Bölüm 5'te yapılar hakkında yeni öğrendiğinize göre, bu sorunu Liste 6-1'de gösterildiği gibi yapılarla çözmeye cazip gelebilirsiniz.
fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
Burada, iki üyesi olan bir IpAddr
yapısı tanımladık:
IpAddrKind
türünde bir tür üyesi (daha önce tanımladığımız numaralandırma) ve
String
türünde bir adres üyesi. Bu yapının iki örneğine sahibiz.
Birincisi home
'dır ve 127.0.0.1
ilişkili adres verileriyle kendi türünde
IpAddrKind::V4
değerine sahiptir. İkinci örnek geri döngüdür. Tür değeri V6
olarak
IpAddrKind
'in diğer türevine sahiptir ve onunla ilişkili ::1
adresine sahiptir.
Tür ve adres değerlerini bir araya toplamak için bir yapı kullandık,
bu yüzden şimdi değişken değerle ilişkilendirildi.
Bununla birlikte, aynı kavramı yalnızca bir numaralandırma kullanarak temsil etmek daha özlüdür:
bir yapı içindeki bir numaralandırma yerine, verileri doğrudan her bir numaralandırma
değişkenine koyabiliriz. IpAddr
enum
'un bu yeni tanımı,
hem V4
hem de V6
varyantlarının ilişkili String
değerlerine sahip olacağını söylüyor:
fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
enum
'un her türevine doğrudan veri ekliyoruz, bu nedenle ekstra bir yapıya gerek yok.
Burada ayrıca numaralandırmaların nasıl çalıştığına dair başka bir ayrıntıyı görmek daha kolaydır:
tanımladığımız her bir numaralandırma değişkeninin adı aynı zamanda numaralandırmanın bir örneğini
oluşturan bir fonksiyon haline gelir. Diğer bir deyişle, IpAddr::V4()
,
bir String
bağımsız değişkeni alan ve IpAddr
türünün bir örneğini döndüren bir fonksiyon çağrısıdır.
enum
'u tanımlamanın bir sonucu olarak bu yapıcı fonksiyonu otomatik olarak tanımlarız.
Bir yapı yerine bir numaralandırma kullanmanın başka bir avantajı daha vardır: her değişken, farklı türde ve miktarda ilişkili veriye sahip olabilir. V4 tip IP adresleri her zaman 0 ile 255 arasında değerlere sahip dört sayısal bileşene sahip olacaktır:
fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
V4 ve V6 IP adreslerini depolamak için veri yapılarını tanımlamanın birkaç farklı yolunu gösterdik.
Ancak, ortaya çıktığı gibi, IP adreslerini saklamak ve hangi tür olduklarını kodlamak istemek o kadar
yaygın ki, standart kütüphanenin kullanabileceğimiz bir tanımı var!
Standart kütüphanenin IpAddr
'yi nasıl tanımladığına bakalım: tanımladığımız ve kullandığımız
tam enum
ve varyantlara sahiptir, ancak adres verilerini varyantların içine,
her varyant için farklı tanımlanmış iki farklı yapı şeklinde gömer:
#![allow(unused)] fn main() { struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
Bu kod, herhangi bir türde veriyi bir numaralandırma değişkeninin içine koyabileceğinizi gösterir: örneğin dizgiler, sayısal türler veya yapılar. Hatta başka bir numaralandırma ekleyebilirsiniz! Ayrıca, standart kütüphane türleri genellikle bulabileceklerinizden çok daha karmaşık değildir.
Standart kütüphanenin IpAddr
için bir tanım içermesine rağmen, standart kütüphanenin tanımını
kapsamımıza almadığımız için kendi tanımımızı çakışmadan oluşturup kullanabileceğimize dikkat edin.
Bölüm 7'de türleri kapsama almak hakkında daha fazla konuşacağız.
Liste 6-2'deki başka bir numaralandırma örneğine bakalım: bu, türevlerinde gömülü çok çeşitli türlere sahiptir.
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
Bu numaralandırmanın farklı türlerde dört çeşidi vardır:
Quit
onunla ilişkili hiçbir veriye sahip değil.Move
bir yapının yaptığı gibi alanları adlandırmıştır.Write
tek birString
'i dahil eder.ChangeColor
üç tanei32
değerini dahil eder.
Liste 6-2'dekiler gibi değişkenlerle bir numaralandırma tanımlamak,
farklı türde yapı tanımları tanımlamaya benzer, ancak numaralandırmanın struct
anahtar sözcüğünü
kullanmaması ve tüm değişkenlerin Message
türü altında gruplandırılması dışında.
Aşağıdaki yapılar, önceki numaralandırma değişkenlerinin sahip olduğu aynı verileri tutabilir:
struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct fn main() {}
Ancak, her biri kendi tipine sahip olan farklı yapıları kullanırsak,
bu tür mesajların herhangi birini almak için, tek bir mesaj olan Liste 6-2'de
tanımlanan Message
enum
'u ile yapabileceğimiz kadar kolay bir fonksiyon tanımlayamazdık.
Numaralandırmalar ve yapılar arasında bir benzerlik daha vardır:
impl
kullanarak yapılar üzerinde yöntemleri tanımlayabildiğimiz gibi,
enum
'lar üzerinde de yöntemler tanımlayabiliriz. İşte Message
enum
'umuzda
tanımlayabileceğimiz call
adında bir metod:
fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call(); }
Metodun gövdesi, metodu çağırdığımız değeri almak için self
'i kullanır.
Bu örnekte, m
adında Message::Write(String::from("hello"))
'yu tutan bir değişken oluşturduk
ve m.call()
çalıştığında self
call
metodunun gövdesinde olacak.
Standart kütüphanedeki çok yaygın ve kullanışlı olan başka bir numaralandırmaya bakalım:
Option
.
Option
Numaralandırması ve Null
Değerlerine Göre Avantajları
Bu bölüm, standart kütüphane tarafından tanımlanan başka bir numaralandırma olan
Option
'ın örnek olay incelemesini incelemektedir. Option
türü, bir değerin
bir şey olabileceği veya hiçbir şey olamayacağı çok yaygın senaryoyu kodlar.
Örneğin, öğeleri içeren bir listenin ilkini talep ederseniz, bir değer alırsınız. Boş bir listenin ilk maddesini talep ederseniz, hiçbir şey alamazsınız. Bu kavramı tür sistemi cinsinden ifade etmek, derleyicinin, ele almanız gereken tüm durumları ele alıp almadığınızı kontrol edebileceği anlamına gelir; bu işlevsellik, diğer programlama dillerinde son derece yaygın olan hataları önleyebilir.
Programlama dili tasarımı genellikle hangi özellikleri eklediğinize göre düşünülür, ancak hariç tuttuğunuz özellikler de önemlidir. Rust, diğer birçok dilde bulunan null özelliğine sahip değildir. null, hiçbir değer olmadığı anlamına gelen bir değerdir. null olan dillerde, değişkenler her zaman iki durumdan birinde olabilir: null veya null değil.
null'un mucidi Tony Hoare, “Null References: The Billion Dollar Mistake,” adlı 2009 sunumunda şunları söylüyor:
Ben buna milyar dolarlık hatam diyorum. O zaman, nesne yönelimli bir dilde referanslar için ilk kapsamlı tip sistemini tasarlıyordum. Amacım, derleyici tarafından otomatik olarak gerçekleştirilen kontrol ile referansların tüm kullanımının kesinlikle güvenli olmasını sağlamaktı. Ancak, uygulanması çok kolay olduğu için boş bir referans koymanın cazibesine karşı koyamadım. Bu, son kırk yılda muhtemelen milyarlarca dolarlık acıya ve hasara neden olan sayısız hataya, güvenlik açığına ve sistem çökmesine neden oldu.
Boş değerlerle ilgili sorun, boş olmayan bir değer olarak boş bir değer kullanmaya çalışırsanız, bir tür hata almanızdır. Bu boş veya boş olmayan özellik yaygın olduğundan, bu tür bir hatayı yapmak son derece kolaydır.
Bununla birlikte, null'un ifade etmeye çalıştığı kavram hala kullanışlıdır: null, şu anda geçersiz olan veya herhangi bir nedenle mevcut olmayan bir değerdir.
Sorun gerçekten konseptte değil, uygulanmasındadır. Bu nedenle, Rust'ın boş değerleri yoktur,
ancak var olan veya olmayan bir değer kavramını kodlayabilen bir numaralandırmaya sahiptir.
Bu numaralandırma Option<T>
'dir ve standart kütüphane tarafından
aşağıdaki gibi tanımlanır:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
Option<T>
enum
'u o kadar kullanışlıdır ki, girişe bile dahil edilmiştir;
Bunu açıkça kapsama sokmanız gerekmez. Varyantları da başlangıç bölümüne dahil edilmiştir:
Some
ve None
'ı doğrudan Option::
ön eki olmadan kullanabilirsiniz. Option<T>
numaralandırma
hala normal bir numaralandırmadır ve Some(T)
ve None
hala Option<T>
türünün varyantlarıdır.
<T>
sözdizimi, Rust'ın henüz bahsetmediğimiz bir özelliğidir.
Bu genel bir tür parametresidir ve yaygınları Bölüm 10'da daha ayrıntılı olarak ele alacağız.
Şimdilik, bilmeniz gereken tek şey <T>
'nin Option
enum
'unun Some
varyantının herhangi bir
türden tek bir veri parçasını tutabileceği anlamına geldiğidir.
Sayı türlerini ve dize türlerini tutmak için Option
değerlerini kullanmanın bazı örnekleri:
fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; }
some_number
türü Option<i32>
şeklindedir. some_char
türü, farklı bir tür olan
Option<char>
'dır. Rust, Some
varyantı içinde bir değer belirttiğimiz için bu türlerin
çıkarımını yapabilir. absent_number
için Rust, genel Otpion
türüne açıklama eklememizi gerektirir:
derleyici, yalnızca None
değerine bakarak karşılık gelen Some
varyantının tutacağı türü çıkaramaz.
Burada, absent_number
için Option<i32>
türünde olmasını kastettiğimizi Rust'a söylüyoruz.
Bir Some
değerine sahip olduğumuzda, bir değerin mevcut olduğunu ve değerin Some
içinde tutulduğunu
biliriz. None
değerine sahip olduğumuzda, bir anlamda null ile aynı anlama gelir:
geçerli bir değerimiz yoktur. Öyleyse neden Option<T>
seçeneğine sahip olmak boş değere sahip
olmaktan daha iyidir?
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
Bu kodu çalıştırırsak şöyle bir hata mesajı alırız:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error
Aslında bu hata mesajı, farklı türler oldukları için Rust'ın bir i8
ve bir Option<i8>
nasıl
ekleneceğini anlamadığı anlamına gelir. Rust'ta i8
gibi bir değerimiz olduğunda,
derleyici her zaman geçerli bir değere sahip olmamızı sağlayacaktır.
Bu değeri kullanmadan önce null değerini kontrol etmek zorunda kalmadan güvenle ilerleyebiliriz.
Yalnızca bir Option<i8>
olduğunda (veya hangi tür değerle çalışırsak çalışalım),
muhtemelen bir değere sahip olmama konusunda endişelenmemiz gerekir ve derleyici,
değeri kullanmadan önce bu durumu ele aldığımızdan emin olacaktır.
Başka bir deyişle, onunla T
işlemleri gerçekleştirmeden önce Option<T>
öğesini T
'ye dönüştürmeniz
gerekir. Genellikle bu, null ile ilgili en yaygın sorunlardan birini yakalamaya yardımcı olur:
bir şeyin gerçekte boş olmadığını varsaymak.
Yanlış bir şekilde boş olmayan bir değer varsayma riskini ortadan kaldırmak,
kodunuza daha fazla güvenmenize yardımcı olur. Muhtemelen null olabilecek bir değere sahip olmak için,
bu değerin türünü Option<T>
yaparak açıkça seçmelisiniz. Ardından, bu değeri kullandığınızda,
değer boş olduğunda durumu açıkça ele almanız gerekir. Bir değerin Option<T>
olmayan bir türü
olduğu her yerde, değerin boş olmadığını güvenle varsayabilirsiniz.
Bu, Rust'ın null'ın yaygınlığını sınırlamak ve Rust kodunun güvenliğini artırmak için kasıtlı bir
tasarım kararıydı.
Öyleyse, Option<T>
türünde bir değeriniz olduğunda, bu değeri kullanabilmeniz için Some
varyantından
T
değerini nasıl alırsınız? Option<T>
enum
'u, çeşitli durumlarda yararlı olan çok sayıda yönteme
sahiptir; dokümantasyonundan kontrol edebilirsiniz. Option<T>
üzerindeki metodlara aşina olmak,
Rust ile olan yolculuğunuzda son derece yararlı olacaktır.
Genel olarak, bir Option<T>
değeri kullanmak için her bir değişkeni işleyecek bir koda sahip olmak
istersiniz. Yalnızca bir Some(T)
değerine sahip olduğunuzda çalışacak bir kod istiyorsunuz
ve bu kodun iç T
'yi kullanmasına izin veriliyor. None
değeriniz varsa ve bu kodda başka bir kodun
çalıştırılmasını istiyorsunuz. bir T
değeri mevcuttur. match
ifadesi,
numaralandırmalarla kullanıldığında tam da bunu yapan bir kontrol akışı yapısıdır:
sahip olduğu numaralandırmanın hangi türevine bağlı olarak farklı kod çalıştırır ve bu kod,
eşleşen değerin içindeki verileri kullanabilir.