Rust, 쉽게 하자!

Rust 입문

[Rust] String과 &str

바로크냥 2022. 9. 25. 08:17

4. String과 &str

String과 &str은 문자열을 표현합니다. (&은 “앰퍼샌드”라고 읽습니다.)

더 정확히 말하자면, String은 문자열, &str는 문자열 슬라이스(또는 문자열 리터럴)입니다. 

다른 프로그래밍 언어에서 다루는 문자열은 String과 비슷합니다.

String은 크기를 바꿀 수 있고, 슬라이싱을 할 수 있습니다. 

슬라이싱은 일부만 잘라 낸다는 의미입니다. 

 

str은 str 형식으로는 사용되지 않고 늘 참조형인 &str 형식으로 사용됩니다.

관습적으로 str과 &str 모두 문자열 슬라이스라고 부릅니다. 문자열 리터럴 또는 슬라이스를 담는데 사용 됩니다. 문자열 리터럴이란, 쉽게 말해서, 따옴표 안의 문자들입니다.

let s = "hello";

여기에서 “hello”가 문자열 리터럴입니다. 다른 언어에서 일반적으로 문자열을 선언하듯이, 이렇게 선언하면 String 타입이 아니라 &str 즉 문자열 슬라이스 또는 문자열 리터럴 타입이 됩니다.

let s:&str = "hello";

이렇게 선언한 것과 같습니다.

 

문자열을 사용하는 우리 입장에서 이 둘의 가장 큰 차이는 가변성입니다.

&str은 불변형입니다. 

String은 가변형입니다.

가변형이라는 의미는 문자열을 자르거나 붙일 수 있다는 의미입니다.

또 다른 큰 차이는 Copy 트레이트를 가지느냐하는 것인데, 다른 글에서 자세히 알아 보겠습니다.


왜 str을 못 쓰고 &str을 써야 하는지 탐구해 보겠습니다. 어렵다고 느껴진다면 그냥 넘어가도 괜찮습니다.

fn main() {
    let s = *"hi";
    println!("{}", s);
}

// 출력
..에러
 |     let s = *"hi";
 |         ^ doesn't have a size known at compile-time

“Hi” 앞에 붙은 “*”은 역참조를 표현 하는 기호입니다.  

Rust의 참조와 역참조에 대해서  간단하게 설명 하겠습니다.

Rust에서 “&”은 참조를 나타내는 기호입니다. 참조형 변수는  C/C++의 포인터 처럼 메모리의 주소를 저장하고 있습니다.  

그래서 만약 우리가   let s = “hi”처럼 선언하면 변수 s는 &str 타입이 되고 “hi”가 저장된 주소를 저장합니다.

이때 “hi”처럼 문자열 리터럴은 문자들의 배열처럼 취급이 됩니다.

즉 메모리 안에서 문자 하나를 담는 일정한 크기의 영역들이 연속으로 이어져 있는 것으로 취급됩니다. 그리고 그 첫 번째 문자의 주소를 &str 타입 변수 s가 가지고 있습니다. 

여기서 “*”를 사용해 역참조를 하면 그 주소에 있는 내용물을 가져 오게 됩니다.

그래서  let s = *”hi"의 변수 s 의 타입은 str입니다. 

위 코드에서 

  let s = *"hi";

부분을 

  let s = “hi";

으로 하면 에러가 발생 하지 않습니다. 다만 변수 s의 타입은 &str이 됩니다. 

 

에러 메시지를 보면 컴파일 타임에 변수 s의 크기가 정해져 있지 않다고 알려 줍니다.

"hi" 길이가 2 size라는 건 눈으로 봐도 알겠는데 어떤 크기를 가졌는지 모른다?

다음 코드의 에러 메시지를 보면 더 자세히 알 수 있습니다.

fn main() {
   let s = "hello world".to_string();
    let s1 = s[1..3];
    println!("{:?}", s1);
}

// 출력
error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:3:9
  |
3 |     let s1 = s[1..3];
  |         ^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `str`
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature
help: consider borrowing here
  |
3 |     let s1 = &s[1..3];
  |              +

error[E0277]: the size for values of type `str` cannot be known at compilation time
   --> src/main.rs:4:22
    |
4   |     println!("{:?}", s1);
    |                      ^^ doesn't have a size known at compile-time
    |
    = help: the trait `Sized` is not implemented for `str`
