Rust'ta operatör aşırı yükleme, kendi tipleriniz için +, -, *, [] gibi operatörlerin ne yapacağını std::ops modülündeki trait'leri uygulayarak belirlemek demektir. a + b ifadesi derleyici için Add::add(a, b) çağrısından ibarettir; tipiniz Add uyguluyorsa toplama yazabilirsiniz, uygulamıyorsa derleme hatası alırsınız. Rust yeni operatör icat etmenize izin vermez ve yalnızca bir trait'le desteklenen operatörler aşırı yüklenebilir. Atama operatörü = ile kısa devre yapan && ve || bu yüzden aşırı yüklenemez.
Bu yazıda Add, Sub, Mul, Neg ve AddAssign uygulamalarını bir vektör tipi üzerinde görüyor, Rhs varsayılan tip parametresini ve Output ilişkili tipini açıklıyor, referanslar için operatör tanımlamayı, Index ile köşeli parantez erişimini, orphan kuralını ve generic kodda operatör sınırlarını inceliyoruz. Trait tanımlamanın temelleri için trait yazısına, karşılaştırma operatörlerinin derive ile otomatik üretilmesi için derive yazısına bakabilirsiniz.
Add trait'i ve Output ilişkili tipi

Std belgesindeki tanım kısadır: pub trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; }. Burada iki yeni kavram var. Rhs, sağ taraftaki işlenenin tipini belirleyen generic bir parametredir ve varsayılan değeri Self'tir; yani bir şey belirtmezseniz aynı tipten iki değeri toplarsınız. Output ise bir ilişkili tiptir ve işlemin sonucunun tipini belirler. Sonucun işlenenlerle aynı tipte olması gerekmez; std belgesi SystemTime ile Duration toplamının yine SystemTime verdiğini örnek gösterir.
İlişkili tip ile generic parametre arasındaki fark kitapta Iterator üzerinden açıklanır. Generic bir parametre, aynı tip için trait'in birden çok kez, farklı parametrelerle uygulanabilmesine izin verir. İlişkili tip ise her uygulama için tek bir seçim yapılmasını sağlar. Add ikisini birlikte kullanır: Vektor için hem Add<Vektor> hem Add<f64> uygulayabilirsiniz, ama her birinin sonucu tek bir Output tipidir. Böylece a + b yazdığınızda derleyici sonucun tipini, işlenenlerin tiplerinden kesin olarak bilir.
use std::ops::{Add, AddAssign, Mul, Neg, Sub};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vektor {
x: f64,
y: f64,
}
impl Add for Vektor {
type Output = Vektor;
fn add(self, diger: Vektor) -> Vektor {
Vektor { x: self.x + diger.x, y: self.y + diger.y }
}
}
impl Sub for Vektor {
type Output = Vektor;
fn sub(self, diger: Vektor) -> Vektor {
Vektor { x: self.x - diger.x, y: self.y - diger.y }
}
}
impl Mul<f64> for Vektor {
type Output = Vektor;
fn mul(self, k: f64) -> Vektor {
Vektor { x: self.x * k, y: self.y * k }
}
}
impl Neg for Vektor {
type Output = Vektor;
fn neg(self) -> Vektor {
Vektor { x: -self.x, y: -self.y }
}
}
impl AddAssign for Vektor {
fn add_assign(&mut self, diger: Vektor) {
self.x += diger.x;
self.y += diger.y;
}
}
fn main() {
let a = Vektor { x: 1.0, y: 2.0 };
let b = Vektor { x: 0.5, y: -1.0 };
println!("{:?}", a + b);
println!("{:?}", a - b);
println!("{:?}", a * 3.0);
println!("{:?}", -a);
let mut konum = a;
konum += b;
konum += b;
println!("{:?}", konum);
println!("{}", a + b == Vektor { x: 1.5, y: 1.0 });
}
// çıktı: Vektor { x: 1.5, y: 1.0 }
// çıktı: Vektor { x: 0.5, y: 3.0 }
// çıktı: Vektor { x: 3.0, y: 6.0 }
// çıktı: Vektor { x: -1.0, y: -2.0 }
// çıktı: Vektor { x: 2.0, y: 0.0 }
// çıktı: trueÖrnekte dört farklı trait var. Add ve Sub iki Vektor alır. Mul<f64> sağ tarafı f64 olan bir çarpma tanımlar; bu sayede a * 3.0 yazılabiliyor. Dikkat: bu yalnızca Vektor * f64 sırasını tanımlar, 3.0 * a için ayrıca f64 üzerinde Mul<Vektor> uygulamanız gerekir. Neg tekli eksi operatörüdür ve tek işlenen alır. AddAssign ise += operatörünü sağlar; dönüş değeri yoktur, &mut self alıp değeri yerinde günceller. Add uygulamak += operatörünü kendiliğinden getirmez, ikisini ayrı ayrı uygulamanız gerekir.
Son satırdaki == karşılaştırması PartialEq trait'inden gelir ve derive ile otomatik üretildi. Karşılaştırma operatörleri de trait tabanlıdır: == ve != için PartialEq, <, <=, > ve >= için PartialOrd. Clone ve Copy türetmemizin sebebi ise operatör metotlarının işlenenleri değer olarak almasıdır. Copy olmasaydı a + b ifadesi a ve b değerlerini taşıyacak ve sonraki satırlarda kullanamayacaktık.
Örnekte use std::ops::{Add, AddAssign, Mul, Neg, Sub}; satırına dikkat edin. Std belgesi bu modüldeki trait'lerin bir kısmının prelude ile her programa otomatik geldiğini belirtir; aritmetik operatör trait'lerini ise uygulayacağınız zaman kapsama almanız gerekir. Operatörü yalnızca kullanmak için, yani a + b yazmak için trait'i içe aktarmanız gerekmez; içe aktarma impl Add for ... satırındaki trait adını çözmek içindir. İsterseniz impl std::ops::Add for Vektor diyerek tam yolu da yazabilirsiniz. Modül yollarının ve use yazımının kuralları modül sistemi yazısında anlatılıyor.
Trait uygulanmamışsa ne olur?
Kendi tipiniz üzerinde bir operatör kullandığınızda derleyici ilgili trait uygulamasını arar. Bulamazsa E0369 hatası verir ve hangi trait'in eksik olabileceğini not olarak ekler. Hata kodu sayfası da aynı şeyi önerir: tipin bu ikili işlemi destekleyip desteklemediğini kontrol edin, kendi tipinizse std::ops trait'lerinden birini uygulayın. Aşağıdaki Para tipi bir sayı sarmalasa bile toplama işlemi kendiliğinden gelmez.
struct Para(u32);
fn main() {
let a = Para(5);
let b = Para(7);
let c = a + b;
}
// error[E0369]: cannot add `Para` to `Para`
// note: an implementation of `Add` might be missing for `Para`Farklı sağ taraf tipi ve referanslar için operatör

