&str와 lifetime

&str는 string literal과 빌림 문자열 두 가지가 있습니다. string literal은 String이 아니고, 큰 따옴표안에 들어가 있는 문자열이고, 빌림 문자열은 문자열을 빌리는 것입니다.

string literal은 static 수명이 있고, 빌림 문자열은 필요한 경우에 static이 아닌 수명을 지정해줘야 합니다.

1. 예시

가. string literal 예시

fn main() {
    let my_str = "I am a string";    
    println!("{}", my_str);
}

my_str 다음의 숨겨진 타입을 보면 &’static str로 되어 있습니다.

my_str의 타입이 &'static str임

println!(“{}”, my_str);을 println!(“{}”, &my_str);로 my_str 앞에 &를 붙여도 동작합니다.

나. &str 예시

fn print_str(my_str: &str) {
    println!("{my_str}");
}

fn main() {
    let my_str = "I am a string".to_string();
    print_str(&my_str);
}

my_str은 .to_string() 함수를 적용해서 string literal을 String 타입으로 변환했고, 이것을 빌리기 위해 my_str앞에 &를 붙여서 print_str의 인수로 전달했습니다.

2. 문자열 리터럴의 lifetime

fn returns_str() -> &str {
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{my_str}");
}

위 코드는 returns_str 함수를 이용해 문자열을 리턴 받은 후 그 문자열을 화면에 출력하려고 하는 것인데,

명명된 lifetime parameter가 필요함

리턴 타입에서 명명된 라이프타임 파라미터가 기대된다는 에러 메시지가 나오면서 중간에 녹색으로 & 다음에 ‘static을 붙이는 것을 고려하라고 합니다.

반환 값이 “I am a str”, 다시 말해 문자열 리터럴이므로 &’static을 붙여야 합니다.

수정된 코드는 아래와 같습니다.

fn returns_str() -> &'static str {
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{my_str}");
}

코드를 수정하고 실행하니 ‘I am a str’가 잘 출력됩니다.

'static을 추가해서 화면 출력이 잘 됨

위에 &str을 String으로 바꿔보라고 하는 제안도 있었는데, 이것을 적용하면 아래와 같이 됩니다.

fn returns_str() -> String {
    "I am a str".to_string()
}

fn main() {
    let my_str = returns_str();
    println!("{my_str}");
}

String 타입으로 바꾸면 수명 문제도 발생하지 않고, 출력값도 같습니다.

3. &str의 lifetime 1

#[derive(Debug)]
struct City {
    name: &str,
    date_founded: u32,
}

fn main() {
    let city_names = vec![String::from("Hoccimin"), String::from("Busan")];
    let my_city = City {
        name: &city_names[0],
        date_founded: 1952,
    };

    println!("{my_city:?}");
}

위 코드는 City 구조체를 만든 후 my_city라는 인스턴스를 만드는데, name으로 city_names라는 Vector에서 첫번째 것을 빌림으로 가져와서 대입하고, date_founded는 직접 1952를 입력한 다음 화면에 출력하는 것입니다.

그런데, 실행하면

빌림 문자열인 경우의 lifetime parameter 문제

&str에 명명된 lifetime parameter가 기대된다고 하면서

struct City 다음에 <‘a>를 붙이고, name 필드의 & 다음에도 ‘a를 붙이라고 합니다. 이렇게 하면 “name이 City 만큼 오래 살아야” 하므로 문제가 해결됩니다.

여기서 a는 라이프 타임 파라미터를 지칭하는 것으로 다른 문자를 사용해도 됩니다.

아래와 같이 수정하고 실행하면

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}

fn main() {
    let city_names = vec![String::from("Hoccimin"), String::from("Busan")];
    let my_city = City {
        name: &city_names[0],
        date_founded: 1952,
    };

    println!("{my_city:?}");
}

잘 출력됩니다.

lifetime parameter인 'a를 추가해서 화면 출력 성공

4. &str의 lifetime 2

그러나, &city_names[0]이 문자열 리터럴이 아니고 빌림 문자열이기 때문에 City 필드를 name: &’static str로 수정하면

#[derive(Debug)]
struct City {
    name: &'static str,
    date_founded: u32,
}

fn main() {
    let city_names = vec![String::from("Hoccimin"), String::from("Busan")];
    let my_city = City {
        name: &city_names[0],
        date_founded: 1952,
    };

    println!("{my_city:?}");
}

아래와 같은 에러가 발생합니다.

'a가 아닌 'static을 추가해서 에러가 발생

“&city_names[0]이라는 빌린 값이 충분히 오래 살지 못한다”고 하고,
그 아래에서는 “이 사용법은 city_names가 ‘static을 위해 빌리는 것이 요구된다”고 합니다. 왜냐하면 City 구조체 정의 시 name 타입 지정시’static 을 사용했기 때문입니다.

따라서, 3번에서와 같이 ‘a를 사용해야 합니다.

그런데 City 오른쪽에 <‘a>를 추가하지 않고, name 오른쪽에 만 ‘a를 추가하면