......

위 코드는 to_string 메소드로 string 타입 문자열을 만들고 s[1..3]으로 슬라이스를 했습니다.

슬라이스를 했기 때문에 변수 s1의 타입은 str입니다. (사실은 이 자체가 에러입니다)

빌드해 보면 에러가 발생합니다. 앞선 코드와 동일합니다. 

에러 메시지를 보면, str 타입에는  'Sized' 트레이트가 구현되어 있지 않다고 합니다. 

 'Sized' 트레이트가 구현되어 있지 않으면 size를 알 수가 없습니다.

size를 알 수 없으면 불안정하다고 판단해서 사용을 제한하도록 설계되어 있어서 에러가 발생합니다. 

이것은 Rust의 메모리 관리법과 관련이 있습니다.

stack에 저장되는 데이터 타입은 컴파일할 때 size가 알려져 있습니다.

heap에 저장되는 데이터 타입은 컴파일할 때 size가 알려져 있지 않습니다.

그래서  tack에 저장하는 데이터는  컴파일할 때 size를 알 수 있어야 합니다.

 

사실 문자열 슬라이스는  heap에도 저장되지 않고 stack에도 저장되지 않습니다.

컴파일된 바이너리 형태로 메모리의 code segment라는 곳에 저장된다고 알려져 있습니다.

code segment는 text segment라고도 불리는데, 실행가능한 명령어나 오브젝트가 저장되는 공간입니다. 

code segment는 공유 가능해서 컴파일러나 쉘 명령 함수 등이 stack이나 heap에 방해받지 않고 저장되어 있습니다.

메모리 구조에 대한 내용은 다른 글에서 자세히 알아 보겠습니다.

 

위의 에러 메시지를 보면,  참조형인 &s[1..3]을 사용하라고 추천하고 있습니다.

&s[1..3]은 &str 타입입니다.

슬라이스는 반드시 &str 타입을 사용해야 합니다.

 

참조형을 사용하면 문자열 리터럴의 크기가 어떻든 그 주소의 크기는 일정 하기 때문에 stack에 저장해 두고 사용할 수 있습니다.


String과 str의 가장 큰 차이점은 데이터 길이의 가변성에 있습니다.

이 차이는 데이터가 어떤 메모리 영역에 저장되는 타입인가에 따라 달라집니다.

String의 데이터는 heap에 저장됩니다.

&str의 데이터는 code segment에 저장됩니다.

둘 다 주소를 포함한 헤더 부분은 stack에 저장 됩니다.

heap은 크기가 가변적인 데이터가 저장되는 공간이고,

stack은 크기가 불변이고 정해진 데이터가 저장되는 공간이라는 정도로 알아 둡시다. 

Rust의 난해한 점 대부분은 stack과 heap이라는 메모리의 다른 두 공간을 이해하면 쉬워집니다.

stack과 heap 이야기는 앞으로 계속 나옵니다.


다시 강조하지만, 다른 언어에서 문자열을 다루는 방식으로 Rust에서 사용하고 싶다면 String 타입을 사용해야 합니다. 

&str을 String으로 변환 하려면 to_string 메소드를 사용하거나 String::from을 사용하는 방법이 있습니다. 

다음 코드에서, Rust의 문자열을 선언하는 세 가지 방법을 통해 &str을 String으로 변환 하는 방법을 살펴 보겠습니다.

여기서 문자열은 String 타입과 &str 타입 둘 다를 의미합니다.

// 문자열 리터럴에서 String 타입으로 생성
let s1 = String::from("hello");

//  문자열 리터럴을 to_string()로 변환
let s2 = "hello".to_string();

// &str로 리터럴 참조
let s3: &str = "hello";

S1, s2는 String 타입, s3는 &str 타입입니다.

String과 &str 모두 println!()으로 출력할 수 있습니다.

 

 

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
반응형

'Rust 입문' 카테고리의 다른 글

[Rust] Array (배열)  (0) 2022.09.27
[Rust] 연산자  (0) 2022.09.27
[Rust] 데이터 타입 변환과 유추  (0) 2022.09.25
[Rust] 변수, 상수 그리고 static  (0) 2022.09.25
[Rust] 데이터 타입  (0) 2022.09.25