2장에서 살펴본 망델브로 집합 플로터에서는 복수평명 위의 수를 표현하기 위해 num 트레이트의 Complex 타입을 사용한 예시가 아래와 같음
#[derive(Clone, Copy, Debug)]
struct Complex<T> {
/// 복소수의 실수 부분
re: T,
/// 복소수의 허수 부분
im: T,
}
보통 복소수의 형태는 a+bi 형식으로 나타낼 수 있으며, 여기서 a는 실수 부분이고 b는 허수 부분이다. T
는 제네릭 타입으로, 실수 부분과 허수 부분이 다양한 수치 타입을 가질 수 있도록 한다.
예를 들어, 복소수 3+4i를 표현하기 위해 Complex
구조체를 사용할 수 있다.
let z = Complex { re: 3, im: 4 };
Complex
수는 기본 제공 수치 타입처럼 러스트의 +와 연산자로 더하고 곱할 수 있었다.
연산자 오버로딩 : 사용자 정의 타입도 산술 연산자를 비롯한 여러 연산자를 지원 하는데 몇 가지 기본 트레이트를 구현하기만 되는 것을 말함
참고로 C++, C#, 파이썬, 루비의 연산자 오버로딩과 효과가 매우 비슷하다는 것으로 연산자 오버로딩을 위한 트레이트는 언어의 어떤 부분을 지원하는지에 따라 몇 가지 범주
로 나뉜다.
이번 장의 목표는 이 장의 목표는 사용자 정의 타입을 언어에 잘 통합할 수 있도록 돕고, 이러한 연산자를 사용하는 타입에 가장 자연스럽게 작용하는
제네릭 함수
를 작성하는 방법에 대해 이해를 돕는 것이다.
위의 표에 대한것들을 하나 하나 각자의 파트별로 조사해볼 예정!!
트레이트
Add<T>
T 값을 자기 자신에게 더하는 능력
러스트 표현식 a + b 는 a.add(b)로, 표준 라이브러리의 트레이트 std::ops::Add
가 가진 메서드 add에 대한 호출 축약 표시이다. 표현식 a + b가 Complex 값에도 통하는 이유는 num 크레이트가 Complex에 이 트레이트를 구현해 두었기 때문이다.
다른 연산자의 경우에도 비슷하다. 예시로 a* b는 트레이트 std::ops: Mul
이 가진 메서드 a.mul(b)의 축약 표기이고, std::ops :: Neg는 앞에 붙는 부호 부정 연산자를 담당한다.
Rust에서 연산자 오버로딩을 사용하는 방법을 설명하는 예시 코드
use std::ops::Add;
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 30);
여기서 Add
트레이트를 사용하여 add
메서드를 호출해 숫자를 더하는 예이다. 4.125f32.add(5.75)
는 4.125 + 5.75와 같은 결과를 가진다.
std::ops::Add
트레이트 정의trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
이 트레이트는 Rhs
라는 타입 매개변수를 가지며 기본값은 Self
이다.
Output
은 덧셈의 결과 타입+
연산자를 사용할 수 있게 된다.add
메서드는 self와 rhs라는 두 개의 매개변수를 가진다.self
는 메서드를 호출하는 값을 나타내며, rhs
는 오른쪽 피연산자add
메서드는 self
와 rhs
를 더한 결과를 반환복소수 타입인 Complex에 대해 Add 트레이트를 구현한 예로 Complex<i32>
타입의 값을 더하기 위해 Add<Complex<i32>>
트레이트를 구현해야 한다.
use std::ops::Add;
impl Add for Complex<i32> {
type Output = Complex<i32>;
fn add(self, rhs: Self) -> Self::Output {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
같은 타입(self와 rhs가 Complex로 같은 타임)을 더하는 것이므로 그냥 Add
라고만 사용하면 된다.
Complex
타입에 대해 제네릭으로 Add 트레이트 구현각각의 Complex, Complex, Complex 타입에 대해 Add 트레이트를 개별적으로 구현할 필요가 없다. 대신 제네릭
을 사용하여 한 번에 모든 타입을 아우를 수 있다.
여러 타입에 대해 일반적으로 사용할 수 있도록 제네릭을 사용하는 예
use std::ops::Add;
impl<T> Add for Complex<T>
where
T: Add<Output = T>,
{
type Output = Self;
fn add(self, rhs: Self) -> Self {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
여기서 where T: Add<Output=T>는 타입 T
가 덧셈이 가능하며 그 결과로 또 다른 T
타입을 반환한다는 것을 나타낸다. 이는 합리적인 제한이지만, 더 일반적인 제네릭 구현을 위해 조금 더 느슨하게 가져갈 수 있다.
왼쪽과 오른쪽 피연산자가 서로 다른 타입일 수 있도록 한 더 일반적인 제네릭 구현 예
use std::ops::Add;
impl<L, R> Add<Complex<R>> for Complex<L>
where
L: Add<R>,
{
type Output = Complex<L::Output>;
fn add(self, rhs: Complex<R>) -> Self::Output {
Complex {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
Rust는 다양한 타입이 섞여 있는 연산을 지원하지 않으려는 경향이 있다. 앞의 제네릭 구현에서 L
과 R
이 다를 수 있지만, 실제로는 대부분 같은 타입이 된다.
다른 조합을 구현하고 있는 타입 중에는 L
로 사용할 수 있는 것이 많지 않기 때문에, 이러한 일반적인 제네릭 구현이 간단한 제네릭 정의보다 크게 유용하지 않을 수 있다.
Rust에서는 다양한 타입에 대해 연산자 오버로딩을 지원하여 사용자 정의 타입이 기본 제공 타입처럼 동작하도록 할 수 있다. 이를 통해 더 자연스럽고 읽기 쉬운 코드를 작성할 수 있으며, 제네릭을 사용하여 코드의 재사용성을 높일 수 있다.
Add
트레이트를 구현하면 사용자 정의 타입에서도 +
연산자를 사용할 수 있다.Complex
타입에 대해 여러 타입을 지원하도록 제네릭으로 Add
트레이트를 구현할 수 있다.트레이트 이름 | 표현식 | 동등한 표현식 |
---|---|---|
std::ops::Neg | -x | x.neg() |
std::ops::Not | !x | x.not() |
러스트의 부호 있는 수치 타입은 모두 단항 부호 부정 연산자 -를 위해서 std::ops ::Neg
를 구현하
고 있고, 정수 타입과 bool은 단항 보수 연산자를 위해서 std::ops :: Not
을 구현하고 있다.
!
를 bool 값에 쓰면 값이 반대가 되지만, 정수에 쓰면 비트별 반전(즉, 비트 뒤집기)이 일어난다.
trait Neg {
type Output;
fn neg(self) -> Self::Output;
}
trait Not {
type Output;
fn not(self) → Self::Output;
}
type Output;
: neg 메서드의 결과 타입을 정의합fn neg(self) -> Self::Output;
: self의 부호를 부정한 결과를 반환type Output
;: not 메서드의 결과 타입을 정의합니다.fn not(self) -> Self::Output
;: self의 논리 부정을 반환합니다.복소수의 부호 부정은 실수 부분과 허수 부분 각각의 부호를 부정하는 방식으로 구현된다. 이를 제네릭하게 구현하여 다양한 타입의 복소수를 지원할 수 있다.
use std::ops::Neg;
impl<T> Neg for Complex<T>
where
T: Neg<Output = T>,
{
type Output = Complex<T>;
fn neg(self) -> Complex<T> {
Complex {
re: -self.re,
im: -self.im,
}
}
}
Neg
와 Not
트레이트를 사용한다.Neg
트레이트는 부호 부정(-
), Not
트레이트는 논리 부정(!
) 연산자를 정의한다.Complex<T>
) 타입에 대해 Neg
트레이트를 제네릭하게 구현하여, 실수 부분과 허수 부분 각각의 부호를 부정할 수 있다.Complex
타입이 기본 제공 수치 타입처럼 부호 부정 연산을 지원하도록 할 수 있다.범주 | 트레이트 이름 | 표현식 | 동등한 표현식 |
---|---|---|---|
산술 연산자 | std::ops::Add | x + y | x.add(y) |
std::ops::Sub | x - y | x.sub(y) | |
std::ops::Mul | x * y | x.mul(y) | |
std::ops::Div | x / y | x.div(y) | |
std::ops::Rem | x % y | x.rem(y) |
범주 | 트레이트 이름 | 표현식 | 동등한 표현식 |
---|---|---|---|
비트별 연산자 | std::ops::BitAnd | x & y | x.bitand(y) |
std::ops::BitOr | `x | y` | |
std::ops::BitXor | x ^ y | x.bitxor(y) | |
std::ops::Shl | x << y | x.shl(y) | |
std::ops::Shr | x >> y | x.shr(y) |
이항 연산자를 지원하는 트레이트들은 모두 동일한 일반적인 형태를 갖는다. 예를 들어, std::ops::BitXor
트레이트의 정의
trait BitXor<Rhs = Self> {
type Output;
fn bitxor(self, rhs: Rhs) -> Self::Output;
}
Rhs
: 오른쪽 피연산자의 타입을 나타내며, 기본값은 Self
이다.Output
: 연산의 결과 타입을 정의한다.bitxor
메서드: self
와 rhs
를 받아서 Self::Output
타입의 결과를 반환한다.bool
은 비트별 연산자를 구현하고 있다.+
연산자를 사용하여 String
을 &str
슬라이스나 다른 String
과 연결할 수 있다.&str
을 +
의 왼쪽 피연산자로 사용할 수 없게 러스트가 막고 있기에, 짧은 문자열을 왼쪽에 거듭 연결하는 방식으로 긴 문자열을 쓸 수 없다.write!
매크로를 사용하는 것이 더 효율적이다. 이는 17장의 '텍스트 추가와 삽입' 절에서 자세히 다룬다.복합 배정 표현식은
x += y나 x&= y
와 같은 것을 말하며, 두 피연산자를 가지고 덧셈이나 비트별 논리곱 같은 연산을 수행한 뒤 결과를 다시 왼쪽 피연산자에 저장한다.
러스트에서 복합 배정 표현식의 값은 저장된 값이 아니라 항상 ()
이다.
러스트는 접근법이 다르다. x += y는 그런 뜻이 아니라 메서드 호출 x.add_assign(y)의 축약 표기다. 여기서 add_assign
은 std::ops :: AddAssign 트레이트가 가진 유일한 메서드다.
trait AddAssign<Rhs=Self>{
fn add_assign(&mut self, rhs: Rhs);
}
범주 | 트레이트 이름 | 표현식 | 동등한 표현식 |
---|---|---|---|
산술 연산자 | std::ops::AddAssign | x += y | x.add_assign(y) |
std::ops::SubAssign | x -= y | x.sub_assign(y) | |
std::ops::MulAssign | x *= y | x.mul_assign(y) | |
std::ops::DivAssign | x /= y | x.div_assign(y) | |
std::ops::RemAssign | x %= y | x.rem_assign(y) | |
비트별 연산자 | std::ops::BitAndAssign | x &= y | x.bitand_assign(y) |
std::ops::BitOrAssign | `x | = y` | |
std::ops::BitXorAssign | x ^= y | x.bitxor_assign(y) | |
std::ops::ShlAssign | x <<= y | x.shl_assign(y) | |
std::ops::ShrAssign | x >>= y | x.shr_assign(y) |
bool
타입은 비트별 복합 배정 연산자를 구현하고 있다.Complex
타입에 대한 AddAssign
트레이트 구현use std::ops::AddAssign;
impl<T> AddAssign for Complex<T>
where
T: AddAssign<T>,
{
fn add_assign(&mut self, rhs: Complex<T>) {
self.re += rhs.re;
self.im += rhs.im;
}
}
+=
연산자를 정의한다.where T: AddAssign<T>
는 타입 T가 AddAssign 트레이트를 구현해야 한다는 조건을 의미add_assign
메서드는 &mut self와 rhs를 받아, Complex 값을 더한 결과를 self에 반영합니다.복합 배정 연산자를 위한 기본 제공 트레이트는 그에 상응하는 이항 연산자를 위한 트레이트와 완전히 별개이다.
std::ops::Add
를 구현한다고 해서 std::ops::AddAssign
이 자동으로 구현되지는 않는다.+=
연산자의 왼쪽 피연산자로 사용하고 싶다면, 해당 타입에 대해 직접 AddAssign
을 구현해야 한다.Complex
타입에 대해 제네릭으로 AddAssign 트레이트를 구현하여, 실수 부분과 허수 부분 각각에 대해 +=
연산을 지원할 수 있다.Rust에서 상등(
==
) 연산자와 같지 않음(!=
) 연산자는std::cmp::PartialEq
트레이트를 통해 정의된eq
와ne
메서드를 호출하는 축약 표기
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}
eq
메서드는 두 값을 비교하여 같으면 true
, 다르면 false
를 반환ne
메서드는 기본적으로 eq 메서드를 반대로 사용하여 정의된다. 즉, ne
는 eq
의 결과를 부정Complex
타입에 대한 PartialEq
트레이트를 구현하는 예시
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
T
에 대해 상등 비교가 가능한 Complex<T>
를 위한 비교를 정의한다.re
)과 허수 부분(im
)을 각각 비교하여 두 Complex
값이 같은지 확인한다.PartialEq의 구현은 왼쪽 피연산자의 각 필드를 그에 대응하는 오른쪽 피연산자의 필드와 비교하는 게 전부라서 자주 쓰이는 연산임에도 이기에 PartialEq
자동 생성이 있다.
Rust는 요청이 있을 경우
PartialEq
의 구현을 자동으로 생성해 준다. 다음과 같이 타입 정의에derive
어트리뷰트를 추가하면 됨
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
re: T,
im: T,
}
PartialEq
는 부분 동치 관계를 나타내며, 완전 동치 관계는 아니다.PartialEq
의 확장의 의미trait Eq: PartialEq<Self> {}
PartialEq
를 구현해야 하며, x == x가 항상 true여야 합니다.PartialEq
트레이트:String, Vec, HashMap
같은 비Copy 값을 비교할 때 이동이 발생하지 않음을 의미let s = "d\x6fv\x65t\x61i\x6c".to_string();
let t = "\x640\x76e\x74a\x69l".to_string();
assert!(s == t); // s와 t는 차용될 뿐이다.
// 따라서 여기서도 값을 계속 사용할 수 있다.
assert_eq!(format!("{} {}", s, t), "dovetail dovetail");
차용(borrow)
되었으므로 값이 이동하지 않는다.where
Rhs: ?Sized
PartialEq
트레이트의 정의trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}
Rhs: ?Sized
:?Sized
를 사용하면 비균일 크기 타입을 지원할 수 있다.assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));
PartialEq
트레이트를 사용하여 문자열 비교를 수행합니다.여기서 Self Rhs는 모두 비균일 크기 타입인 str이므로 ne의 self와 rhs 매개변수는 모두 &str값이 된다. 균일 크기 타입, 비균일 크기 타입. Sized 트레이트는 13장의 'Sized' 절에서 자세히 살펴본다.
동치 관계(equivalence relation)의 세 가지 요구 사항:
x == y
가 참이면 y == x
도 참x == y
이고 y == z
이면 x == z
여야 한다.x == x
는 항상 참이어야 한다.f32
와 f64
는 IEEE 표준 부동소수점 값을 사용한다.assert!(f64::is_nan(0.0/0.0));
assert_eq!(0.0/0.0 == 0.0/0.0, false);
assert_eq!(0.0/0.0 != 0.0/0.0, true);
assert_eq!(0.0/0.0 < 0.0/0.0, false);
assert_eq!(0.0/0.0 > 0.0/0.0, false);
assert_eq!(0.0/0.0 <= 0.0/0.0, false);
assert_eq!(0.0/0.0 >= 0.0/0.0, false);
Rust에서 동치 관계를 만족시키기 위해 PartialEq
와 Eq
트레이트가 사용된다. 그러나, 이들 트레이트는 각각 부분 동치 관계와 완전 동치 관계를 나타낸다.
==
연산자는 일반적으로 동치 관계의 세 가지 요구 사항 중 첫 번째와 두 번째를 만족한다.x == y
가 참이면 y == x
도 참이어야 한다.x == y
이고 y == z
이면 x == z
여야 한다.x == x
는 항상 참이어야 한다.그러나, IEEE 부동소수점 값(예: f32
와 f64
)에 대해서는 세 번째 요구 사항인 반사성을 만족하지 못한다. 예를 들어, NaN 값은 자기 자신과도 같지 않다고 취급된다.
assert!(f64::is_nan(0.0/0.0));
assert_eq!(0.0/0.0 == 0.0/0.0, false);
assert_eq!(0.0/0.0 != 0.0/0.0, true);
PartialEq
는 부분 동치 관계를 나타낸다.PartialEq
트레이트는 두 값이 같은지 비교하기 위해 사용된다.trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}
eq
메서드는 두 값을 비교하여 같으면 true
, 다르면 false
를 반환한다.ne
메서드는 기본적으로 eq
메서드를 반대로 사용하여 정의된다.Eq
트레이트는 완전 동치 관계를 나타내며, PartialEq
의 확장으로 정의된다.trait Eq: PartialEq<Self> {}
Eq
는 구현하지 않는다.impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
T
에 대해 상등 비교가 가능한 Complex<T>
를 위한 비교를 한다고 함impl<T: Eq> Eq for Complex<T> {}
Complex
타입에 대해 Eq
를 구현하는 예입니다. 매우 간단하게 구현할 수 있다.Rust는 PartialEq와 Eq 트레이트의 구현을 자동으로 생성할 수 있다.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Complex<T> {
re: T,
im: T,
}
derive
어트리뷰트를 사용하면, 필드나 요소를 차례로 비교하는 방식으로 상등성을 판단하는 구현이 자동으로 생성된다.Complex<i32>
는 i32가 Eq를 구현하므로 Eq를 구현하지만, Complex<f32>
는 f32가 Eq를 구현하지 않으므로 PartialEq만 구현합니다.PartialEq
만 받는 제네릭 코드를 작성할 때는 첫 번째와 두 번째 요구 조건이 지켜진다고 가정할 수 있다.std::cmp::Eq
트레이트를 바운드로 사용해야 한다.PartialEq
트레이트는 부분 동치 관계를 나타내며, Eq
트레이트는 완전 동치 관계를 나타낸다.PartialEq
를 구현하면 ==
와 !=
연산자를 사용할 수 있으며, Eq
를 구현하면 완전 동치 관계를 보장할 수 있다.PartialEq
와 Eq
의 구현을 자동으로 생성할 수 있으며, 이를 통해 사용자 정의 타입에서도 상등 비교 연산을 지원할 수 있다.Rust의 PartialOrd
트레이트는 값들 간의 부분적인 순서 비교를 가능하게 한다. 이 트레이트는 두 값이 비교 가능한 경우에만 순서 관계를 정의하며, 순서를 매길 수 없는 경우도 허용한다.
Rust에서 순서 비교 연산자 (<
, >
, <=
, >=
)는 std::cmp::PartialOrd
트레이트를 통해 구현된다. 이 트레이트는 상등 비교가 가능한 타입에 대해서만 순서 비교를 정의한다.
trait PartialOrd<Rhs = Self>: PartialEq<Rhs>
where
Rhs: ?Sized,
{
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
fn lt(&self, other: &Rhs) -> bool {
self.partial_cmp(other) == Some(Ordering::Less)
}
fn le(&self, other: &Rhs) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal))
}
fn gt(&self, other: &Rhs) -> bool {
self.partial_cmp(other) == Some(Ordering::Greater)
}
fn ge(&self, other: &Rhs) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Greater | Ordering::Equal))
}
}
PartialEq
)이어야 순서 비교도 가능함.Option<Ordering>
을 반환. 순서를 매길 수 없으면 None
을 반환.partial_cmp
를 사용하여 각각 <
, <=
, >
, >=
연산자를 정의함.PartialOrd가 Partial Eq를 확장하고 있다. 즉, 상등 비교가 가능한 타입이어야 순서 비교도 가능하다.
PartialOrd에서 직접 구현해야 하는 메서드는 partial_cmp뿐이다. partial_cmp가 Some(o)를 반환하면, o는 self와 other의 관계를 나타낸다.
enum Ordering {
Less, // self < other
Equal, // self == other
Greater, // self > other
}
Interval
타입에 대해 PartialOrd
를 구현하여 순서 비교를 가능하게 함.
use std::cmp::{Ordering, PartialOrd};
#[derive(Debug, PartialEq)]
struct Interval<T> {
lower: T, // 포함
upper: T, // 미포함
}
impl<T: PartialOrd> PartialOrd for Interval<T> {
fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.lower >= other.upper {
Some(Ordering::Greater)
} else if self.upper <= other.lower {
Some(Ordering::Less)
} else {
None
}
}
}
fn main() {
let interval1 = Interval { lower: 10, upper: 20 };
let interval2 = Interval { lower: 20, upper: 40 };
let interval3 = Interval { lower: 7, upper: 8 };
let interval4 = Interval { lower: 0, upper: 8 };
let interval5 = Interval { lower: 7, upper: 1 };
assert!(interval1 < interval2);
assert!(interval3 > interval4);
assert!(interval3 <= interval5);
let left = Interval { lower: 18, upper: 30 };
let right = Interval { lower: 28, upper: 40 };
assert!(!(left < right));
assert!(!(left >= right));
}
표현식 | 동등한 메서드 호출 | 기본 정의 |
---|---|---|
x < y | x.lt(y) | x.partial_cmp(&y) == Some(Ordering::Less) |
x > y | x.gt(y) | x.partial_cmp(&y) == Some(Ordering::Greater) |
x <= y | x.le(y) | matches!(x.partial_cmp(&y), Some(Ordering::Less |
x >= y | x.ge(y) | matches!(x.partial_cmp(&y), Some(Ordering::Greater |
Rust에서 PartialOrd
트레이트 외에도 완전한 순서 비교를 제공하는 Ord
트레이트가 있다.
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
PartialOrd
를 확장하며, PartialOrd
와 Eq
트레이트를 구현해야 함.Ordering
을 반환. 순서를 매길 수 없는 경우가 없음.use std::cmp::Reverse;
fn main() {
let mut intervals = vec![
Interval { lower: 10, upper: 20 },
Interval { lower: 5, upper: 15 },
Interval { lower: 1, upper: 5 },
];
intervals.sort_by_key(|i| i.upper);
println!("{:?}", intervals);
intervals.sort_by_key(|i| Reverse(i.lower));
println!("{:?}", intervals);
}
PartialOrd
트레이트는 부분적인 순서 비교를 가능하게 하며, 순서를 매길 수 없는 경우를 허용한다.Ord
트레이트는 완전한 순서 비교를 제공하며, 모든 값을 비교할 수 있다.Interval
타입에 대해 PartialOrd
와 Ord
를 적절히 구현하여 순서 비교와 정렬을 지원할 수 있다.Rust에서는
std::ops::Index
와std::ops::IndexMut
트레이트를 구현하여a[i]
같은 색인 표현식의 동작 방식을 정의할 수 있다. 배열은[]
연산자를 직접 지원하지만, 다른 타입에 대해서는 색인 표현식을 사용할 때Index
와IndexMut
트레이트를 통해 메서드를 호출한다.
trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
Idx
를 매개변수로 받아 색인 연산을 수행하는 트레이트t
ype Output: 색인 연산의 결과 타입을 나타낸다.trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
Index
트레이트를 확장하여 변경 가능한 색인 연산을 지원한다.use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("+", 10);
m.insert("百", 100);
m.insert("千", 1000);
m.insert("万", 10_000);
m.insert("亿", 1_0000_0000);
assert_eq!(m["+"], 10);
assert_eq!(m["亿"], 1_0000_0000);
HashMap<&str, i32>
는 Index<&str>를 구현하므로, m["+"]와 같은 표현식을 사용할 수 있다.m.index("+")
와 동등하다.use std::ops::{Index, IndexMut};
struct Image<P> {
width: usize,
pixels: Vec<P>,
}
impl<P: Default + Copy> Image<P> {
fn new(width: usize, height: usize) -> Image<P> {
Image {
width,
pixels: vec![P::default(); width * height],
}
}
}
impl<P> Index<usize> for Image<P> {
type Output = [P];
fn index(&self, row: usize) -> &[P] {
let start = row * self.width;
&self.pixels[start..start + self.width]
}
}
impl<P> IndexMut<usize> for Image<P> {
fn index_mut(&mut self, row: usize) -> &mut [P] {
let start = row * self.width;
&mut self.pixels[start..start + self.width]
}
}
: Image 구조체는 제네릭 타입 P를 가지며, width와 pixels 필드를 포함한다.
: new 메서드는 주어진 크기로 새 이미지를 생성한다.
Index for Image
:
IndexMut for Image
:
fn main() {
let mut desserts = vec!["Howalon".to_string(), "Soan papdi".to_string()];
desserts[0].push_str(" (fictional)");
desserts[1].push_str(" (real)");
println!("{:?}", desserts);
use std::ops::IndexMut;
(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");
println!("{:?}", desserts);
}
push_str
메서드는 &mut self에 작용하므로, 이 표현식들은 IndexMut 트레이트의 index_mut 메서드를 사용하여 동등한 표현으로 변환된다.Image
예제에서 row
가 범위 밖에 있으면 .index() 메서드가 self.pixels의 범위 밖을 색인하게 되어 패닉이 발생할 수 있다.Index
와 IndexMut
트레이트를 구현하면 사용자 정의 타입에서도 배열처럼 []
색인 연산을 사용할 수 있다.Rust에서 모든 연산자를 오버로딩할 수 있는 것은 아니다. 특정 연산자는 현재 오버로딩할 수 없으며, 일부는 특정 타입에만 사용할 수 있다.
?
?
연산자는 Result
와 Option
값에만 사용할 수 있다.&&
와 ||
bool
) 값에만 사용할 수 있다..
와 ..
.
연산자는 항상 범위의 경계를 나타내는 구조체를 생성하며, ..
연산자는 레퍼런스를 빌려오거나 값을 이동하거나 복사한다.val
처럼 사용하는 역참조 연산자는 오버로딩할 수 있다.Deref
와 DerefMut
트레이트를 사용하여 오버로딩할 수 있다..
val.field
와 val.method()
처럼 필드에 접근하고 메서드를 호출할 때 사용하는 점(.
) 연산자는 오버로딩할 수 있다.Deref
와 DerefMut
트레이트를 사용하여 오버로딩할 수 있다.f(x)
f(x)
의 오버로딩을 지원하지 않는다.Fn
, FnMut
, FnOnce
에 대해서는 14장에서 설명한다.?
연산자, 논리 연산자 &&
와 ||
, 점(.
) 연산자, 그리고 &
연산자는 현재 오버로딩할 수 없다.와 필드 접근 및 메서드 호출 연산자
.는
Deref와
DerefMut` 트레이트를 사용하여 오버로딩할 수 있다.f(x)
는 오버로딩할 수 없지만, 클로저를 사용하여 대체할 수 있다.