struct City {
name: &'a str,
date_founded: u32,
}
lifetime을 선언하지 않고 사용해 에러 발생

선언되지 않은 라이프타임 에러가 발생합니다. 따라서, City 오른쪽에 <‘a>를 추가해서 lifetime을 선언해야 합니다.

literal과 variable, value, const, static

Rust에서 리터럴(literal)은 소스 코드에 직접 값을 표현한 것이며(예: 5, “hello”, true), 변수(variable)는 프로그램 실행 중에 값을 저장하고 변경할 수 있는 이름이 있는 저장 공간입니다(예: let x = 5;).

1. 리터럴과 변수

가. 리터럴(Literals)

프로그래밍에서 리터럴은 변수(variable)나 상수(const)와 같이 특정 데이터 유형의 값을 직접 나타내는 표현 방식입니다. 예를 들어, 숫자 10, 문자 ‘a’, 문자열 “hello” , true, false 등이 모두 리터럴입니다. 

  • 직접적인 값:리터럴은 프로그램 실행 중에 변경될 수 없는 고정된 값 자체를 의미합니다. 
  • 불변성: 리터럴은 정의된 이후에 그 값을 변경할 수 없습니다.
  • 표현식:때로는 코드에서 특정 값을 계산하는 표현식 자체가 리터럴로 사용되기도 합니다. 예를 들어, (2 + 3)과 같이 계산 결과가 고정된 값으로 나타나는 경우. 

나. 변수(Variables)

  • 이름이 있는 저장소: 변수는 메모리에 값을 저장하는 이름이 붙은 공간입니다.
  • 가변성 또는 불변성: Rust에서는 let x = 5;처럼 불변 변수도 만들 수 있고, let mut x = 5;처럼 가변 변수도 만들 수 있습니다. 가변 변수는 값을 변경할 수 있습니다.
  • 모든 타입 가능: 변수는 기본 타입은 물론, 구조체나 참조 등 다양한 데이터 타입의 값을 저장할 수 있습니다.
  • 데이터 저장과 조작: 변수는 프로그램에서 필요에 따라 데이터를 저장하거나 변경하는 데 사용됩니다.

다. 비교표

항목리터럴(Literal)변수(Variable)
표현 방식값을 직접 표현이름이 있는 저장 공간
변경 가능 여부변경 불가 (immutable)변경 가능 또는 불가능 (mutable / immutable)
※ Rust에서는 기본값이 immutable
저장 방식변수처럼 메모리에 저장되지 않음메모리에 저장됨
사용 목적상수값(변하지 않는 값) 표현데이터 저장 및 조작
예시5, “hello”, truelet x = 5;에서 x
let mut y = “hello”;에서 y

라. 예제 1

fn main() {
let x = 10; // 10은 정수 리터럴
let y = "hello"; // "hello"는 문자열 리터럴

println!("x = {}", x);
println!("y = {}", y);
}

여기서:

  • 10은 숫자 리터럴 (미리 정의된 값)
  • “hello”는 문자열 리터럴 (미리 코드에 써 넣은 값)

즉, 이런 리터럴 값은 코드에서 바로 보이고 바뀌지 않는 상수 같은 값입니다.


마. 예제 2

fn main() {
let a = 5; // 5는 리터럴
let b = a + 3; // 3도 리터럴, b는 변수

println!("b = {}", b); // 출력: b = 8
}
  • 여기서 5와 3은 literal 또는 literal value (미리 정해진 값)로 바뀔 수 없는데,
  • a와 b는 변수로 나중에 바뀔 수 있습니다. 그러나, Rust에서는 mut가 없으면 불변이 기본임

바. 요약

Rust에서 10, “hello”, true 같은 값들은 literal로서
코드에 직접 써 넣은, 미리 정의된 값들입니다.

2. 리터럴과 값(value)

가. Literal (리터럴)

“소스 코드에 직접 쓰인 값”
즉, 고정된 형태의 값을 코드에 명시한 것입니다.

42       // 정수 리터럴
3.14 // 부동소수점 리터럴
"hello" // 문자열 리터럴
true // 불리언 리터럴
'b' // 문자 리터럴

리터럴은 컴파일 타임에 결정되며 변하지 않습니다.


나. Value (값)

프로그램이 실행될 때 메모리에 존재하는 데이터입니다.
리터럴, 변수, 연산 결과 등 다양한 방식으로 생성될 수 있습니다.

let x = 5;            // 리터럴 5를 변수 x에 저장 → value는 5
let y = x + 2; // 연산 결과 7도 value
let s = String::from("hi"); // value는 "hi"라는 힙에 저장된 문자열

value는 runtime의 개념이며, 리터럴은 value를 만들기 위한 한 방법입니다.


다. 비교표

항목LiteralValue
정의코드에 직접 적힌 고정 값실행 중 사용되는 데이터
예시“hello”, 3.14, true변수에 저장된 값, 함수의 반환값 등
생성 시점컴파일 타임런타임
변경 가능성불변변경 가능 (가변 변수에 저장된 경우)
위치코드에 직접 작성됨메모리에 존재

