콘텐츠로 이동

Rust 에러 E0369: binary operation cannot be applied to type (연산자 미지원)

E0369는 ==, <, + 같은 연산자를, 아직 그 연산을 할 줄 모르는 타입에 썼다는 뜻이에요. Rust에서 연산자는 내가 만든 구조체나 열거형에 원래부터 들어 있지 않아요. 연산자마다 트레이트가 하나씩 있고, 그 트레이트를 derive하거나 구현해야 타입이 연산자를 쓸 수 있어요. 보통은 #[derive(...)] 한 줄이면 고쳐지고, 어떤 걸 붙여야 하는지도 컴파일러가 알려 줘요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0369]: binary operation `==` cannot be applied to type `Point`
--> src/main.rs:9:10
|
9 | if a == b { // ❌ Point는 자기 자신을 비교하는 법을 몰라요
| - ^^ - Point
| |
| Point
|
note: an implementation of `PartialEq` might be missing for `Point`
--> src/main.rs:1:1
|
1 | struct Point {
| ^^^^^^^^^^^^ must implement `PartialEq`
help: consider annotating `Point` with `#[derive(PartialEq)]`
|
1 + #[derive(PartialEq)]
2 | struct Point {
|

“이항 연산 ==를 Point 타입에 적용할 수 없다”는 뜻이에요. note는 “Point에 PartialEq 구현이 빠진 것 같다”고 말하고, help는 “Point에 #[derive(PartialEq)]를 붙여 보라”고 제안해요.

이 에러를 내는 프로그램은 다음과 같아요.

struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
if a == b { // ❌ Point는 자기 자신을 비교하는 법을 몰라요
println!("Same point!");
}
}

첫 줄은 연산자에 따라 달라지지만, 같은 에러예요.

  • binary operation `<` cannot be applied to type `Version`: PartialOrd가 없어요(>, <=, >=도 마찬가지예요).
  • cannot add `Point` to `Point`: Add가 없어요. “cannot subtract”(뺄 수 없음), “cannot multiply”(곱할 수 없음) 같은 메시지도 볼 수 있어요.
  • cannot add `&str` to `&str`: 문자열 슬라이스 두 개를 +로 이어 붙이려고 했어요.

연산자 아래의 라벨은 양쪽의 타입을 보여 줘요. 여기서는 Point와 Point예요. 그다음 note:가 내 구조체를 가리키면서 PartialEq를 구현해야 한다(must implement PartialEq)고 말해요. 다시 말해 “Point 두 개를 비교하라고 했는데, Point에서 같다는 게 무슨 뜻인지 아무도 알려 주지 않았어요”라는 거예요.

경기 심판에게 물건 두 개를 건네면서 “어느 쪽이 더 커요?“라고 묻는다고 생각해 보세요. 숫자라면 답이 뻔해요. 하지만 배낭 두 개라면 심판에게 먼저 기준이 필요해요. 무게로 잴까요? 부피로 잴까요? Rust는 바로 이렇게 꼼꼼한 심판이에요.

모든 연산자는 사실 변장한 메서드 호출이에요. a == b는 PartialEq::eq를, a < b는 PartialOrd::partial_cmp를, a + b는 Add::add를 호출해요. 숫자, String, 그리고 많은 표준 타입은 이미 이 트레이트들을 구현하고 있어요. 하지만 새로 만든 구조체는 하나도 없는 상태로 시작해요.

Rust는 추측하지 않아요. 추측은 가끔 틀리기 때문이에요. 두 User는 ID만 같으면 같은 걸까요, 아니면 모든 필드가 같아야 할까요? Point + Point는 좌표를 더해야 할까요, 아니면 점을 더한다는 것 자체가 말이 안 될까요? 트레이트를 derive하거나(필드별로 비교하는 기본 방식) 직접 impl을 써서 내가 정하면 돼요. 이 트레이트들은 자주 쓰는 트레이트 페이지에서 설명해요.

방법 1: ==와 !=에는 #[derive(PartialEq)]

섹션 제목: “방법 1: ==와 !=에는 #[derive(PartialEq)]”

“같다”가 “모든 필드가 같다”는 뜻일 때 ==와 !=를 위해 이 방법을 고르세요. 대부분은 이게 정답이고, rustc가 제안하는 것도 바로 이거예요. Debug도 같이 붙이면 구조체를 출력할 수 있어요.

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
if a == b {
println!("같은 점이에요!");
}
println!("{:?} != {:?}: {}", a, c, a != c);
}

실행 결과:

같은 점이에요!
Point { x: 1, y: 2 } != Point { x: 5, y: 0 }: true
▶ 직접 실행해 보기

모든 필드도 비교할 수 있어야 해요. 어떤 필드의 타입에 PartialEq가 없다면, 그 타입에도 derive해 주세요. 열거형도 똑같아요. enum Status에 #[derive(PartialEq)]를 붙이면 status == Status::Banned가 컴파일돼요.

방법 2: <와 >에는 #[derive(PartialOrd)]

섹션 제목: “방법 2: <와 >에는 #[derive(PartialOrd)]”

타입에 자연스러운 순서가 있을 때 이 방법을 고르세요. PartialOrd에는 PartialEq가 필요해서, rustc는 #[derive(PartialEq, PartialOrd)]를 제안해요. derive한 버전은 필드를 위에서 아래로 비교해요. 먼저 major를 비교하고, 그게 같을 때만 minor를 비교해요. 그래서 구조체 안의 필드 순서가 중요해요.

#[derive(Debug, PartialEq, PartialOrd)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let installed = Version { major: 1, minor: 4 };
let required = Version { major: 1, minor: 7 };
if installed < required {
println!("{:?}에서 업데이트해 주세요", installed);
}
let next = Version { major: 2, minor: 0 };
println!("2.0이 1.7보다 새 버전인가요: {}", next > required);
}

