동작하는 코드
지금까지 문자열을 두 가지 방식으로 사용했어요. 차이를 눈으로 확인해볼게요.
fn main() {
// 방식 1: 큰따옴표만 쓰기
let greeting = "안녕하세요";
// 방식 2: String::from() 사용
let mut name = String::from("민수");
name.push_str("님");
println!("{greeting}, {name}!");
}
"안녕하세요"는 그냥 쓸 수 있는데, 왜 어떤 때는 String::from()을 써야 할까요?
직접 수정하기
greeting에push_str("!")을 추가해보세요. 어떤 에러가 나나요?name을greeting처럼let name = "민수";로 바꾸고,push_str을 시도해보세요. 무슨 일이 일어나나요?
"왜?" — 두 가지 문자열이 필요한 이유
소유권 관점에서 보면 명확해요.
| | String | &str |
| --------- | ------------------- | ---------------------------------------- |
| 비유 | 내가 주인인 문자열 | 빌린 문자열 |
| 저장 위치 | 힙 (크기 변경 가능) | 읽기 전용 메모리 또는 다른 String의 일부 |
| 수정 가능 | mut이면 가능 | 불가 |
| 소유권 | 있음 | 없음 (참조일 뿐) |
"안녕하세요"처럼 코드에 직접 쓴 문자열은 &str 타입이에요. 프로그램 바이너리에 포함되어 있고, 수정할 수 없어요. 빌려 쓰는 거예요.
String::from("안녕하세요")는 그 내용을 힙에 복사해서 내 것으로 만든 거예요. 주인이 있으니까 수정도 가능해요.
String 만드는 방법들
fn main() {
// 방법 1: String::from()
let s1 = String::from("hello");
// 방법 2: .to_string()
let s2 = "hello".to_string();
// 방법 3: format! 매크로
let name = "민수";
let age = 25;
let s3 = format!("{name}님은 {age}살");
println!("{s1}, {s2}, {s3}");
}
세 방법 모두 String 타입을 만들어요. format!은 여러 값을 조합할 때 편해요.
String 수정하기
fn main() {
let mut s = String::from("안녕");
s.push_str("하세요"); // 문자열 추가
s.push('!'); // 문자 하나 추가
println!("{s}"); // "안녕하세요!"
println!("길이: {}", s.len()); // 바이트 수
}
push_str()은 &str을 받아서 뒤에 붙여요. push()는 char 하나를 추가해요.
문자열 슬라이스 — 일부분만 빌리기
&str은 문자열의 일부분을 가리킬 수도 있어요.
fn main() {
let sentence = String::from("hello world");
let hello = &sentence[0..5]; // "hello"
let world = &sentence[6..11]; // "world"
println!("{hello}, {world}");
}
&sentence[0..5]는 sentence의 0번째부터 5번째 바이트 직전까지를 빌리는 거예요. 새로운 String을 만드는 게 아니라, 원본의 일부를 참조하는 거예요.
함수에서 문자열 받기 — &str이 더 유연해요
함수 매개변수로 문자열을 받을 때는 &str이 좋아요.
fn greet(name: &str) {
println!("안녕, {name}!");
}
fn main() {
let owned = String::from("민수");
let literal = "지수";
greet(&owned); // String을 &str로 전달 가능!
greet(literal); // &str도 당연히 가능!
}
&str을 매개변수로 쓰면 String이든 문자열 리터럴이든 둘 다 받을 수 있어요. &String을 매개변수로 쓰면 String만 받을 수 있고요. 그래서 &str이 더 유연한 선택이에요.
더 알아보기: String과 &str의 변환
String과 &str 사이를 오가는 방법이에요.
fn main() {
// &str -> String
let s: String = "hello".to_string();
let s2: String = String::from("hello");
// String -> &str
let borrowed: &str = &s; // 자동 변환
let slice: &str = s.as_str(); // 명시적 변환
println!("{borrowed}, {slice}");
}
String에서 &str로 바꾸는 건 그냥 빌리는 거라 비용이 없어요. 반대로 &str에서 String으로 바꾸는 건 힙에 복사하는 거라 비용이 들어요.
연습 1. 아래 함수를 완성하세요. 이름과 나이를 받아서 인사 문자열을 반환해요.
fn make_greeting(name: &str, age: i32) -> String {
// "안녕, {name}님! {age}살이시군요!" 형태로 반환
// 힌트: format! 매크로를 사용하세요
}
fn main() {
let msg = make_greeting("민수", 25);
println!("{msg}");
}
연습 2. 아래 코드의 에러를 고쳐보세요. 가능하면 clone()을 사용하지 않고 해결하세요.
fn print_length(s: String) {
println!("길이: {}", s.len());
}
fn main() {
let word = String::from("Rust");
print_length(word);
println!("단어: {word}"); // 에러!
}
힌트: 함수가 소유권을 가져가지 않으려면 어떻게 해야 할까요? 매개변수 타입을 바꿔보세요.
Q1. String과 &str의 가장 큰 차이는?
- A)
String은 숫자를 저장하고,&str은 문자를 저장한다 - B)
String은 소유권이 있고 수정 가능하며,&str은 빌린 참조다 - C)
String은 느리고,&str은 빠르다 - D) 차이가 없다, 같은 타입이다
Q2. 함수 매개변수로 &str을 쓰면 좋은 이유는?
- A) 속도가 더 빠르다
- B)
String과&str모두 받을 수 있어서 유연하다 - C) 에러가 발생하지 않는다
- D) 문자열을 수정할 수 있다
Q3. 아래 코드의 결과는?
fn main() {
let s = String::from("hello world");
let word = &s[0..5];
println!("{word}");
}
- A) "hello world"
- B) "hello"
- C) "h"
- D) 컴파일 에러