라. 관계 요약

  • 리터럴은 value를 만드는 수단 중 하나입니다.
  • 모든 리터럴은 value지만, 모든 value가 리터럴은 아닙니다.

예:

let a = 1 + 2;  
// 1과 2는 리터럴 → 3은 value (리터럴이지만 연산 결과이기도 함)

let b = a * 2;
// b의 값 6은 value지만 literal은 아님 (런타임 연산 결과)

3. literal과 const, static

구분의미특징
literal코드에 직접 써 넣은 값미리 정의된 값. 변수나 상수에 저장 가능
const컴파일 시점 상수항상 값이 고정, 함수 밖/안 모두 사용 가능, 메모리 없음
static정적 변수 (전역 상수)프로그램 전체에서 공유, 고정 메모리 공간 사용

가. 예제

const CONST_VALUE: i32 = 10;
static STATIC_VALUE: i32 = 20;

fn main() {
let literal = 5; // 5는 literal

println!("literal: {}", literal);
println!("const: {}", CONST_VALUE);
println!("static: {}", STATIC_VALUE);
}

나. 설명

  1. 5는리터럴(literal)
    → 그냥 코드에 직접 쓴 고정된 값
  2. CONST_VALUE는 const
    → 컴파일 시간에 값이 확정됨. 메모리에 저장되지 않음.
    → const는 let처럼 지역적으로도 가능하지만 항상 값이 바뀌지 않음
  3. STATIC_VALUE는 static
    → 프로그램이 끝날 때까지 살아있는 고정 메모리 주소에 저장됨
    → 전역 변수처럼 사용 가능
    → 다만static mut은 unsafe하게 써야 함

다. 비교표

항목값이 고정됨변경 가능성메모리 위치사용 시기
literal스택 또는 인라인코드 내 직접
const없음 (값 인라인)컴파일 시
static❌ (기본)전역 메모리런타임 전체 기간

라. static mut의 대체 수단

static mut COUNTER: i32 = 0;

fn main() {
unsafe {
COUNTER += 1;
println!("counter: {}", COUNTER);
}
}
  • static mut은 동시성 문제 때문에 unsafe로 접근해야 함

Rust의const와 static은 값이 컴파일 타임에 반드시 결정되어야 하는데,

다음처럼 런타임 정보나 복잡한 로직으로 초기화해야 하는 경우에는 문제가 생깁니다:

static MY_STRING: String = String::from("hello"); // ❌ 에러!

🚫String은 컴파일 타임에 생성할 수 없기 때문에 static으로 직접 초기화 불가능

이럴 때 쓰는 게 바로 아래 세 가지입니다:

(1) const fn

  • const fn은 컴파일 타임에 실행 가능한 함수를 정의합니다.
  • const 상수나 const 표현식에서 사용할 수 있음
const fn square(x: i32) -> i32 {
x * x
}

const RESULT: i32 = square(4); // ✅ OK!

📌 단점: const fn은 제한이 많아서, String, Vec, 파일읽기 등은 못 씀


(2) lazy_static (deprecated 경향 있음)

  • 복잡한 값도 런타임 최초 1회 초기화 후 전역처럼 사용 가능
  • macro_use가 필요하고, 내부적으로 unsafe와 Mutex를 씀
#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
static ref CONFIG: HashMap<&'static str, i32> = {
let mut m = HashMap::new();
m.insert("threshold", 10);
m
};
}

fn main() {
println!("threshold = {}", CONFIG["threshold"]);
}

✅ 장점: 복잡한 초기화 가능
⚠️ 단점: 오래된 방식이고 macro 기반, 무거움


(3) once_cell (추천)

  • 최신 Rust 커뮤니티에서는 lazy_static 대신 once_cell을 많이 씁니다.
  • 런타임 최초 1회 초기화, macro 없이, 더 가볍고 안전
use once_cell::sync::Lazy;
use std::collections::HashMap;

static CONFIG: Lazy<HashMap<&'static str, i32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("threshold", 10);
m
});

fn main() {
println!("threshold = {}", CONFIG["threshold"]);
}

✅ 장점: lazy_static보다 깔끔하고 안전
🔒 sync::Lazy: 여러 스레드에서 안전하게 사용 가능
🧵 unsync::Lazy: 단일 스레드용으로 더 빠름


(4) const, static 등 비교표

방식초기화 시점복잡한 타입 가능스레드 안전성비고
const컴파일 타임❌ (기본 타입만)N/A빠르고 간단
static컴파일 타임❌ (기본 타입만)가능전역 메모리 사용
const fn컴파일 타임제한적가능복잡한 초기화 불가능
lazy_static런타임 (1회만)무겁고 오래된 방식
once_cell런타임 (1회만)✅ or ❌최신 방식, 매크로 없이 가능

(5) 요약

  • 간단한 상수는 const
  • 전역 정적 값은 static
  • 복잡한 초기화가 필요한 전역 값은 once_cell::Lazy 추천