Rust-003, String vs str slices
기본 강좌 연재에 들어가기에 앞서 문자열에 대한 설명을 먼저 다루고자 한다. 최신 개발 언어의 특징은 고전적인 컴파일러(C/C++)가 아닌 바로 실무에 사용할 수 있도록 필요한 라이브러리가 포함된 프레임워크 형태로 제공되고 메모리 관리 또한 GC(garbage collection) 형태가 많다. 개발자는 바로 실무에 유용한 함수를 사용(적용)하면 되는 것이다.
이러한 이유로 Rust를 학습할 때 C/C++ 언어의 개념이 없는 초급자는 특히 문자열을 이해하는 데 어려움을 겪는다. 포인터, 참조, Char와 String 구분이 대표적이다. C++에서 문자열을 다루는 간단한 예제를 살펴보고 Rust에서는 문자열을 어떻게 다루는지 비교해 보자. 이후에 Rust의 기본 문법부터 차근차근 정리할 것이므로 이번 포스팅은 건너뛰어도 문제가 없다.
C++에서 char, string
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char arr[100] = {'H', 'e', 'l', 'l', 'o'};
cout << sizeof(arr) << " : " << arr << endl;
char arr1[] = "Hello";
cout << sizeof(arr1) << " : " << arr1 << endl;
const char *arr2 = "Hello";
cout << sizeof(arr2) << " : " << arr2 << endl;
char *arr3 = new char[100];
strcpy_s(arr3, 100, "Hello");
strcat_s(arr3, 100, " World");
cout << strlen(arr3) << " : " << arr3 << endl;
delete[] arr3;
string str = "Hello";
str = str + " World";
cout << size(str) << " : " << str << endl;
const char *strChar = str.c_str();
cout << strlen(strChar) << " : " << strChar << endl;
return 0;
}
C++에서 string은 기본 자료형이 아니라 클래스/객체이다. 대부분의 개발 언어가 원시적으로 기본 데이터 타입에는 문자열(String)은 없다. char의 연속일 뿐이다. 그래서 배열과 포인터로 이 연속을 문자열로 표현(View)하는 것이다. C++에서는 이것을 string 클래스로 제공한다.
Rust에 슬라이스(Slices)라는 용어가 있는데 이것은 컬렉션의 일부 연속 요소를 참조하는데 사용한다.1 Rust에서 &str은 (string)slices이며 C++의 char*와 유사하고 String은 C++의 string과 유사하지만 많은 차이점은 존재한다.
String은 힙(Heap)에 할당되고 변경가능(Mutable)하다. 반면에 &str은 좀더 복잡한데 기본적으로 하드코딩된 문자열처럼 변경이 불가하다. &str은 스택에 할당되기도 하고, 때론 힙 참조이며 코드에 포함되기도 한다.2
String vs &str
String holds a string in memory and owns the memory for it. &str is just a reference to another string but it doesn’t own the memory for it.3 Prefer &str as a function parameter or if you want a read-only view of a string; String when you want to own and mutate a string.4
String
Stringcontains a string in memory and owns the memory for it.- Use
Stringfor returning strings created within a function or (usually) when storing strings in a struct or enum. - If you have a
Stringyou can pass a reference to it to convert it to&str.
&str
&stris just a reference to another string (slice) but does not own the memory for it.- Prefer
&strin function arguments to accept string slices and make it clear the function will not mutate the string. - If you have a
&str and want a newStringyou can clone it either byto_owned()orto_string()(they are effectively the same - use whichever makes your code clearer to read and consistent). These will copy the memory and make a newString.
// &str
let c: &str = "Hello World";
// String
let s: String = c.to_string();
let s: String = String::from("Hello World");
&str은 Rust 언어가 구현하는 원시 타입이지만 String은 표준 라이브러리에 구현되어 있다.
『String is an owned buffer of UTF-8 bytes allocated on the heap. Mutable Strings can be modified, growing their capacity as needed. &str is a fixed-capacity “view” into a String allocated elsewhere, commonly on the heap, in the case of slices dereferenced from Strings, or in static memory, in the case of string literals. &str is a primitive type implemented by the Rust language, while String is implemented in the standard library.5』
Translate between String and &str
to_string() 메소드는 &str를 String로 변환하고, String에서 참조를 빌리면 &str로 자동 변환된다.
fn main() {
let s = "Jane Doe".to_string();
say_hello(&s);
}
fn say_hello(name: &str) {
println!("Hello {}!", name);
}
위의 예제를 좀더 확장해 보면:
fn main() {
let c: &str = "World"; // immutable
// let s: &'static str = "World";
let s: String = c.to_string();
let result = say_hello(&s);
println!("{}", result);
let mut my_str = "가나닭"; // to mutable
println!("{}", my_str);
my_str = &"마바삵";
println!("{}", my_str);
}
fn say_hello(name: &str) -> String {
let s: String = String::from("Hello");
let c: char = 32 as char;
s + &c.to_string() + &name.to_string()
}
String의 원 소스를 보면 아래와 같이 정의되어 있다.
#[derive(PartialOrd, Eq, Ord)]
#[cfg_attr(not(test), rustc_diagnostic_item = "String")]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct String {
vec: Vec<u8>,
}
Rust는 소유권(ownership), 빌림/대여(borrowing), 슬라이스의 개념으로 프로그램의 메모리 안전성을 컴파일 타임에 보장한다. 다른 프로그램언어와 같이 메모리 사용에 대한 제어권은 주지만, 데이터의 소유자가 스코프 밖으로 벗어났을 때 소유자가 자동으로 데이터를 버리도록 하는 것이다.
이를 통해 직접 메모리 할당/해제를 하거나 가비지 컬렉터를 사용하지 않아도 메모리를 관리할 수 있다. 소유권의 규칙은 모든 값은 Owner라고 불리는 변수가 있고 한 번에 하나의 Owner만 존재할 수 있어서 Owner가 스코프 밖으로 벗어나면 값은 버려진다. 보다 자세한 설명은 이후 연재할 강좌에서 개별 주제로 다룰 예정이다.
Reference
- YONGJIN LAB, “[Rust] 슬라이스”
- Doug Milford, “Rust String vs str slices”, 『Often allocated on the stack, sometimes a heap reference, sometimes embeded in the code』
- dev.to, “Rust String vs &str”
- ameyalokare.com, “Rust: str vs String”
- prev.rust-lang.org, “Frequently Asked Questions, Strings”
- Rust Tutorial (5)