클로저 문서의 검증 코드다.
//! 20 클로저: 잡는 방식(빌림·가변 빌림·이동)과 Fn·FnMut·FnOnce, move, 스레드에 넘기기(E0373),
//! Fn 자리에 FnMut 클로저(E0525), FnOnce 두 번 부르기(E0382), 필드 단위 잡기(2021+), 클로저 크기,
//! 함수 포인터로 바뀌기, 클로저 돌려주기(클로저마다 타입이 다름, E0308), async 클로저와 AsyncFn(1.85),
//! 가변으로 잡은 동안 읽기(E0502), 2018 에디션과의 차이, 타입이 처음 부를 때 굳는 것(E0308),
//! unwrap_or_else에 FnOnce 넘기기, clippy redundant_closure를 확인한다.
//! 의존성: futures = "0.3" (block_on으로 async 클로저를 돌린다)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/20_closures_verify.rs"
use std::mem::size_of_val;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
static SNIPPET: AtomicUsize = AtomicUsize::new(0);
fn compile_errors(code: &str) -> String {
compile_with("rustc", code, "2024")
}
/// rustc 또는 clippy-driver로 코드 조각을 주어진 에디션으로 컴파일해 stderr를 돌려준다.
fn compile_with(tool: &str, code: &str, edition: &str) -> String {
let n = SNIPPET.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("vr20_{}_{}", 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",
edition,
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("rustc 실행");
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn expect(label: &str, code: &str, needle: &str) {
let e = compile_errors(code);
let first = e
.lines()
.find(|l| l.starts_with("error"))
.unwrap_or("(오류 없음)");
println!("[{label}] {first}");
if needle.is_empty() {
assert!(!e.contains("error"), "{label}: {e}");
} else {
assert!(e.contains(needle), "{label}: {e}");
}
}
/// 오류 메시지에 들어 있어야 할 문구를 모두 확인한다(첫 줄의 오류 코드와 note·help 문구).
fn expect_all(label: &str, code: &str, needles: &[&str]) {
let e = compile_errors(code);
let first = e
.lines()
.find(|l| l.starts_with("error"))
.unwrap_or("(오류 없음)");
println!("[{label}] {first}");
for n in needles {
assert!(e.contains(n), "{label}: {n}\n{e}");
}
}
fn call_twice<F: Fn() -> usize>(f: F) -> usize {
f() + f()
}
fn call_mut_twice<F: FnMut() -> usize>(mut f: F) -> usize {
f() + f()
}
fn call_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn make_op(name: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match name {
"add" => Box::new(|a, b| a + b),
_ => Box::new(|a, b| a * b),
}
}
async fn apply_async<F: AsyncFn(u32) -> u32>(f: F) -> u32 {
f(1).await + f(2).await
}
struct Point {
x: i32,
y: i32,
}
fn main() {
// 0. 둘레의 값을 잡는 정렬 기준: origin에서 가까운 순서
let origin: i32 = 10;
let mut near = vec![3, 14, 9, 12];
near.sort_by_key(|&x| (x - origin).abs());
println!("origin {origin}에서 가까운 순: {near:?}");
assert_eq!(near, [9, 12, 14, 3]);
expect(
"처음 부를 때 굳은 타입과 다른 타입으로 부름",
"pub fn g() { let c = |x| x; let _s = c(String::from(\"a\")); let _n = c(5); }",
"error[E0308]",
);
// 1. 세 가지 잡는 방식
let name = String::from("kim");
let greet = || name.len(); // 빌림만 한다 -> Fn
assert_eq!(call_twice(greet), 6);
println!("name은 아직 쓸 수 있다: {name}");
let mut count = 0;
let mut inc = || {
count += 1;
count
}; // 가변 빌림 -> FnMut
assert_eq!(call_mut_twice(&mut inc), 3); // 1 + 2
assert_eq!(inc(), 3);
println!("count = {count}");
assert_eq!(count, 3);
let owned = String::from("moved");
let give = move || owned; // 잡은 값을 밖으로 내보낸다 -> FnOnce만
assert_eq!(call_once(give), "moved");
// move로 잡아도 값을 내보내지 않으면 Fn이다.
let data = [1, 2, 3];
let sum = move || data.iter().sum::<i32>() as usize;
assert_eq!(call_twice(sum), 12);
// 2. 컴파일 오류들
expect_all(
"Fn 자리에 값을 바꾸는 클로저",
"fn call_twice<F: Fn()>(f: F) { f(); f(); }
pub fn g() { let mut n = 0; let c = || n += 1; call_twice(c); }",
&[
"error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`",
"closure is `FnMut` because it mutates the variable `n` here",
],
);
expect(
"move를 붙여도 값을 바꾸면 Fn이 아니다",
"fn call_twice<F: Fn()>(f: F) { f(); f(); }
pub fn g() { let mut n = 0; let c = move || n += 1; call_twice(c); let _ = n; }",
"error[E0525]",
);
expect(
"같은 클로저를 인자 자리에 바로 씀",
"fn call_twice<F: Fn()>(f: F) { f(); f(); }
pub fn g() { let mut n = 0; call_twice(|| n += 1); }",
"error[E0594]",
);
expect_all(
"FnOnce 클로저를 두 번 부름",
"pub fn g() { let s = String::from(\"a\"); let f = move || s; let _a = f(); let _b = f(); }",
&[
"error[E0382]: use of moved value: `f`",
"closure cannot be invoked more than once because it moves the variable `s` out of its environment",
],
);
expect(
"가변으로 잡은 클로저가 살아 있는 동안 읽기",
"pub fn g() { let mut count = 0; let mut inc = || count += 1; println!(\"{count}\"); inc(); }",
"error[E0502]",
);
expect_all(
"move 없이 스레드에 빌린 값을 넘김",
"pub fn g() { let v = vec![1]; let h = std::thread::spawn(|| v.len()); h.join().unwrap(); }",
&[
"error[E0373]: closure may outlive the current function, but it borrows `v`",
"help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword",
],
);
expect(
"move를 붙이면 통과",
"pub fn g() { let v = vec![1]; let h = std::thread::spawn(move || v.len()); h.join().unwrap(); }",
"",
);
// 3. 스레드에 넘기기
let list = [1, 2, 3];
let h = thread::spawn(move || list.iter().map(|x| x * 10).collect::<Vec<_>>());
assert_eq!(h.join().unwrap(), vec![10, 20, 30]);
// 범위 스레드(scope)는 끝나기 전에 합류하므로 move 없이 빌려도 된다.
let words = ["a", "bb", "ccc"];
let total = thread::scope(|s| {
let h = s.spawn(|| words.iter().map(|w| w.len()).sum::<usize>());
h.join().unwrap()
});
assert_eq!(total, 6);
// 4. 필드 단위로 잡기 (2021 에디션부터)
let mut p = Point { x: 1, y: 2 };
let mut bump_x = || p.x += 10; // p.x만 가변으로 빌린다
let y = p.y; // p.y는 따로 읽어도 된다
bump_x();
assert_eq!((p.x, y), (11, 2));
expect(
"필드 단위 잡기가 없으면 막혔을 코드",
"pub struct P { pub x: i32, pub y: i32 }
pub fn g() { let mut p = P { x: 1, y: 2 }; let mut c = || p.x += 1; let y = &p.y; c(); let _ = y; }",
"",
);
let e2018 = compile_with(
"rustc",
"pub struct P { pub x: i32, pub y: i32 }
pub fn g() { let mut p = P { x: 1, y: 2 }; let mut c = || p.x += 1; let y = &p.y; c(); let _ = y; }",
"2018",
);
let first = e2018.lines().find(|l| l.starts_with("error")).unwrap();
println!("[같은 코드, 2018 에디션] {first}");
assert!(e2018.contains("error[E0502]: cannot borrow `p.y` as immutable"));
// 5. 클로저의 크기는 잡은 것의 크기다.
let none = || 1;
let big = [0u8; 100];
let by_ref = || big.len();
let by_move = move || big.len();
let sizes = (
size_of_val(&none),
size_of_val(&by_ref),
size_of_val(&by_move),
);
println!(
"클로저 크기: 아무것도 안 잡음 {}, 참조로 잡음 {}, [u8; 100]을 move {}",
sizes.0, sizes.1, sizes.2
);
assert_eq!(sizes, (0, 8, 100));
// 6. 아무것도 잡지 않으면 함수 포인터로 바뀐다.
let add = |a: i32, b: i32| a + b;
let fp: fn(i32, i32) -> i32 = add;
assert_eq!(fp(5, 7), 12);
expect_all(
"무언가 잡은 클로저를 함수 포인터로",
"pub fn g() -> fn(i32) -> i32 { let n = 3; let c = move |x| x + n; c }",
&[
"error[E0308]: mismatched types",
"closures can only be coerced to `fn` types if they do not capture any variables",
],
);
// 7. 클로저 돌려주기
let add5 = make_adder(5);
assert_eq!(add5(1), 6);
assert_eq!(make_op("add")(3, 4), 7);
assert_eq!(make_op("mul")(3, 4), 12);
expect(
"move 없이 매개변수를 잡은 클로저를 돌려줌",
"pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 { |x| x + n }",
"error[E0373]",
);
// 몸통이 똑같아도 클로저마다 타입이 다르다. 그래서 한 Vec에 담으려면 Box<dyn Fn>이 필요하다.
expect(
"잡는 클로저 두 개를 한 Vec에",
"pub fn g() { let n = 1; let a = move || n; let b = move || n; let _v = vec![a, b]; }",
"no two closures, even if identical, have the same type",
);
let n = 1;
let ops: Vec<Box<dyn Fn() -> i32>> = vec![Box::new(move || n), Box::new(move || n + 1)];
assert_eq!(ops.iter().map(|f| f()).sum::<i32>(), 3);
// 다른 스레드와 함께 쓸 콜백 목록은 + Send + Sync를 붙인다.
let shared: Vec<Box<dyn Fn(i32) -> i32 + Send + Sync>> =
vec![Box::new(|x| x + 1), Box::new(move |x| x * n)];
let from_threads = thread::scope(|s| {
let a = s.spawn(|| shared[0](1));
let b = s.spawn(|| shared[1](5));
(a.join().unwrap(), b.join().unwrap())
});
assert_eq!(from_threads, (2, 5));
// 8. 정렬 키 클로저는 FnMut이라 바깥 값을 바꿀 수 있다.
let mut calls = 0;
let mut v = vec![3, 1, 2];
v.sort_by_key(|&x| {
calls += 1;
x
});
assert_eq!(v, [1, 2, 3]);
assert!(calls >= 3);
// unwrap_or_else는 FnOnce를 받는다: 잡은 값을 밖으로 내보내는 클로저도 넘길 수 있다.
let fallback = String::from("기본값");
let missing: Option<String> = std::hint::black_box(None);
let got = missing.unwrap_or_else(move || {
let mut s = fallback; // 잡은 String을 꺼내 쓴다 -> 이 클로저는 FnOnce뿐이다
s.push('!');
s
});
assert_eq!(got, "기본값!");
// 9. async 클로저 (1.85): 돌려주는 퓨처가 잡은 값을 빌린다.
let prefix = String::from("id-");
let tag = async |n: u32| format!("{prefix}{n}");
let r = futures::executor::block_on(async { (tag(1).await, tag(2).await) });
assert_eq!(r, ("id-1".to_string(), "id-2".to_string()));
let factor = 10;
let times = async |x: u32| x * factor;
assert_eq!(futures::executor::block_on(apply_async(times)), 30);
expect_all(
"보통 클로저 + async 블록은 잡은 값을 빌린 퓨처를 못 돌려준다",
"pub fn g() { let s = String::from(\"a\"); let c = move || async { s.len() }; let _f = c(); }",
&[
"error: lifetime may not live long enough",
"closure implements `Fn`, so references to captured variables can't escape the closure",
],
);
expect(
"async move 클로저는 잡은 값을 빌린 퓨처를 돌려준다",
"pub async fn h() -> usize { let s = String::from(\"a\"); let c = async move || s.len(); c().await + c().await }",
"",
);
println!("async 클로저: {:?}", r);
// 10. clippy: 함수를 그대로 부르기만 하는 클로저는 함수 이름으로 바꾸라고 한다.
let w = compile_with(
"clippy-driver",
"fn double(x: i32) -> i32 { x * 2 }
pub fn h(v: Vec<i32>) -> Vec<i32> { v.into_iter().map(|x| double(x)).collect() }",
"2024",
);
let first = w.lines().find(|l| l.starts_with("warning")).unwrap();
println!("[clippy] {first}");
assert!(w.contains("#[warn(clippy::redundant_closure)]` on by default"));
// 11. 출력 예측 카드
let mut log = Vec::new();
let mut n = 1;
let mut step = |k: i32| {
n *= k;
log.push(n);
};
step(2);
step(3);
step(4);
println!("n = {n}, log = {log:?}");
assert_eq!((n, log), (24, vec![2, 6, 24]));
println!("ALL CHECKS PASSED");
}