Благодаря системе трейтов Rust поддерживает перегрузку операторов (operator overloading), позволяя определять собственное поведение для таких операторов, как +, -, * и других. Перегрузка операторов позволяет сократить код, сделать его более выразительным.
Для определения операторов надо использовать один из трейтов из модуля std::ops. Поскольку список трейтов довольно большой и может изменяться, то я отмечу лишь некоторые, а полный список можно посмотреть по адресу https://doc.rust-lang.org/std/ops/index.html. В частности, мы можем использовать следующие трейты:
Add: оператор сложения +.
AddAssign: сложение с присвоением +=.
BitAnd: поразрядный оператор &.
BitAndAssign: поразрядный оператор с присвоением &=.
BitOr: поразрядный оператор |.
BitOrAssign: поразрядный оператор с присвоением |=.
BitXor: поразрядный оператор ^.
BitXorAssign: поразрядный оператор с присвоением ^=.
Deref: операция разыменования неизменяемой ссылки типа *v.
DerefMut: операция разыменования изменяемой ссылки типа *v = 1;.
Div: оператор деления /.
DivAssign: деление с присвоением /=.
Drop: кастомный код внутри деструктора
Fn: оператор вызова функции с передачей неизменяемого объекта
FnMut: оператор вызова функции с передачей изменяемого объекта
FnOnce: оператор вызова функции с передачей объекта по значению
Index: оператор индексации (container[index]) в неизменяемом контексте.
IndexMut: оператор индексации (container[index]) в изменяемом контексте.
Mul: оператор умножения *.
MulAssign: умножение с присвоением *=.
Neg: оператор арифметического отрицания -.
Not: оператор логического отрицания !.
RangeBounds: применяется для определения диапазонов, например, .., a.., ..b, ..=c, d..e, or f..=g.
Rem: оператор получения остатка от деления %.
RemAssign: остаток от деления с присвоением %=.
Shl: оператор сдвига влево <<
ShlAssign: сдвиг влево с присвоением <<=.
Shr: оператор сдвига вправо >>
ShrAssign: сдвиг вправо с присвоением >>=.
Sub: оператор вычитания -.
SubAssign: вычитание с присвоением -=.
Рассмотрим на примере реализации оператор сложения для кастомной структуры:
use std::ops::Add; // поключаем трейт Add из модуля std::ops
struct Counter{
value: u32
}
impl Add for Counter{
type Output = Counter; // тип генерируемого значения
fn add(self, other: Counter) -> Counter {
Counter {
value: self.value + other.value
}
}
}
fn main() {
let counter1 = Counter{value:5};
let counter2 = Counter{value:15};
let counter3 = counter1 + counter2;
println!("{}", counter3.value); // 20
}
Прежде всего для использования трейта нам надо подключить этот трейт:
use std::ops::Add;
Здесь определена структура Counter, которая представляет некоторое число, хранимое в поле value
struct Counter{
value: u32
}
Далее собственно реализуем трейт Add для структуры Counter:
impl Add for Counter{
type Output = Counter; // тип генерируемого значения
fn add(self, other: Counter) -> Counter {
Counter {
value: self.value + other.value
}
}
}
Большинство трейтов-операторов (но не все!) предполагают реализацию двух компонентов. Прежде всего нам надо определить ассоциированный тип Output, который будет возвращаться третом. В данном случае мы предполагаем, что мы будем складывать два объекта Counter и результатом также будет объект Counter. Поэтому в качестве выходного типа указываем тип Counter:
type Output = Counter;
Далее нам надо определить функцию, которая называется как и трейт, только в нижнем регистре - в нашем случае функцию add.
Первый параметр функции - self представляет левый операнд оператора. Второй параметр функции (в примере выше параметр other) представляет правый операнд.
Так как мы складываем два значения Counter, то и оба параметра представляют этот тип.
fn add(self, other: Counter) -> Counter {
Counter {
value: self.value + other.value
}
}
В данном случае просто складываем значения поля value обоих объектов Counter и возвращаем результат в виде нового объекта Counter.
После этого мы сможем складывать два объекта Counter и получать результат сложения в виде нового значения Counter:
let counter1 = Counter{value:5};
let counter2 = Counter{value:15};
let counter3 = counter1 + counter2;
Однако отмечу, что логика реализации оператора может быть произвольной, и возвращаемый типы не обязательно должен представлять тип Counter (тип, для которого реализуется трейт). Например, возвратим просто число:
use std::ops::Add; // поключаем трейт Add из модуля std::ops
struct Counter{
value: u32
}
impl Add for Counter{
type Output = u32; // тип генерируемого значения
fn add(self, other: Counter) -> u32 {
self.value + other.value
}
}
fn main() {
let counter1 = Counter{value:6};
let counter2 = Counter{value:15};
let result = counter1 + counter2;
println!("{}", result); // 21
}
По умолчанию правый - второй операнд представляет значение текущего типа, для которого определяется оператор. Однако мы можем в реальности использовать произвольный тип. Для этого реализация оператора типизируется нужным типом. Например:
use std::ops::Add; // поключаем трейт Add из модуля std::ops
struct Counter{
value: u32
}
// тип правого операнда - u32
impl Add<u32> for Counter{
type Output = u32; // тип генерируемого значения
fn add(self, other: u32) -> u32 {
self.value + other
}
}
fn main() {
let counter1 = Counter{value:6};
let result = counter1 + 11;
println!("{}", result); // 17
}
В данном случае тип правого операнда будет представлять тип u32, поэтому и реализация операнда типизируется этим типом:
impl Add<u32> for Counter{