꼭 구현할 표준 트레이트 문서의 검증 코드다.
//! 22 꼭 구현할 표준 트레이트: derive 결과(Debug·Clone·Copy·PartialEq·Eq·PartialOrd·Ord·Hash·Default),
//! 파생 Ord의 필드 순서 규칙, 막히는 derive(E0277·E0204·E0184)와 f64·String 필드에 붙는 묶음, Display와 to_string,
//! From·Into·TryFrom, AsRef, Send·Sync 확인, 그리고 Eq와 Hash가 어긋나면 HashMap이 같은 키를 못 찾는 반례와 clippy 린트
//! (from_over_into, derived_hash_with_manual_eq, derive_ord_xor_partial_ord)를 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/22_standard-traits_verify.rs"
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{BuildHasherDefault, DefaultHasher, Hash, Hasher};
use std::sync::atomic::{AtomicUsize, Ordering};
static SNIPPET: AtomicUsize = AtomicUsize::new(0);
/// rustc 또는 clippy-driver로 코드 조각을 컴파일해 stderr를 돌려준다.
fn compile_with(tool: &str, code: &str) -> String {
let n = SNIPPET.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("vr22_{}_{}", std::process::id(), n));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("snippet.rs");
std::fs::write(&file, code).unwrap();
let out = std::process::Command::new(tool)
.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("컴파일러 실행");
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn expect(label: &str, tool: &str, code: &str, needle: &str) {
let e = compile_with(tool, code);
let first = e
.lines()
.find(|l| l.starts_with("error") || l.starts_with("warning"))
.unwrap_or("(오류 없음)");
println!("[{label}] {first}");
assert!(e.contains(needle), "{label}: {e}");
}
// 1. 흔한 derive 묶음
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
struct Version {
major: u16,
minor: u16,
patch: u16,
}
// 필드 순서가 정렬 순서를 정한다.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct ByAge {
age: u8,
name: &'static str,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct ByName {
name: &'static str,
age: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
enum Level {
Low,
#[default]
Mid,
High,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
}
}
// 2. From·TryFrom
#[derive(Debug, PartialEq)]
struct Celsius(f64);
#[derive(Debug, PartialEq)]
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
#[derive(Debug, PartialEq)]
struct Percent(u8);
impl TryFrom<i32> for Percent {
type Error = String;
fn try_from(v: i32) -> Result<Self, Self::Error> {
if (0..=100).contains(&v) {
Ok(Percent(v as u8))
} else {
Err(format!("0~100 밖의 값: {v}"))
}
}
}
// 3. AsRef: String과 &str을 모두 받는다.
fn shout<S: AsRef<str>>(s: S) -> String {
s.as_ref().to_uppercase()
}
// 4. Eq와 Hash가 어긋난 키 (반례): 같다고 하면서 해시는 다르게 낸다.
struct BrokenKey(String);
impl PartialEq for BrokenKey {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for BrokenKey {}
impl Hash for BrokenKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state); // 대소문자를 그대로 해시한다 -> 규칙 위반
}
}
// 고친 키: 같다고 판단하는 기준(소문자)으로 해시한다.
struct FixedKey(String);
impl PartialEq for FixedKey {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for FixedKey {}
impl Hash for FixedKey {
fn hash<H: Hasher>(&self, state: &mut H) {
for b in self.0.bytes() {
state.write_u8(b.to_ascii_lowercase());
}
state.write_u8(0xff);
}
}
fn assert_send_sync<T: Send + Sync>() {}
fn main() {
// 1. derive 결과
let v = Version {
major: 1,
minor: 2,
patch: 3,
};
let w = v; // Copy라서 v를 계속 쓸 수 있다
assert_eq!(v, w);
println!("{v:?}");
println!("{v:#?}");
assert_eq!(format!("{v:?}"), "Version { major: 1, minor: 2, patch: 3 }");
assert_eq!(
Version::default(),
Version {
major: 0,
minor: 0,
patch: 0
}
);
let mut vs = [
Version {
major: 1,
minor: 10,
patch: 0,
},
Version {
major: 1,
minor: 2,
patch: 9,
},
Version {
major: 0,
minor: 99,
patch: 99,
},
];
vs.sort();
let shown: Vec<String> = vs.iter().map(|v| v.to_string()).collect();
println!("정렬: {shown:?}");
assert_eq!(shown, ["v0.99.99", "v1.2.9", "v1.10.0"]);
let a1 = ByAge {
age: 30,
name: "kim",
};
let a2 = ByAge {
age: 20,
name: "lee",
};
let n1 = ByName {
name: "kim",
age: 30,
};
let n2 = ByName {
name: "lee",
age: 20,
};
println!(
"ByAge: kim(30) < lee(20)? {}, ByName: kim(30) < lee(20)? {}",
a1 < a2,
n1 < n2
);
assert!(a1 > a2);
assert!(n1 < n2);
assert!(Level::Low < Level::High);
assert_eq!(Level::default(), Level::Mid);
let mut lv = [Level::High, Level::Low, Level::Mid];
lv.sort();
assert_eq!(lv, [Level::Low, Level::Mid, Level::High]);
let set: HashSet<Version> = [v, w].into_iter().collect();
assert_eq!(set.len(), 1);
// 0. 아무것도 붙이지 않은 타입은 ==도 {:?}도 안 된다.
let bare = compile_with(
"rustc",
"pub struct V(pub u8);
pub fn f(a: V, b: V) -> bool { a == b }
pub fn g(a: V) -> String { format!(\"{a:?}\") }",
);
println!(
"[아무 트레이트도 없는 타입] {}",
bare.lines().find(|l| l.starts_with("error")).unwrap()
);
assert!(bare.contains("error[E0369]: binary operation `==` cannot be applied to type `V`"));
assert!(bare.contains("error[E0277]: `V` doesn't implement `Debug`"));
// assert_eq!는 ==로 비교하고, 다르면 {:?}로 찍는다: PartialEq만 있으면 막힌다.
expect(
"PartialEq만 있는 타입에 assert_eq!",
"rustc",
"#[derive(PartialEq)] pub struct V(pub u8);
pub fn f() { assert_eq!(V(1), V(1)); }",
"error[E0277]: `V` doesn't implement `Debug`",
);
// 2. 막히는 derive
expect(
"f64 필드에 Eq derive",
"rustc",
"#[derive(PartialEq, Eq)] pub struct P { pub x: f64 }",
"error[E0277]",
);
expect(
"String 필드에 Copy derive",
"rustc",
"#[derive(Clone, Copy)] pub struct N { pub s: String }",
"error[E0204]",
);
expect(
"Drop이 있는 타입에 Copy",
"rustc",
"#[derive(Clone, Copy)] pub struct G;
impl Drop for G { fn drop(&mut self) {} }",
"error[E0184]",
);
expect(
"f64 필드에 Hash derive",
"rustc",
"#[derive(Hash)] pub struct P { pub x: f64 }",
"error[E0277]: the trait bound `f64: Hash` is not satisfied",
);
expect(
"f64 필드에 Ord derive",
"rustc",
"#[derive(PartialEq, PartialOrd, Ord)] pub struct P { pub x: f64 }",
"error[E0277]: the trait bound `f64: Ord` is not satisfied",
);
// f64와 String이 섞인 타입에 붙일 수 있는 묶음
let ok = compile_with(
"rustc",
"#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
pub struct Price { pub amount: f64, pub currency: String }",
);
println!(
"[f64·String 필드에 Debug·Clone·PartialEq·PartialOrd·Default] {}",
if ok.contains("error") {
"오류"
} else {
"(오류 없음)"
}
);
assert!(!ok.contains("error"), "{ok}");
expect(
"Display는 derive할 수 없다",
"rustc",
"#[derive(Display)] pub struct D;",
"error",
);
// NaN은 자기 자신과도 같지 않다(비교 결과 자체가 없다). 그래서 f64는 Eq가 아니다.
let nan = std::hint::black_box(f64::NAN);
assert!(nan.partial_cmp(&nan).is_none());
// 3. Display와 to_string, From·Into·TryFrom, AsRef
assert_eq!(v.to_string(), "v1.2.3");
let f: Fahrenheit = Celsius(100.0).into(); // From만 구현했는데 into가 생긴다
assert_eq!(f, Fahrenheit(212.0));
assert_eq!(Percent::try_from(42), Ok(Percent(42)));
assert_eq!(
Percent::try_from(150),
Err("0~100 밖의 값: 150".to_string())
);
let p: Result<Percent, _> = (-1).try_into();
assert!(p.is_err());
assert_eq!(shout("abc"), "ABC");
assert_eq!(shout(String::from("xy")), "XY");
expect(
"Into를 직접 구현하면 clippy가 From을 권한다",
"clippy-driver",
"pub struct W(pub i32);
impl Into<i32> for W { fn into(self) -> i32 { self.0 } }",
"an implementation of `From` is preferred",
);
// Borrow의 약속: String과 그 &str은 같은 해시를 낸다. 그래서 String 키 맵을 &str로 찾는다.
let hash_of = |x: &dyn Fn(&mut DefaultHasher)| {
let mut h = DefaultHasher::new();
x(&mut h);
h.finish()
};
let owned = String::from("abc");
assert_eq!(hash_of(&|h| owned.hash(h)), hash_of(&|h| "abc".hash(h)));
let by_name: HashMap<String, i32> = [(owned.clone(), 7)].into_iter().collect();
assert_eq!(by_name.get("abc"), Some(&7));
// 4. Send·Sync는 컴파일러가 알아서 붙인다.
assert_send_sync::<Version>();
assert_send_sync::<String>();
expect(
"Rc는 Send가 아니다",
"rustc",
"fn need_send<T: Send>() {}
pub fn f() { need_send::<std::rc::Rc<u8>>(); }",
"error[E0277]",
);
// 5. Eq와 Hash가 어긋나면: 같다고 하는 키를 HashMap이 못 찾는다.
assert!(BrokenKey("Kim".into()) == BrokenKey("KIM".into()));
// 해시 키를 고정해(DefaultHasher::new) 실행할 때마다 같은 결과가 나오게 한다.
let mut broken: HashMap<BrokenKey, i32, BuildHasherDefault<DefaultHasher>> = HashMap::default();
broken.insert(BrokenKey("Kim".into()), 1);
let found_broken = broken.get(&BrokenKey("KIM".into())).copied();
broken.insert(BrokenKey("KIM".into()), 2);
println!(
"어긋난 키: get(\"KIM\") = {:?}, 같은 키를 두 번 넣은 뒤 len = {}",
found_broken,
broken.len()
);
assert_eq!(found_broken, None);
assert_eq!(broken.len(), 2);
let mut fixed: HashMap<FixedKey, i32, BuildHasherDefault<DefaultHasher>> = HashMap::default();
fixed.insert(FixedKey("Kim".into()), 1);
let found_fixed = fixed.get(&FixedKey("KIM".into())).copied();
fixed.insert(FixedKey("KIM".into()), 2);
println!(
"고친 키: get(\"KIM\") = {:?}, 다시 넣은 뒤 len = {}",
found_fixed,
fixed.len()
);
assert_eq!(found_fixed, Some(1));
assert_eq!(fixed.len(), 1);
expect(
"Hash는 derive, PartialEq는 손으로",
"clippy-driver",
"#[derive(Hash, Eq)] pub struct K(pub String);
impl PartialEq for K { fn eq(&self, o: &Self) -> bool { self.0.eq_ignore_ascii_case(&o.0) } }",
"you are deriving `Hash` but have implemented `PartialEq` explicitly",
);
expect(
"PartialOrd는 derive, Ord는 손으로",
"clippy-driver",
"use std::cmp::Ordering;
#[derive(PartialEq, Eq, PartialOrd)] pub struct A(pub i32);
impl Ord for A { fn cmp(&self, o: &Self) -> Ordering { o.0.cmp(&self.0) } }",
"you are implementing `Ord` explicitly but have derived `PartialOrd`",
);
println!("ALL CHECKS PASSED");
}