Rhs parametresini değiştirerek farklı tipleri birleştirebilirsiniz. Kitabın örneğinde milimetre ve metre ayrı newtype'lar olarak tanımlanır ve impl Add<Metre> for Milimetre ile birim dönüşümü toplama işleminin içine gömülür. Kitap varsayılan tip parametrelerinin iki amaçla kullanıldığını söyler: var olan kodu bozmadan bir tipi genişletmek ve çoğu kullanıcının ihtiyaç duymayacağı özel durumları mümkün kılmak. Add trait'indeki Rhs = Self varsayılanı ikinci amaca hizmet eder: çoğu zaman aynı tipleri toplarsınız ve fazladan bir şey yazmanız gerekmez.
Operatörler işlenenleri değer olarak aldığı için Copy olmayan tiplerde her toplama değerleri tüketir. Std belgesi bu durumda, generic kodun gereksiz klonlama yapmadan çalışabilmesi için tipin hem değer hem referans için operatör uygulamasını önerir. Aşağıdaki Matris bir Vec içerdiği için Copy olamaz. impl Add for &Matris yazarak toplama işlemini referanslar üzerinde tanımladık; &m1 + &m2 iki matrisi tüketmeden yeni bir matris üretiyor ve sonrasında m1 ve m2 hâlâ kullanılabiliyor. Std belgesinin önerisi, hem T hem &T için Add<T> ve Add<&T> uygulamaktır; bu, T + T, T + &T, &T + T ve &T + &T olmak üzere dört kombinasyon eder.
use std::ops::Add;
#[derive(Debug)]
struct Milimetre(u32);
struct Metre(u32);
impl Add<Metre> for Milimetre {
type Output = Milimetre;
fn add(self, m: Metre) -> Milimetre {
Milimetre(self.0 + m.0 * 1000)
}
}
#[derive(Debug, Clone)]
struct Matris(Vec<i32>);
impl Add for &Matris {
type Output = Matris;
fn add(self, diger: &Matris) -> Matris {
Matris(self.0.iter().zip(&diger.0).map(|(a, b)| a + b).collect())
}
}
fn main() {
println!("{:?}", Milimetre(250) + Metre(2));
let m1 = Matris(vec![1, 2, 3]);
let m2 = Matris(vec![10, 20, 30]);
let m3 = &m1 + &m2;
println!("{:?} {:?} {:?}", m1, m2, m3);
}
// çıktı: Milimetre(2250)
// çıktı: Matris([1, 2, 3]) Matris([10, 20, 30]) Matris([11, 22, 33])Referans uygulaması olmasaydı ve m1 + m2 yazsaydınız, toplamadan sonra m1 değerini yazdırma denemesi taşınmış değer hatasına dönüşürdü. Bu hatanın mantığını E0382 yazısında ayrıntılı anlatıyoruz. Zincirdeki zip ve collect kullanımının açıklaması ise iterator yazısında.
Index ve IndexMut: köşeli parantez erişimi
Index trait'i kap[indeks] sözdizimini sağlar. Std belgesine göre bu ifade, değiştirilemez bir bağlamda *kap.index(indeks) çağrısının kısaltmasıdır; değiştirilebilir bir değer istendiğinde ise IndexMut kullanılır. index metodu bir referans döndürür ve ifade bunu otomatik olarak dereference eder. Bu yüzden eleman tipi Copy ise let x = plan[gun]; gibi bir yazım değeri kopyalar. İndeks tipi tam sayı olmak zorunda değildir; aşağıda bir enum'ı indeks olarak kullanıyoruz ve gun as usize dönüşümüyle diziye yönlendiriyoruz. += satırı IndexMut sayesinde çalışıyor.
use std::ops::{Index, IndexMut};
#[derive(Debug, Clone, Copy)]
enum Gun {
Pazartesi,
Sali,
Carsamba,
}
struct HaftalikPlan {
saatler: [u8; 3],
}
impl Index<Gun> for HaftalikPlan {
type Output = u8;
fn index(&self, gun: Gun) -> &u8 {
&self.saatler[gun as usize]
}
}
impl IndexMut<Gun> for HaftalikPlan {
fn index_mut(&mut self, gun: Gun) -> &mut u8 {
&mut self.saatler[gun as usize]
}
}
fn main() {
let mut plan = HaftalikPlan { saatler: [2, 3, 1] };
println!("{}", plan[Gun::Sali]);
plan[Gun::Carsamba] += 4;
println!("{}", plan[Gun::Carsamba]);
println!("{:?}={}", Gun::Pazartesi, plan[Gun::Pazartesi]);
}
// çıktı: 3
// çıktı: 5
// çıktı: Pazartesi=2Index uygulamalarında geçersiz bir indeks verildiğinde ne olacağına siz karar verirsiniz. Standart kütüphanedeki dilim ve vektörler sınır dışı erişimde panikler; olmayabilecek bir eleman için get gibi Option döndüren bir metot sunmak da yaygındır. Bu tasarım farkını Vec yazısında v[i] ve v.get(i) karşılaştırmasıyla görebilirsiniz.
Orphan kuralı ve newtype deseni
Bir trait'i bir tipe uygulayabilmek için trait'in ya da tipin sizin crate'inizde tanımlı olması gerekir; buna orphan kuralı denir. Add trait'i de Vec tipi de standart kütüphaneye ait olduğundan impl Add for Vec<i32> yazamazsınız ve derleyici E0117 ile reddeder. Bu kural, iki farklı crate'in aynı tip için çakışan uygulamalar yazmasını önler.
use std::ops::Add;
impl Add for Vec<i32> {
type Output = Vec<i32>;
fn add(self, diger: Vec<i32>) -> Vec<i32> {
self.into_iter().chain(diger).collect()
}
}
fn main() {}
// error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crateÇözüm newtype desenidir: dış tipi tek alanlı bir tuple struct ile sararsınız ve trait'i bu yerel tipe uygularsınız. Kitaba göre bu sarmalayıcının çalışma zamanı maliyeti yoktur, derleme sırasında ortadan kalkar. Yukarıdaki Milimetre, Metre ve Matris tam olarak bu desenin örnekleridir. Sarmalayıcının dezavantajı, iç tipin metotlarına kendiliğinden sahip olmamasıdır; ihtiyaç duyduğunuz metotları elle iletirsiniz ya da sarmalayıcı işaretçi gibi davranıyorsa Deref uygularsınız. Deref ayrıntıları Box yazısında anlatılıyor.
Generic kodda operatör sınırları
Operatör trait'leri generic fonksiyonlarda sınır olarak da kullanılır. Aşağıdaki topla fonksiyonu, toplanabilen ve kopyalanabilen her tiple çalışır. T: Add<Output = T> sınırı iki şey söyler: T kendisiyle toplanabilir ve toplamın sonucu yine T olur. Output = T kısmını yazmasaydınız sonucu toplam değişkenine atayamazdınız, çünkü derleyici sonucun tipini bilmezdi. Aynı fonksiyon tam sayılarla da kayan noktalı sayılarla da çalışıyor; standart kütüphane bu tipler için Add uygulamalarını zaten sağlar.
use std::ops::Add;
fn topla<T: Add<Output = T> + Copy>(liste: &[T], sifir: T) -> T {
let mut toplam = sifir;
for &x in liste {
toplam = toplam + x;
}
toplam
}
fn main() {
println!("{}", topla(&[1, 2, 3], 0));
println!("{}", topla(&[0.5, 0.25], 0.0));
}
// çıktı: 6
// çıktı: 0.75Pratikte bir dizi değeri toplamak için iter().sum() daha kısadır; buradaki fonksiyon sınırların nasıl yazıldığını göstermek içindir. Generic parametrelerin ve where yazımının ayrıntıları generics yazısında bulunuyor.
Aşırı yüklenebilen operatörler tablosu