실행 결과:

Version { major: 1, minor: 4 }에서 업데이트해 주세요
2.0이 1.7보다 새 버전인가요: true
▶ 직접 실행해 보기

내 타입의 Vec을 .sort()하고 싶다면 Eq와 Ord도 필요해요. 정렬을 참고하세요.

좌표나 돈을 더하는 것처럼, 내 타입의 두 값을 더하는 게 분명한 의미가 있을 때 이 방법을 고르세요. 수학 연산자에는 derive가 없으니까 짧은 impl을 직접 써요. Output은 +가 돌려주는 타입이에요.

use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let start = Point { x: 1, y: 2 };
let step = Point { x: 3, y: 4 };
let end = start + step;
println!("{:?}", end);
println!("{}", end == Point { x: 4, y: 6 });
}

실행 결과:

Point { x: 4, y: 6 }
true
▶ 직접 실행해 보기

Sub, Mul, Div, Neg도 모두 std::ops에 있고, 똑같은 방식으로 구현해요. 여기서 Copy를 derive했기 때문에 + 뒤에도 start와 step을 계속 쓸 수 있어요. 더 자세한 내용은 자주 쓰는 트레이트와 고급 트레이트에 있어요.

방법 4: +나 format!으로 문자열 이어 붙이기

섹션 제목: “방법 4: +나 format!으로 문자열 이어 붙이기”

에러가 cannot add `&str` to `&str`라고 할 때 이 방법을 고르세요. 문자열에 +를 쓰려면 왼쪽에는 소유한 String이, 오른쪽에는 &str이 있어야 해요. rustc의 help도 “create an owned String from a string reference”(문자열 참조로부터 소유한 String을 만들라)라고 자기 말로 알려 줘요. 그러니까 first.to_owned() + second예요. String이 두 개라면 오른쪽을 빌리세요. s1 + &s2처럼요. 그리고 format!은 언제나 동작하고, 아무것도 이동시키지 않아요.

fn main() {
let first = String::from("안녕하세요, ");
let second = String::from("Ferris");
let shout = format!("{first}{second}!"); // 둘 다 빌려요
let joined = first + &second; // first는 joined 안으로 이동해요
let short = "안녕, ".to_owned() + "꽃게야"; // 왼쪽이 &str이면 먼저 소유한 값으로 만들어요
println!("{shout}");
println!("{joined}");
println!("{short}");
println!("{second}는 아직 여기 있어요");
}

실행 결과:

안녕하세요, Ferris!
안녕하세요, Ferris
안녕, 꽃게야
Ferris는 아직 여기 있어요
▶ 직접 실행해 보기

+는 왼쪽 String의 소유권을 가져가서 그 뒤에 이어 붙여요. 그래서 그 줄 다음에는 first를 쓸 수 없어요. 헷갈린다면 그냥 format!을 쓰세요. 문자열을 참고하세요.

제네릭 코드에서 비교할 때. 유명한 largest 함수는 binary operation `>` cannot be applied to type `&T`로 실패해요. T는 어떤 타입이든 될 수 있고, 순서가 없는 타입일 수도 있기 때문이에요.

fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { // ❌ T는 비교할 수 없는 타입일 수도 있어요
largest = item;
}
}
largest
}

해결: rustc가 제안하는 그대로 트레이트 바운드를 추가하세요. fn largest<T: PartialOrd>(list: &[T]) -> &T. 제네릭을 참고하세요.

열거형 값을 확인할 때. if status == Status::Banned는 열거형에 #[derive(PartialEq)]가 붙기 전까지 실패해요. 열거형을 바꿀 수 없다면(다른 크레이트에서 온 경우), 대신 matches!(status, Status::Banned)나 match를 쓰세요.

String 두 개를 이어 붙일 때. String 두 개로 first + second를 쓰면 E0369가 아니에요. help: consider borrowing here(여기서 빌려 보라)와 함께 E0308(“expected &str, found String”)이 나요. 해결은 first + &second예요. 같은 종류의 문제고, 방법 4와 해결책도 같아요.

서로 다른 두 타입을 비교할 때. i32인 count와 f64인 limit으로 count == limit을 쓰면 E0308이 나고, String과 char로 name == 'F'를 쓰면 E0277(“can’t compare String with char”)이 나요. 기억하기 쉬운 규칙: 왼쪽 타입에 그 연산자가 아예 없으면 E0369, 연산자는 있지만 그 오른쪽 타입과는 안 될 때는 E0308이나 E0277이에요. 해결: 한쪽을 변환하세요(count as f64 == limit, name == "F").

이해도 점검

1. 구조체 Order에 if order_a == order_b를 썼더니 E0369가 났어요. 보통 어떻게 고칠까요?

2. struct Version { major: u32, minor: u32 }에 #[derive(PartialOrd)]를 붙이면 두 버전을 어떻게 비교할까요?

3. first와 second가 둘 다 &str이에요. 에러 없이 이어 붙이는 식은 무엇일까요?

점수: 0 / 3