슬라이스와 String·&str 문서의 검증 코드다.
//! 슬라이스와 String·&str 문서의 주장을 확인한다: 크기(String 3칸, &str·&[T] 2칸), 바이트 길이와 글자 수,
//! 글자 경계가 아닌 곳을 자르면 panic, get은 None, chars·bytes·char_indices, 정수 인덱스 금지(E0277),
//! 슬라이스가 원본을 빌리는 것(E0502·E0505), 리터럴은 &'static str이라 &String 자리에 못 넣는 것(E0308),
//! 슬라이스로는 길이를 못 바꾸는 것(E0599), clippy의 ptr_arg·char_indices_as_byte_indices,
//! 앞 n글자 자르기(prefix_chars)와 확인 문제 C1의 출력.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/09_slices-strings_verify.rs"
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
static SNIPPET: AtomicUsize = AtomicUsize::new(0);
/// 코드 조각을 tool(rustc 또는 clippy-driver)로 컴파일해 stderr를 돌려준다.
fn compile_with(tool: &str, code: &str) -> String {
let n = SNIPPET.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("vr09_{}_{n}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("snippet.rs");
std::fs::write(&file, code).unwrap();
let out = Command::new(tool)
.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("컴파일러 실행");
let _ = std::fs::remove_dir_all(&dir);
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn expect_error(code: &str, err: &str, phrase: &str) {
let stderr = compile_with("rustc", code);
assert!(
stderr.contains(&format!("error[{err}]")),
"{err} 기대, 실제:\n{stderr}"
);
assert!(stderr.contains(phrase), "'{phrase}' 기대, 실제:\n{stderr}");
println!("{err} 확인: {phrase}");
}
fn expect_lint(code: &str, lint: &str) {
let stderr = compile_with("clippy-driver", code);
assert!(stderr.contains(lint), "{lint} 기대, 실제:\n{stderr}");
println!("clippy 확인: {lint}");
}
fn compile_checks() {
expect_error(
r#"pub fn f(){ let s=String::from("hi"); let _c=s[0]; }"#,
"E0277",
"the type `str` cannot be indexed by `{integer}`",
);
expect_error(
r#"pub fn f(){ let s=String::from("hi"); let _c=s[0]; }"#,
"E0277",
"you can use `.chars().nth()` or `.bytes().nth()`",
);
expect_error(
r#"fn first_word(s:&str)->&str{ s.split(' ').next().unwrap_or("") }
pub fn f(){ let mut s=String::from("hello world"); let w=first_word(&s); s.clear(); println!("{w}"); }"#,
"E0502",
"cannot borrow `s` as mutable because it is also borrowed as immutable",
);
for label in [
"immutable borrow occurs here",
"mutable borrow occurs here",
"immutable borrow later used here",
] {
expect_error(
r#"fn first_word(s:&str)->&str{ s.split(' ').next().unwrap_or("") }
pub fn f(){ let mut s=String::from("hello world"); let w=first_word(&s); s.clear(); println!("{w}"); }"#,
"E0502",
label,
);
}
expect_error(
r#"pub fn f(){ let v=vec![1,2,3]; let sl:&[i32]=&v[1..]; drop(v); println!("{sl:?}"); }"#,
"E0505",
"cannot move out of `v` because it is borrowed",
);
// 리터럴은 &'static str이라 &String 인자에 넣을 수 없다
expect_error(
r#"fn n(s:&String)->usize{ s.len() }
pub fn f()->usize{ n("hi") }"#,
"E0308",
"found reference `&'static str`",
);
// 부분 슬라이스도 &str이라 &String 인자에 넣을 수 없다
expect_error(
r#"fn n(s:&String)->usize{ s.len() }
pub fn f(o:&String)->usize{ n(&o[1..]) }"#,
"E0308",
"expected `&String`, found `&str`",
);
// 슬라이스는 길이를 바꾸는 메서드가 없다
expect_error(
r#"pub fn f(xs:&mut [i32]){ xs.push(1); }"#,
"E0599",
"no method named `push` found for mutable reference `&mut [i32]`",
);
// &String·&Vec 인자는 기본 clippy가 경고한다
expect_lint(
r#"pub fn n(s:&String)->usize{ s.len() }"#,
"clippy::ptr_arg",
);
expect_lint(
r#"pub fn n(s:&String)->usize{ s.len() }"#,
"writing `&String` instead of `&str` involves a new object where a slice will do",
);
expect_lint(r#"pub fn n(v:&Vec<i32>)->i32{ v[0] }"#, "clippy::ptr_arg");
// 글자 번호를 바이트 위치로 쓰면 경고한다
expect_lint(
r#"pub fn n(s:&str)->Option<&str>{ for (i,c) in s.chars().enumerate(){ if c==' ' { return Some(&s[..i]); } } None }"#,
"clippy::char_indices_as_byte_indices",
);
expect_lint(
r#"pub fn n(s:&str)->Option<&str>{ for (i,c) in s.chars().enumerate(){ if c==' ' { return Some(&s[..i]); } } None }"#,
"#[deny(clippy::char_indices_as_byte_indices)]` on by default",
);
expect_lint(
r#"pub fn n(s:&str)->Option<&str>{ for (i,c) in s.chars().enumerate(){ if c==' ' { return Some(&s[..i]); } } None }"#,
"consider using `.char_indices()` instead",
);
}
fn first_word(s: &str) -> &str {
s.split(' ').next().unwrap_or("")
}
/// 앞의 n글자를 복사 없이 잘라 준다. 글자 경계에서만 자르므로 panic하지 않는다.
fn prefix_chars(s: &str, n: usize) -> &str {
s.char_indices().nth(n).map_or(s, |(i, _)| &s[..i])
}
fn sum(xs: &[i32]) -> i32 {
xs.iter().sum()
}
fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
let err = std::panic::catch_unwind(f).expect_err("panic 기대");
if let Some(s) = err.downcast_ref::<String>() {
s.clone()
} else {
(*err.downcast_ref::<&str>().unwrap()).to_string()
}
}
fn runtime_checks() {
let w = std::mem::size_of::<usize>();
assert_eq!(std::mem::size_of::<String>(), 3 * w);
assert_eq!(std::mem::size_of::<&str>(), 2 * w);
assert_eq!(std::mem::size_of::<&[i32]>(), 2 * w);
assert_eq!(std::mem::size_of::<&String>(), w);
assert_eq!(std::mem::size_of::<Box<str>>(), 2 * w);
println!(
"sizes: String {} / &str {} / &[i32] {} / &String {}",
3 * w,
2 * w,
2 * w,
w
);
// 슬라이스는 원본 버퍼를 가리킨다 (복사하지 않는다)
let s = String::from("hello world");
let world = &s[6..];
assert_eq!(world.as_ptr(), s[6..].as_ptr());
assert_eq!(world.as_ptr() as usize - s.as_ptr() as usize, 6);
assert_eq!((s.len(), s.capacity(), world.len()), (11, 11, 5));
// 리터럴은 &'static str이다. 끝 표시 없이 길이를 따로 든다
let lit: &'static str = "hello world";
assert_eq!(first_word(lit), "hello");
assert_eq!("a\0b".len(), 3);
// 바이트 길이와 글자 수
assert_eq!(("Hola".len(), "Hola".chars().count()), (4, 4));
let hello = "Здравствуйте";
assert_eq!((hello.len(), hello.chars().count()), (24, 12));
let ko = "안녕";
assert_eq!((ko.len(), ko.chars().count()), (6, 2));
let hi = "नमस्ते";
assert_eq!((hi.len(), hi.chars().count()), (18, 6));
assert_eq!(&hello[0..4], "Зд");
let bytes: Vec<u8> = "Зд".bytes().collect();
assert_eq!(bytes, [208, 151, 208, 180]);
let chars: Vec<char> = "Зд".chars().collect();
assert_eq!(chars, ['З', 'д']);
let idx: Vec<(usize, char)> = "a안b".char_indices().collect();
assert_eq!(idx, [(0, 'a'), (1, '안'), (4, 'b')]);
// 글자 경계가 아닌 곳을 자르면 panic, get은 None
std::panic::set_hook(Box::new(|_| {}));
let msg = panic_message(|| {
let h = String::from("Здравствуйте");
let _ = &h[0..1];
});
let msg_ko = panic_message(|| {
let h = String::from("안녕");
let _ = &h[..1];
});
let _ = std::panic::take_hook();
println!("panic: {msg}");
println!("panic: {msg_ko}");
assert!(msg.contains(
"end byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2 of string)"
));
assert!(msg_ko.contains(
"end byte index 1 is not a char boundary; it is inside '안' (bytes 0..3 of string)"
));
let hi_bytes: Vec<usize> = hi.chars().map(char::len_utf8).collect();
assert_eq!(hi_bytes, [3, 3, 3, 3, 3, 3]);
assert_eq!(hello.get(0..1), None);
assert_eq!(hello.get(0..2), Some("З"));
assert!(!ko.is_char_boundary(1) && ko.is_char_boundary(3));
// &str 인자는 String·리터럴·부분 슬라이스를 모두 받는다
let owned = String::from("hello world");
assert_eq!(first_word(&owned), "hello");
assert_eq!(first_word("hi there"), "hi");
assert_eq!(first_word(&owned[6..]), "world");
// 배열·Vec의 슬라이스
let a = [1, 2, 3, 4, 5];
let v: Vec<i32> = (1..=5).collect();
assert_eq!(&a[1..3], &[2, 3]);
assert_eq!(&v[1..3], &a[1..3]);
// 앞의 n글자: 바이트가 아니라 chars로
let first_two: String = hello.chars().take(2).collect();
assert_eq!(first_two, "Зд");
assert_eq!(prefix_chars("안녕하세요", 2), "안녕");
assert_eq!(prefix_chars("hi", 5), "hi");
assert_eq!("안녕하세요".chars().nth(2), Some('하'));
// &[T] 인자는 Vec·배열·부분 슬라이스를 모두 받는다
assert_eq!((sum(&v), sum(&a), sum(&v[1..3])), (15, 15, 5));
// &mut [T]로는 칸을 고칠 수 있다 (길이는 그대로)
let mut w = vec![5, 4, 3, 2, 1];
w[1..4].sort();
assert_eq!(w, [5, 2, 3, 4, 1]);
// 확인 문제 C1
let t = "a안b";
let starts: Vec<usize> = t.char_indices().map(|(i, _)| i).collect();
let c1 = format!("{} {} {:?}", t.len(), t.chars().count(), starts);
println!("C1: {c1}");
assert_eq!(c1, "5 3 [0, 1, 4]");
println!("runtime: lengths, panic on boundary, get None ok");
}
fn main() {
compile_checks();
runtime_checks();
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_word_cases() {
assert_eq!(first_word(""), "");
assert_eq!(first_word("one"), "one");
assert_eq!(first_word("안녕 세상"), "안녕");
}
#[test]
fn prefix_chars_never_panics() {
let samples = [
"",
"a",
"héllo",
"안녕하세요",
"Здравствуйте",
"नमस्ते",
"a안b",
];
for s in samples {
for n in 0..15 {
let p = prefix_chars(s, n);
assert_eq!(p.chars().count(), n.min(s.chars().count()));
assert!(s.starts_with(p));
}
}
}
}