Kitabın operatör ekindeki tabloya göre aşağıdaki operatörler bir trait ile desteklenir. Bileşik atama sürümleri ayrı trait'lerdir. Ek, .. ve ..= aralık ifadelerinin yanına da PartialOrd yazar. ?, &&, ||, = ve ödünç alma için kullanılan & operatörü listede trait'siz olarak geçer.
| Operatör | Trait | Metot | Bileşik atama trait'i |
|---|---|---|---|
| a + b | Add | add | AddAssign (+=) |
| a - b | Sub | sub | SubAssign (-=) |
| a * b | Mul | mul | MulAssign (*=) |
| a / b | Div | div | DivAssign (/=) |
| a % b | Rem | rem | RemAssign (%=) |
| -a | Neg | neg | yok |
| !a | Not | not | yok |
| a & b, a | b, a ^ b | BitAnd, BitOr, BitXor | bitand, bitor, bitxor | BitAndAssign, BitOrAssign, BitXorAssign |
| a << b, a >> b | Shl, Shr | shl, shr | ShlAssign, ShrAssign |
| *a | Deref / DerefMut | deref, deref_mut | yok |
| a[i] | Index / IndexMut | index, index_mut | yok |
| a == b, a != b | PartialEq | eq, ne | yok |
| a < b, a <= b, a > b, a >= b | PartialOrd | partial_cmp, lt, le, gt, ge | yok |
Std belgesi ayrıca Fn, FnMut ve FnOnce trait'lerinin de bu modülde yer aldığını hatırlatır; bunlar bir değerin fonksiyon gibi çağrılmasını sağlayan trait'lerdir ve closure yazısında işleniyor.
Sık yapılan hatalar
- Beklenmedik anlam yüklemek. Std belgesi operatör uygulamalarının bağlamında şaşırtıcı olmamasını, örneğin
Muluygulamasının çarpmaya benzemesini ve birleşme gibi beklenen özellikleri korumasını önerir.+ile bir veritabanına kayıt atmak gibi yan etkili tasarımlardan kaçının. - Add uygulayıp += beklemek. Bileşik atamalar ayrı trait'lerdir;
AddAssignuygulamadan+=çalışmaz. - Operatörün işlenenleri taşıdığını unutmak.
Copyolmayan tiplerdea + biki değeri de tüketir. Referans uygulamaları ekleyin ya da bilinçli olarak klonlayın. - Sıranın önemli olduğunu gözden kaçırmak.
Vektor * f64tanımlamakf64 * Vektorsırasını tanımlamaz. - Dış tip için doğrudan uygulamaya çalışmak. Orphan kuralı yüzünden E0117 alırsınız; newtype ile sarın.
Sık Sorulan Sorular
Rust'ta kendi operatörümü tanımlayabilir miyim?
Hayır. std::ops yalnızca var olan operatörleri aşırı yüklemeye izin verir. Belge, trait'siz aşırı yükleme ya da yeni operatör gerekiyorsa sözdizimini makrolarla genişletmeyi önerir; makro yazımı için macro_rules yazısına bakabilirsiniz.
&& ve || neden aşırı yüklenemiyor?
Std belgesine göre bu iki operatör kısa devre yaptığı için diğer operatörlerden farklı bir tasarım gerektirir ve bu tasarım hâlâ tartışılmaktadır. Bit düzeyindeki & ve | ise BitAnd ve BitOr ile aşırı yüklenebilir.
Operatör uygulaması performansı düşürür mü?
Operatör ifadesi sıradan bir metot çağrısına dönüşür ve generic koddaki diğer çağrılar gibi derlenir. Asıl maliyet, uygulamanızın içinde ne yaptığınıza bağlıdır; örneğin her toplamada yeni bir Vec ayırmak bir maliyettir.
PartialEq'i elle mi yazmalıyım, derive mı etmeliyim?
Bütün alanların eşitliği yeterliyse #[derive(PartialEq)] en doğru seçimdir. Yalnızca bazı alanlara göre karşılaştırmak gibi özel bir anlam istiyorsanız elle uygularsınız. Ayrıntılar derive yazısında.
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 · Trait tanımlama ve impl · derive ile Debug, Clone, PartialEq · Generics · Display ve Debug formatlama · Rust nedir?