데이터 타입에 따른 변수 바인딩(Variable Binding)

Rust에서 variable binding(변수 바인딩)은 값을 변수 이름에 연결하는 과정을 의미하며, Rust 프로그래밍에서 변수 선언과 관련된 기본 개념입니다. Rust는 정적 타입 언어이고 기본적으로 변수가 불변(immutable)으로 선언되기에, 가변성을 가지려면 일부러 명시해야 합니다. 이와 함께 Rust의 바인딩은 shadowing(변수 가리기)이나 스코프와 밀접하게 연관되어 있어, 변수 활용 시 중요한 개념입니다.

Ⅰ. 기본 자료형(Primitive Type)

1. 변수 바인딩 기본

Rust에서 변수 바인딩은 let 키워드로 선언합니다. 이때 변수는 기본적으로 불변입니다. 따라서 선언 후 값을 변경하려 하면 컴파일 에러가 발생합니다.

fn main() {
let x = 5;
println!("x의 값: {}", x);
// x = 6; // 오류: 불변 변수에 재할당 불가
}

이 코드는 x를 5로 바인딩했지만, 이후 x = 6과 같이 변경하려 하면 컴파일 에러가 발생합니다. Rust 컴파일러는 이런 불변 변수 변경 시도를 막아 안정성을 보장합니다.

2. 가변 변수 (Mutable Binding)

변수를 변경 가능하게 하려면 mut 키워드를 사용해 가변 변수로 선언해야 합니다.

fn main() {
let mut x = 5;
println!("초기 x: {}", x);
x = 6;
println!("변경된 x: {}", x);
}

이렇게 하면 x에 대한 값 변경이 가능해집니다. 컴파일러가 에러 발생 시 mut를 추가하라고 친절히 안내하는 점도 Rust의 특징입니다.

3. 타입 추론 및 명시적 타입 선언

Rust 컴파일러는 대부분의 경우 변수 선언 시 타입을 자동 추론합니다.

let x = 42;      // i32로 추론
let y = true; // bool로 추론
let z: u32 = 10; // 명시적 타입 선언

필요에 따라 타입을 명시할 수도 있지만, 대부분 타입 추론에 맡깁니다.

4. 변수 가리기 (Shadowing)

Rust의 독특한 특징인 변수 가리기는 같은 이름의 변수를 같은 스코프 내에서 다시 let으로 선언하여 새 변수를 만드는 것입니다. 이때 이전 변수는 새 변수에 의해 가려집니다.

fn main() {
let x = 5;
let x = x + 1; // 이전 x를 가리고 새 x 선언
{
let x = x * 2;
println!("내부 스코프의 x: {}", x); // 12
}
println!("외부 스코프의 x: {}", x); // 6
}

위 예제에서 첫 번째 x는 5, 두 번째는 6, 내부 스코프의 x는 12입니다. 이렇게 하면 가변 변수 없이도 값 변경 효과를 낼 수 있고 타입도 변경할 수 있습니다.

5. 스코프와 변수 유효 범위

변수는 선언된 블록 {} 내에서만 유효하며, 블록을 벗어나면 사라집니다.

fn main() {
let x = 1;
{
let y = 2;
println!("내부 스코프 y: {}", y); // 2
}
// println!("외부 스코프 y: {}", y); // 오류: y는 유효하지 않음
}

또한 내부 스코프에서 같은 이름으로 변수를 다시 선언하면 외부 변수를 가립니다.

6. 실습 예제 종합

fn main() {
// 불변 변수
let a = 10;
println!("a: {}", a);
// a = 20; // 컴파일 에러!

// 가변 변수
let mut b = 10;
println!("b 초기값: {}", b);
b = 20;
println!("b 변경 후: {}", b);

// 변수 가리기 (shadowing)
let c = 5;
let c = c + 10; // 이전 c 가리기, 이제 c = 15
println!("c: {}", c);

{
let c = c * 2; // 내부 스코프 가리기, c = 30
println!("내부 블록의 c: {}", c);
}

println!("외부 블록의 c: {}", c);

// 타입 변경이 가능한 shadowing
let d = "문자열";
println!("d는 문자열: {}", d);
let d = d.len(); // 같은 이름 변수에 정수 대입 (shadowing)
println!("d는 문자열 길이: {}", d);
}

출력 결과:

a: 10
b 초기값: 10
b 변경 후: 20
c: 15
내부 블록의 c: 30
외부 블록의 c: 15
d는 문자열: 문자열
d는 문자열 길이: 6

Ⅱ. 벡터(Vector)와 튜플(Tuple) 타입 변수 바인딩

1. 벡터(Vector) 타입 변수 바인딩

벡터는 같은 타입 요소들을 동적 크기로 저장하는 컬렉션입니다. Rust에서 벡터는 Vec<T> 타입으로 표현됩니다.

  • 벡터 바인딩 기본 예:
fn main() {
// 빈 vector 생성 (타입 명시 필요)
let mut v: Vec<i32> = Vec::new();
// 값 추가 가능하려면 mut 필요
v.push(1);
v.push(2);
println!("{:?}", v); // 출력: [1, 2]

// 초기값과 함께 벡터 생성 (타입 추론 가능)
let v2 = vec![10, 20, 30];
println!("{:?}", v2); // 출력: [10, 20, 30]
}
  • 벡터의 요소 접근:
let first = v2[0];       // 인덱스를 통한 접근 (주의: 범위 초과 시 패닉)
let maybe_first = v2.get(0); // Option 타입 반환 (안전 접근)
  • 가변 벡터 요소 수정도 mut으로 가능:
let mut v3 = vec![1, 2, 3];
v3[1] = 5; // 두 번째 요소를 5로 변경

벡터는 가변성을 가지고 가리키는 데이터가 동적으로 바뀔 수 있으므로 보통 mut 바인딩과 함께 선언합니다.

2. 3차원 벡터

1) 가변 크기: Vec<Vec<Vec<T>>>

// 2 x 3 x 4 크기의 0으로 채워진 3차원 벡터
let x = 2;
let y = 3;
let z = 4;
let mut v: Vec<Vec<Vec<i32>>> = vec![vec![vec![0; z]; y]; x];

// 읽기
let a = v[1][2][3];

// 쓰기
v[0][1][2] = 42;

헬퍼 함수를 하나 두면 더 편합니다:

fn new_3d<T: Clone>(x: usize, y: usize, z: usize, value: T) -> Vec<Vec<Vec<T>>> {
vec![vec![vec![value.clone(); z]; y]; x]
}

let mut v = new_3d(2, 3, 4, 0i32);

2) 고정 크기: 중첩 배열(스택 또는 Box로 힙에)

크기가 컴파일 타임에 고정되어 있다면 배열을 사용할 수 있습니다.

const X: usize = 2;
const Y: usize = 3;
const Z: usize = 4;

let mut v: [[[i32; Z]; Y]; X] = [[[0; Z]; Y]; X];

v[1][2][3] = 7;

크기가 커서 스택에 올리기 부담스럽다면 Box로 감싸 힙에 둘 수도있습니다.

let mut v: Box<[[[i32; Z]; Y]; X]> = Box::new([[[0; Z]; Y]; X]);

3) 평탄화(flat)한 1차원 Vec<T> + 인덱스 계산

성능/메모리 지역성을 위해 1차원 벡터에 직접 담고 인덱스를 수식으로 계산하는 패턴도 자주 씁니다.

struct Vec3D<T> {
data: Vec<T>,
x: usize,
y: usize,
z: usize,
}

impl<T: Clone> Vec3D<T> {
fn new(x: usize, y: usize, z: usize, value: T) -> Self {
Self {
data: vec![value; x * y * z],
x, y, z,
}
}

#[inline]
fn idx(&self, i: usize, j: usize, k: usize) -> usize {
// (i, j, k) -> linear index
i * self.y * self.z + j * self.z + k
}

fn get(&self, i: usize, j: usize, k: usize) -> &T {
&self.data[self.idx(i, j, k)]
}

fn get_mut(&mut self, i: usize, j: usize, k: usize) -> &mut T {
let idx = self.idx(i, j, k);
&mut self.data[idx]
}
}

let mut v = Vec3D::new(2, 3, 4, 0i32);
*v.get_mut(1, 2, 3) = 5;

2. 튜플(Tuple) 타입 변수 바인딩

튜플은 여러 개의 서로 다른 타입 값을 하나로 묶는 복합 타입입니다. 고정 길이이며 각각 요소의 타입은 다를 수 있습니다.

  • 튜플 선언과 바인딩:
let tup: (i32, f64, char) = (500, 6.4, 'z');
  • 튜플의 요소에 접근하려면 패턴 매칭 또는 점(.) 표기법 사용:
let (x, y, z) = tup;  // 구조 분해 (destructuring)
println!("x: {}, y: {}, z: {}", x, y, z);

let five_hundred = tup.0;
let six_point_four = tup.1;
let z_char = tup.2;

튜플은 기본적으로 불변이며, 가변 선언 시 요소 변경 가능:

let mut tup2 = (1, 2);
tup2.0 = 5; // 첫 번째 요소 변경 가능

4. 스코프와 바인딩의 변수 가리기

벡터, 튜플 등도 기본 자료형과 마찬가지로 스코프 내에서 변수 가리기(shadowing)가 가능합니다:

let v = vec![1];
let v = vec![2, 3]; // 이전 v 가려짐
println!("{:?}", v); // [2, 3]

Ⅲ. 배열

1. 1차원 배열 정의 방법

let arr = [1, 2, 3];  // 길이 3의 1차원 배열

2. 2차원 배열

let arr = [[1, 2, 3], [4, 5, 6]];  // 2x3 배열

3. 3차원 배열

let arr = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
];

Ⅳ. 해시맵

Rust에서 해시맵(HashMap)의 변수 바인딩도 기본적으로 let 키워드를 사용해 이름을 해시맵 값에 바인딩하는 방식으로 이루어집니다. 즉, 해시맵도 다른 변수처럼 데이터 구조 전체를 하나의 변수 이름에 연결하는 행위입니다.

  • 해시맵을 선언할 때는 use std::collections::HashMap;를 임포트하고,
  • 보통 HashMap::new()를 호출하여 빈 해시맵을 만들고,
  • let mut를 붙여 가변 바인딩을 해야 해시맵에 키-값 쌍을 추가하거나 수정할 수 있습니다.

예를 들어:

use std::collections::HashMap;

fn main() {
// 가변 해시맵 바인딩
let mut scores = HashMap::new();

// 값 삽입
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 값 접근
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

println!("Blue 팀 점수: {}", score);

// 해시맵의 모든 키-값 쌍 출력
for (key, value) in &scores {
println!("{key}: {value}");
}
}
  • 여기서 scores 변수에 해시맵 데이터 구조가 바인딩되었고,
  • mut 키워드 때문에 값 삽입과 변경이 가능합니다.
  • .insert() 메서드로 키-값 쌍을 추가하거나 갱신하며,
  • .get() 메서드로 해당 키에 연관된 값을 읽을 수 있습니다.

요약하면, 해시맵도 배열, 벡터, 튜플, 구조체와 같이 Rust에서 변수 바인딩 개념이 동일하게 적용됩니다. let으로 이름을 데이터에 바인딩하고, 변경하려면 mut가 필수입니다. 해시맵은 키-값 쌍 컬렉션이라는 점에서 특수하지만, 바인딩이라는 개념 면에서는 일반 변수와 차이가 없습니다.

추가적으로 해시맵 내부에 구조체 같은 복합 데이터 타입을 저장해도, 변수 바인딩과 관련된 기본 원칙은 같습니다.

Ⅴ. 사용자 정의 타입

Rust에서 사용자 정의 타입(예: 구조체와 열거형)에 대한 변수 바인딩은 기본 타입이나 벡터, 튜플 등과 동일하게 let 키워드를 사용하여 값을 변수 이름에 연결하는 것을 의미합니다. 하지만 구조체와 열거형은 내부 필드나 variant에 데이터를 포함할 수 있어서, 변수 바인딩과 활용이 더 다양한 형태로 나타납니다.

아래에서 구조체와 열거형을 사용자 정의 타입의 변수 바인딩 예제로 구체적으로 설명합니다.

1. 구조체(Struct) 변수 바인딩 예제

// 구조체 정의
struct Point {
x: i32,
y: i32,
}

fn main() {
// 구조체 인스턴스를 생성하고 변수 p에 바인딩
let p = Point { x: 10, y: 20 };

// 구조체 필드에 접근
println!("x: {}, y: {}", p.x, p.y);

// 가변 바인딩 시 필드 값 변경 가능
let mut p_mut = Point { x: 5, y: 5 };
p_mut.x = 15;
println!("변경된 x: {}", p_mut.x);

// 구조체를 분해하여 필드 값을 각각 변수에 바인딩
let Point { x: a, y: b } = p;
println!("분해된 x: {}, y: {}", a, b);
}
  • let p = Point { … };에서 p는 Point 타입 값에 변수 바인딩입니다.
  • 패턴 매칭처럼 let Point { x: a, y: b } = p; 구문으로 구조체 필드를 변수 a, b에 바인딩할 수도 있습니다.
  • mut 키워드로 가변 바인딩을 선언하면 구조체 필드를 수정할 수 있습니다.

2. 열거형(Enum) 변수 바인딩 예제

// 열거형 정의
enum Message {
Quit, // 데이터 없는 variant
Move { x: i32, y: i32 }, // 필드가 있는 variant (struct-like)
Write(String), // 튜플 형태 variant
ChangeColor(i32, i32, i32), // 여러 필드를 가진 튜플 variant
}

fn main() {
// enum 값에 변수 바인딩
let msg = Message::Move { x: 10, y: 20 };

// match로 variant별 데이터 바인딩과 처리
match msg {
Message::Quit => println!("Quit variant"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
}
}
  • let msg = Message::Move { x: 10, y: 20 };에서 msg가 Message enum의 Move variant 값에 바인딩되었습니다.
  • match 문에서는 각 variant의 내부 데이터를 별도의 변수(x, y, text, r, g, b)에 패턴 매칭을 사용한 변수 바인딩으로 추출합니다.
  • 열거형 값 하나가 여러 variant 중 하나로만 존재할 수 있기에, match를 통해안전하게 처리합니다.

요약

구분변수 바인딩 방식설명
기본 타입let x = 5;변수 이름이 단순 값과 바인딩됨
구조체let p = Point { … };변수 이름이 구조체 인스턴스에 바인딩
let Point { x: a, y: b } = p;구조체 필드를 변수에 분해하여 바인딩
열거형let msg = Message::Write(“Hi”.to_string());변수에 enum variant 값 바인딩
match msg { Message::Write(text) => { … } }match 내 패턴 매칭으로 variant 내 데이터에 변수 바인딩

이처럼 사용자 정의 타입에서도 변수 바인딩은 let을 사용해 값을 변수 이름에 연결하는 동일한 개념이며, 구조체는 필드별 분해 바인딩이 가능하고, 열거형은 variant별 패턴 매칭을 통해 내부 데이터를 변수에 바인딩하는 방식으로 동작합니다.

Rust의 Slice와 컬렉션 참조

Rust의 slice(슬라이스)는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입이고, 컬렉션 참조는 컬렉션(배열 포함) 전체에 대한 참조입니다. 그러나, 모두 소유권을 갖지 않고 데이터를 참조하는 것은 같지만, 그 목적과 내부 구조, 사용 방식에 중요한 차이가 있습니다.

1. 슬라이스(Slice)

가.기본 개념

  • 슬라이스는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입으로, &[T] 또는 &str(문자열 슬라이스)와 같이 참조 형태로 표현된다.
  • 슬라이스는 원본 데이터의 소유권을 이동시키지 않으며, 원본 데이터가 유효할 때만 사용할 수 있다.
  • 슬라이스는 연속된 메모리 블록의 시작 주소와 길이 정보를 함께 저장하는 fat pointer(두 개의 값: 포인터 + 길이)이다.
  • 슬라이스는 항상 연속적인 데이터만 참조할 수 있습니다.

나.문법과 사용법

  • 슬라이스는 [start..end] 범위 문법을 사용합니다.
    • start는 포함, end는 포함하지 않습니다(즉, 반 열린 구간).
      let a = [1, 2, 3, 4, 5];
      let slice = &a[1..4]; // [2, 3, 4]
  • start나 end를 생략하면 처음이나 끝을 의미합니다.
    &a[..3] → 처음부터 3번째 전까지
    &a[2..] → 2번째부터 끝까지(끝 포함)
    &a[..] → 전체

다. 문자열 슬라이스

  • 문자열 슬라이스는 String이나 문자열 리터럴의 일부를 참조하며 타입은 &str 입니다.
  • 문자열 일부 참조
    let s = String::from(“hello world”);
    let hello = &s[0..5]; // “hello”
    let world = &s[6..11]; // “world”
  • 문자열 리터럴 
    let s = “hello, world”;     // &’static str
    let part: &str = &s[0..5]; // “hello” 부분만 참조
    “hello” 자체도 &str 타입의 슬라이스이므로, 문자열 리터럴의 일부도 참조할 수 있습니다.

라. 특징과 주의점

  • 슬라이스는 참조이므로, 원본 데이터가 유효해야 한다.
    원본 데이터가 변경(예: String의 clear 등)되면 기존 슬라이스는 무효화되어 컴파일 에러가 발생합다.
  • 슬라이스는 소유권과 라이프타임 개념을 이해하는 데 중요한 역할을 합니다. -> 아래 ‘3. 슬라이스와 라이프타임’에서 설명
  • 슬라이스는 함수의 파라미터로 자주 사용됩니다.
fn main() {
    let s = "hello, world"; 
    println!("First two elements: {:?}", first_two(s));
}

fn first_two(slice: &str) -> &str {
    if slice.len() < 2 {
        panic!("Slice must have at least two elements");
    }
    &slice[0..2]
}

위 코드에서 s는 문자열 리터럴인 문자열 슬라이스이므로 함수로 넘길 때는 &를 안붙여도 되지만(붙여도 됨), 함수에서 받을 때는 타입을 &str으로 지정하고, 반환도 &slice[0..2]이므로&str 타입으로 지정해야 합니다.

실행 결과는 First two elements: “he”입니다.

2. 컬렉션 참조

가. 개념

  • 컬렉션 참조는 컬렉션 전체에 대한 참조입니다. 예를 들어, &Vec, &String, &[T; N] 등입니다. 여기서 &[T; N]은 배열 전체에 대한 참조입니다. 엄격하게 말하면 배열은 컬렉션은 아니지만 “배열 전체를 참조한다”는 측면에서 컬렉션 참조에 포함해서 설명합니다.
  • 이 참조는 해당 컬렉션 타입의 모든 메서드와 속성을 사용할 수 있게 해줍니다.
  • 컬렉션 참조는 그 자체가 소유한 데이터 전체를 가리키며, 슬라이스처럼 부분만을 가리키지는 않습니다.
  • 예시:
    let v = vec![1, 2, 3];
    let r = &v; // Vec 전체에 대한 참조

벡터 컬렉션 참조의 간단한 예시는 다음과 같습니다.

fn main() {
let v = vec![100, 32, 57]; // Vec<i32> 생성
// 벡터에 대한 불변 참조를 사용하여 각 요소 출력
for i in &v {
println!("{}", i);
}
}
  • 여기서 &v는 Vec 타입의 벡터에 대한 불변 참조(&Vec)입니다.
  • for i in &v 구문은 벡터의 각 요소에 대한 참조(&i32)를 순회하며 안전하게 접근합니다.
  • 벡터 자체를 소유하지않고 참조만 하므로, 함수 인자나 반복문에서 자주 활용됩니다.

나. 예제

fn print_array(arr: &[i32; 3]) {
println!("{:?}", arr);
}

fn main() {
let data = [10, 20, 30];
print_array(&data); // &[i32; 3] 타입의 참조 전달
}
  • 위 예제에서 print_array 함수는 크기가 3인 i32 배열에 대한 참조(&[i32; 3])를 인자로 받습니다.
  • &data는 [i32; 3] 타입 배열 전체에 대한 참조입니다.

다. 슬라이스와 주요 차이점 비교

구분슬라이스
(&[T], &str)
컬렉션 참조 (&Vec<T>, &String)
대상컬렉션의 일부(연속 구간)컬렉션 전체
내부 구조포인터 + 길이 (fat pointer)포인터 (컬렉션 구조체 전체)
메서드 사용슬라이스 관련 메서드만 사용 가능컬렉션의 모든 메서드 사용 가능(벡터의 len(), push() 등
용도부분 참조, 함수 인자 등전체 참조, 컬렉션의 메서드 활용
동적 크기길이가 런타임에 결정됨컬렉션 타입에 따라 다름
예시&arr[1..3], &vec[..], &s[2..]&vec, &arr, &String

&[T; N]은 배열 전체를 참조하고, &[T]는 슬라이스로, 배열의 일부 구간(혹은 전체)을 참조할 수 있습니다.
예를 들어, &arr[1..3]은 &[T] 타입(슬라이스)이지만, &arr은 &[T; N] 타입(컬렉션 참조)입니다. 이처럼 &[T; N]은 정확히 N개 원소를 가진 배열 전체에 대한 참조입니다.

또한 &str과 &String은 문자열 슬라이스와 문자열 참조로 다르지만, &String이 &str으로 자동 변환이 가능하기 때문에 함수 인자로는 &str이 더 범용적이고 권장됩니다.

3. 슬라이스와 라이프타임

가. 개념

Rust에서 라이프타임(lifetime)은 참조가 유효한 기간을 명확하게 지정하는 개념으로, 참조의 유효성에 직접적인 영향을 미칩니다. 라이프타임이 중요한 이유와 그 영향은 다음과같습니다.

  • 댕글링 참조 방지
    라이프타임의 가장 큰 목적은 댕글링 참조(dangling reference)를 방지하는 것입니다. 댕글링 참조란, 이미 스코프를 벗어나 소멸된 데이터를 참조하는 상황을 의미합니다. Rust는 각 참조자의 라이프타임을 추적하여, 참조가 원본 데이터보다 오래 살아남을 수 없도록 컴파일 타임에 강제합니다.
  • 안전한 메모리 접근 보장
    라이프타임을 통해 참조자가 항상 유효한 메모리만 가리키도록 보장합니다. 예를 들어, 아래와 같은 코드는 컴파일되지 않습니다.

    let r; // r의 라이프타임 시작
    {
    let x = 5; // x의 라이프타임
    r = &x; // r이 x를 참조
    } // x의 라이프타임 종료, r은 더 이상 유효하지 않음
    println!(“{}”, r); // 컴파일 에러!
    Rust는 위 코드에서 r이 x보다 오래 살아남으려 하므로 컴파일 에러를 발생시킵니다.
  • 함수와 구조체에서의 명확한 라이프타임 관계
    여러 참조가 얽히는 함수나 구조체에서는, 각각의 참조가 얼마나 오래 유효해야 하는지 명확히 지정해야 합니다. 라이프타임 파라미터를 명시함으로써, 함수가 반환하는 참조가 입력 참조자 중 어느 것과 연관되어 있는지 컴파일러가 알 수 있습니다.
  • 암묵적 추론과 명시적 지정
    대부분의 경우 Rust는 라이프타임을 자동으로 추론하지만, 여러 참조가 얽히거나 복잡한 관계가 있을 때는 명시적으로 라이프타임을 지정해야 합니다. 이를 통해 참조 유효성에 대한 컴파일러의 보장이 더욱 강력해집니다.

나. 예제 1

Rust에서 슬라이스의 라이프타임이 중요한 이유를 보여주는 대표적인 예제는, 두 개의 문자열 슬라이스 중 더 긴 쪽을 반환하는 함수입니다. 이 예제를 통해 슬라이스의 라이프타임을 명확히 지정하지 않으면 컴파일 에러가 발생하고, 올바르게 지정하면 안전하게 참조를 반환할 수 있음을 알 수 있습니다.

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

// 라이프타임 명시가 반드시 필요!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
  • 이 함수 시그니처에서 ‘a라는 라이프타임 파라미터를 명시했습니다.
  • 이 뜻은, x와 y 모두 최소한 ‘a만큼 살아 있어야 하며, 반환되는 슬라이스도 ‘a만큼 살아 있어야 한다는 의미입니다.
  • 실제로 함수가 호출될 때, Rust는 x와 y의 라이프타임 중 더 짧은 쪽에 맞춰 반환값의 라이프타임을 제한합니다. 따라서, ‘a와 ‘b를 사용해서
    fn longest<‘a, ‘b>(x: &’a str, y: &’b str) -> &’a str
    라고 하면 x가 반환될 수도 있고 y가 반환될 수도 있기 때문에 에러가 발생합니다.
  • 또한 라이프타임을 명시하지 않아도, Rust는 반환되는 참조가 입력 참조자 중 어느 쪽과 연관되어 있는지 알 수 없어 컴파일 오류가 발생합니다. 따라서, 라이프타임을 ‘a로 통일한 것입니다.

이처럼, 슬라이스의 라이프타임을 명확히 지정하지 않으면 댕글링 참조가 생길 수 있고, Rust는 이를 컴파일 타임에 방지합니다.
따라서 라이프타임 명시는 슬라이스가 안전하게 사용될 수 있는 핵심적인 장치입니다.

위 코드에서 as_str()은 String 타입을 &str 타입으로 변환하는 메소드입니다.

다. 예제2

fn main() {
let result;
{
let string1 = String::from("abc");
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// string1, string2는 여기서 소멸
}
println!("The longest string is {}", result); // 컴파일 에러!
}
  • 위 코드는 result가 내부 스코프의 데이터인 string1 또는 string2의 슬라이스를 참조하게 되므로, 컴파일 에러를 발생시켜 댕글링 참조를 차단합니다.

Rust의 Array와 Vector 비교

Rust에서 array와 vector는 모두 여러 개의 값을 순차적으로 저장하는 자료구조입니다. 하지만 두 타입은 메모리 관리, 크기, 사용 목적 등에서 중요한 차이점이 있습니다. 이 글에서는 각 자료구조의 특징, 사용법, 예제, 그리고 언제 어떤 것을 선택해야 하는지에 대해 자세히 알아보겠습니다.

1. Array(배열)

1.1. 기본 개념

  • 고정 크기: 배열은 선언 시 크기가 고정되며, 이후 변경할 수 없습니다.
  • 동일 타입: 배열의 모든 원소는 같은 타입이어야 합니다.
  • 스택 메모리: 배열은 스택에 저장되어 빠른 접근이 가능합니다.

1.2. 배열 선언과 사용 예제

fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("첫 번째 원소: {}", arr[0]);
println!("배열의 길이: {}", arr.len());

// 배열 반복
for elem in arr.iter() {
println!("{}", elem);
}
}
  • [i32; 5]는 5개의 i32 타입 값을 가지는 배열을 의미합니다.
  • 배열의 크기는 타입의 일부이므로, arr은 항상 5개의 요소만 가질 수 있습니다.
  • arr.iter()로 반복(소유권 이전이 안되며, arr.iter()를 &arr로 바꿔도 됨)해서 요소를 elem에 할당한 후 그 값을 화면에 표시합니다.

    전체적인 출력 결과는 아래와 같습니다.

1.3. 배열의 주요 특징

  • 정적 크기: 크기가 컴파일 타임에 결정됩니다.
  • 빠른 접근: 인덱스를 통한 접근이 가능합니다.
  • 복사: 크기가 작을 경우, 배열 전체가 복사될 수 있습니다.

1.4. 배열의 한계

  • 크기를 동적으로 변경할 수 없습니다.
  • 대용량 데이터에는 적합하지 않을 수 있습니다.

2. Vector(벡터)

2.1. 기본 개념

  • 동적 크기: 벡터는 런타임에 크기를 자유롭게 변경할 수 있습니다.
  • 동일 타입: 모든 원소는 동일한 타입이어야 합니다.
  • 힙 메모리: 벡터는 힙에 저장되어 대용량 데이터 처리에 적합합니다.

2.2. 벡터 선언과 사용 예제

fn main() {
let mut vec: Vec<i32> = Vec::new();
vec.push(10);
vec.push(20);
vec.push(30);

println!("두 번째 원소: {}", vec[1]);
println!("벡터의 길이: {}", vec.len());

// 벡터 반복
for elem in &vec {
println!("{}", elem);
}

// 벡터에서 값 제거
vec.pop();
println!("마지막 원소 제거 후: {:?}", vec);
}
  • Vec::new()로 빈 벡터를 생성하면 type을 추론할 수 없으므로 Vec 다음에 <i32>가 반드시 있어야 합니다.
  • push로 값을 추가하고, pop으로 마지막 값을 제거합니다.
  • 벡터는 mut로 선언해야 원소 추가/삭제가 가능합니다.
  • vec 요소를 소유권 이전없이 참조로 하나씩 꺼내서 elem에 넣은 다음 출력합니다. 배열과 마찬가지로 &vec을 vec.iter()로 바꿔도 됩니다.

    전체적인 출력은 아래와 같습니다.
    두 번째 원소: 20(인덱스가 0부터 시작하기 때문에 1이 두번째 원소가 됩니다)
    벡터의 길이: 3
    10
    20
    30
    마지막 원소 제거 후: [10, 20]

2.3. 벡터의 주요 특징

  • 가변 크기: 필요에 따라 자동으로 크기가 늘어남.
  • 유연성: 다양한 상황에 맞게 사용할 수 있음.
  • 힙 저장: 대용량 데이터에도 적합.

2.4. 벡터의 한계

  • 배열에 비해 약간의 오버헤드가 발생할 수 있음.
  • 힙에 저장되므로 스택보다 접근 속도가 느릴 수 있음.

3. Array vs Vector 비교

특징Array(배열)Vector(벡터)
크기고정동적
선언 위치스택
선언 방식[T; N]Vec<T>
값 추가/제거불가가능 (push, pop 등)
반복.iter().iter()
사용 예시소규모, 고정 데이터대규모, 가변 데이터

4. 실전 예제: 배열과 벡터 변환

4.1. 배열을 벡터로 변환

fn main() {
let arr = [1, 2, 3];
let vec = arr.to_vec();
println!("{:?}", vec); // [1, 2, 3]
}

4.2. 벡터를 배열로 변환

  • 벡터의 길이가 고정되어 있고, 크기를 알고 있을 때만 가능합니다.
fn main() {
let vec = vec![4, 5, 6];
let arr: [i32; 3] = vec.try_into().unwrap();
println!("{:?}", arr); // [4, 5, 6]
}

5. 벡터의 다양한 메서드

fn main() {
let mut v = vec![1, 2, 3, 4, 5];

// 값 삽입
v.insert(2, 99); // 2번 인덱스에 99 삽입

// 여러 개의 값 추가
v.extend([6, 7, 8, 9]);

// 값 삭제
v.remove(1); // 1번 인덱스 값 제거

// 슬라이스
let slice = &v[1..3];
println!("{:?}", slice);

// 반복자 메서드
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled);
}
  • v.insert(2, 99)
    => 2번 인덱스에 99를 추가하니 벡터 v는 [1, 2, 99, 3, 4, 5]가 됩니다.
  • v.extend([6, 7, 8, 9]);
    => push는 맨 뒤에 값 하나를 추가하는데, extend를 한 번에 여러 개의 값을 추가할 수 있습니다. 따라서, v는 [1, 2, 99, 3, 4, 5, 6, 7, 8, 9]가 됩니다.
  • v.remove(1);
    => 1번 인덱스인 2를 제거하므로 v는 [1, 99, 3, 4, 5, 6, 7, 8, 9]가 됩니다.
  • let slice = &v[1..3];
    println!(“{:?}”, slice);
    => v 벡터에서 1번 인덱스부터 3미만인 2번 인덱스까지 참조 형식으로 가져오는 slice는 [99, 3]이 됩니다.
    Vector는 일반 포맷인 {}로는 출력이 안되므로 디버그 포맷인 {:?}으로 출력해야 합니다.
  • let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!(“{:?}”, doubled);
    => v 벡터의 요소 들에 2를 곱한 후 새로운 벡터로 변환한 후 doubled에 저장합니다. doubled의 타입도 i32타입의 Vector이므로 디버그 포맷으로 출력하면
    [2, 198, 6, 8, 10, 12, 14, 16, 18]가 출력됩니다.

6. 성능 차이의 실제 사례

6.1. 반복문에서의 성능

fn main() {
let arr = [1; 1000000];
let vec = vec![1; 1000000];

let mut sum = 0;
for i in 0..arr.len() {
sum += arr[i];
}

let mut sum2 = 0;
for i in 0..vec.len() {
sum2 += vec[i];
}
}
  • 실행 시간: 배열이 벡터보다 약간 더 빠른 경우가 많습니다.
  • 이유: 배열은 스택에 연속적으로 저장되어 CPU 캐시 효율이 높고, 컴파일러가 최적화를 더 적극적으로 적용할 수 있습니다.

위와 같이 배열의 개수를 1백만개로 숫자가 1인데도 array의 경우 overflow가 발생하므로 1십만으로 바꾸는데, 시간을 체크하는 부분을 추가하고, 천단위 쉼표를 추가하도록 아래와 같이 수정한 후

[Cargo.toml] -num-format 크레이트를 사용하기 위해 필요

[dependencies]
num-format = "0.4"

[main.rs]

use std::time::Instant;
use num_format::{Locale, ToFormattedString};

fn main() {
let arr = [1; 100_000];
let vec = vec![1; 100_000];

// 배열 합계 시간 측정
let start = Instant::now();
let mut sum = 0;
for i in 0..arr.len() {
sum += arr[i];
}
let duration = start.elapsed();
println!("Array sum: {}, elapsed: {:?}", sum.to_formatted_string(&Locale::ko), duration);


// 벡터 합계 시간 측정
let start = Instant::now();
let mut sum2 = 0;
for i in 0..vec.len() {
sum2 += vec[i];
}
let duration = start.elapsed();
println!("Vector sum: {}, elapsed: {:?}", sum2.to_formatted_string(&Locale::ko), duration);
}

실행하면 결과는 아래와 같습니다.

Array sum: 100,000, elapsed: 806.2µs
Vector sum: 100,000, elapsed: 2.3802ms

1㎳가 1000㎲이므로, Vector가 약 3배정도 오래 걸립니다.

[프로그램 설명]

  • Rust의 표준 라이브러리에서 제공하는 std::time::Instant을 사용해 각 합계 연산의 소요 시간을 측정합니다.
  • Instant::now()로 현재 시각을 기록하고, 반복문이 끝난 후 elapsed()로 소요 시간을 구합니다.
  • for i in 0..arr.len()라고 0부터 배열의 길이전까지 i를 반복하면 sum에 arr[i]를 더하도록 했는데, for i in arr.iter()라고 하고, sum += i;이라고 해도 되는데, iter()를 이용한 것이 훨씬 빠릅니다. 특히 Vector 속도가 많이 빨라졌습니다.
    Array sum: 100,000, elapsed: 728.6µs
    Vector sum: 100,000, elapsed: 875.7µs
    ※ 그런데 매번 속도가 다르기 때문에 위 수치가 절대적인 것은 아닙니다. 어느 때는 Vector가 빠른 경우도 있습니다.
  • sum 또는 sum2 다음에 num_format의 ToFormattedString(&Locale::ko)를 추가해서 숫자에 천단위마다 쉼표를 추가합니다.

6.2. 크기 변경 및 데이터 추가

  • 배열은 크기가 고정되어 있어, 데이터 추가/삭제가 불가능합니다.
  • 벡터는 push, pop, extend 등으로 동적으로 크기를 조절할 수 있지만, 이 과정에서 메모리 재할당이 발생할 수 있습니다. 대량의 데이터를 추가할 때는 재할당 오버헤드가 성능 저하 요인이 됩니다.

6.3. 벤치마크 및 실제 사용 조언

  • 고정 크기, 빠른 반복/접근이 필요하다면 배열이 유리합니다.
  • 크기가 가변적이거나, 데이터 추가/삭제가 빈번하다면 벡터가 적합합니다.
  • 대용량 데이터 처리에서 벡터는 힙 할당 및 재할당 비용이 있으므로, 성능이 민감한 경우 벡터의 용량을 미리 예약(with_capacity)하는 것이 좋습니다.

※with_capacity란?

  • Vec::with_capacity는 Rust의 벡터(Vec)를 생성할 때 초기 용량(capacity) 을 미리 지정하는 메서드입니다.
  • with_capacity(n)은 최소 n개의 요소를 저장할 수 있는 공간을 미리 할당한 빈 벡터를 생성합니다.
  • 이렇게 하면, 벡터에 요소를 추가할 때마다 메모리를 재할당하는 비용을 줄일 수 있어 성능이 향상됩니다.
  • 사용 예시
fn main() {
let mut vec = Vec::with_capacity(10);
assert_eq!(vec.len(), 0);
assert!(vec.capacity() >= 10);

for i in 0..10 {
vec.push(i);
}

assert_eq!(vec.len(), 10);
assert!(vec.capacity() >= 10);

// 11번째 push 시 재할당 발생 가능
vec.push(11);
assert_eq!(vec.len(), 11);
}
  • 위 예제에서 vec은 처음부터 10개 이상의 요소를 저장할 수 있도록 메모리를 할당받아, 10개까지는 재할당 없이 push가가능합니다.
  • 11번째 요소를 추가하면 내부 용량을 초과하므로 재할당이 발생할 수 있습니다.

7. 요약 표

구분배열 (Array)벡터 (Vector)
저장 위치스택
크기고정동적
접근 속도매우 빠름빠르나 배열보다 느림
데이터 추가/삭제불가가능
성능반복/접근에서 우위대용량·가변성에 우위
  • 배열은 크기가 고정되고, 반복/접근 성능이 최상입니다.
  • 벡터는 크기 가변성, 데이터 추가/삭제의 유연성이 강점이지만, 힙 할당과 재할당에서 오버헤드가 발생할 수 있습니다.
  • 실제 성능 차이는 데이터 크기와 사용 패턴에 따라 다르므로, 벤치마크를 통해 최적의 자료구조를 선택하는 것이 중요합니다.

벡터(Vector), 문자열(String), 슬라이스(Slice)

Rust에서는 데이터를 저장하고 조작하기 위해 다양한 컬렉션을 제공합니다. 컬렉션에는 벡터, 문자열, 슬라이스와 해시맵이 있는데, 오늘은 그중 자주 쓰이는 벡터, 문자열, 그리고 슬라이스에 대해 알아보고 다음 편에 해시맵(HashMap)에 대해 알아보겠습니다. 문자열은 Rust의 특이한 요소 중 하나입니다.


1. 벡터 (Vector)

Vec는 가변 길이의 배열로, 가장 자주 쓰이는 컬렉션 중 하나입니다.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);

    println!("{:?}", v); // [1, 2, 3]
}
  • 고정된 길이의 array와 대비되고, 같은 데이터 타입이어야 하는 것은 동일합니다.
    데이터 형식으로 Vec안에 i32라고 하나만 지정되어 있어서 여러가지 형식 입력이 가능한 tuple과 다릅니다.
  • Vec::new()로 생성하고 push()로 요소 추가. pop()으로 마지막 요소 삭제
    v.pop(); // 마지막 요소 제거
    println!(“{:?}”, v); // [1, 2]
  • 벡터 v는 mut로 가변 변수로 선언해야 데이터 추가, 삭제, 수정 가능
  • println!(“{:?}”, v)로 Debug 포맷으로 벡터 출력.

가. 벡터 초기화

let v = vec![10, 20, 30];
  • vec!를 이용해 여러 요소를 한꺼번에 입력할 수 있습니다. vec!에서 !는 매크로라고 읽습니다.

나. 벡터 접근

fn main() {
    let v = vec![10, 20, 30];
    let third = v[2];
    println!("세 번째 값: {}", third);
    // println!("세 번째 값: {}", v[3]); // panic 발생

    let maybe = v.get(1); // Option 타입 반환

    if let Some(val) = maybe {
        println!("값: {}", val);
    } else {
        println!("없음"); // None일 때 실행
    }
}
  • 벡터의 값을 추출할 때 변수명 다음에 대괄호를 입력하고 그 안에 인덱스를 입력할 수도 있고, .get을 이용할 때는 소괄호를 이용하는데, 둘의 차이점은 대괄호를 이용할 때는 인덱스가 존재하지 않으면 패닉이 발생하나, get을 이용하면 None이 반환됩니다.
  • 위 코드를 실행하면 println!(“세 번째 값: {}”, third);은 실행되는데,
    println!(“세 번째 값: {}”, v[3]);에서 패닉이 발생하므로 이후 코드는 실행되지 않습니다.
  • 따라서, println!(“세 번째 값: {}”, v[3]);을 Ctrl + /를 눌러 주석처리한 다음 실행하면 뒷 부분 get으로 구한 값까지 표시됩니다.
  • get 다음에 index로 범위를 벗어난 5를 입력하고 실행하면 None이 되므로 else문이 실행되어 “없음”이 표시됩니다.

2. 문자열 (String)

가. 문자열의 정의

fn main() {
    let s1 = String::from("Hello");
    let s2 = "World!".to_string();

    println!("{s1}, {s2}");
}
  • String은 가변 문자열 타입으로 Heap에 저장되며,
  • 일반적인 프로그래밍 언어는 큰 따옴표안에 문자열을 입력하는데, Rust는 ① String::from 다음의 괄호안에 큰 따옴표를 이용해 문자열을 입력하거나, ②큰 따옴표 안에 문자열을 입력한 후 .to_string을 추가해서 입력합니다.
  • String::from없이 큰 따옴표 안에 문자열을 넣으면 String이 아니라 다음에 설명하는 문자열 슬라이스가 되어 성격이 다릅니다.
  • 위 코드를 실행하면

나. 문자열 연결

    let s2 = "World!".to_string();
    let s3 = s1 + ", " + &s2;
    println!("{s3}");
    // println!("{s1}");
  • 문자열 연결은 + 연산자를 사용합니다.
  • let s3 = s1 + “, ” + &s2;에서 s2는 빌림(&)을 사용해서 + 후에도 존재하나, s1은 + 후에 s3로 move되었으므로 더 이상 사용할 수 없습니다.

다. 슬라이스 (Slice)

슬라이스는 컬렉션의 일부를 참조하는 타입입니다.

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}, {}", hello, world);
}
  • &s[a..b]는 a부터 b-1까지의 부분 문자열을 참조합니다. 범위 설정과 마찬가지로 b앞에 =을 추가하면 b가 포함됩니다.
  • 슬라이스는 원본이 유효한 동안만 유효합니다.

3. 문자열 리터럴(&str)과 String 비교

Rust에서 &str과 String은 모두 문자열을 나타내는 데 사용되지만, 그 역할과 특징이 다릅니다. &str은 문자열 슬라이스로, 고정 길이이고 값을 직접 소유하지 않습니다. 반면, String은 힙에 할당되어 동적으로 길이를 변경할 수 있으며 값을 소유합니다.

구분&str(문자열 리터럴 )String
저장프로그램 실행 시 정적 메모리(static memory)에 저장됩니다.힙(heap)에 할당되어 동적으로 크기가 변할 수 있습니다.
소유권소유하지 않고 참조만 합니다.데이터를 소유합니다.
가변성변경할 수 없습니다.문자열 내용을 추가, 수정, 삭제할 수 있습니다.
표현&str 또는 “문자열 리터럴” 형태로 표현됩니다. 
예1) let s = “hello world”;
예2) let s = String::from(“hello world”);
let hello = &s[0..5];
String::from(“문자열”) 또는 to_string()과 같은 메서드를 통해 생성합니다. 
예) let s = String::from(“hello world”);

간단하게 말하자면 “hello world”는 문자열 리터럴이고, type은 &str인데, String::from(“hello world”)은 type이 String입니다.
그런데, &str은 &str의 예2처럼 String을 참조하기도 합니다.

Rust의 String은 UTF-8로 인코딩됩니다.

📌 &str과 String 비교 예제 코드

fn main() {
    let s = String::from("hello world");
    let first_word = first_word(&s);
    print!("첫 번째 단어: {}", first_word);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • let s = String::from(“hello world”); : s란 변수에 hello world란 String을 저장합니다.
  • let first_word = first_word(&s); : 변수 s를 참조 형식으로 받아 first_word 함수의 인수로 전달하고, 반환 값을 다시 first_word란 변수에 저장합니다.
  • print!(“첫 번째 단어: {}”, first_word); : 위에서 구한 first_world를 화면에 출력합니다.
  • fn first_word(s: &str) -> &str { : first_word 함수는 인수 s를 &str(String 참조) 타입으로 받고, &str 형식으로 반환합니다.
  • let bytes = s.as_bytes(); : &str인 s를 string slice를 byte slice로 바꿉니다.
  • for (i, &b) in bytes.iter().enumerate() { : 위에서 구한 bytes를 하나씩 옮겨가면서 처리하는데(iter), 인덱스를 같이 반환하도록 enumerate를 같이 사용합니다.
  • if b == b’ ‘ { : b가 b’ ‘, 다시 말해 byte literal ‘ ‘와 같은 경우, 다시 말해 공백을 만나게 되면
  • return &s[0..i]; : 공백 전까지의 글자를 반환합니다.
  • &s[..] : &s가 공백 전까지의 글자이므로 이 글자 전체를 반환합니다. 세미콜론이 없으므로 표현식(expression)이고 반환값입니다.
  • 따라서, 위 코드를 실행하면 hello가 반환됩니다.

🧠 요약

타입설명
Vec<T>가변크기 배열, push, get, pop 지원
StringUTF-8로 인코딩된 힙 문자열
&str슬라이스 타입, 컬렉션 일부 참조
슬라이스소유권 없이 일부분만 안전하게 사용 가능

Rust의 상수와 복합 타입 (Compound Types)

Scalar Types에 대해서는 아래 글에서 살펴봤는데, 이번에는 상수와 복합 타입(Compound Types) 튜플과 배열을 살펴보겠습니다. 상수는 변하지 않는 값이며, 튜플은 다양한 타입의 값을 하나의 그룹으로 묶은 것이고, 배열은 동일한 타입의 값을 고정된 크기로 저장합니다.


🔷 상수 (Constants)

상수는 프로그램 전체에서 변하지 않는 값을 나타냅니다.

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("최대 점수: {}", MAX_POINTS);
}
  • 변수는 let 키워드를 사용하는데, 상수는 const 키워드를 사용하며 함수 외부/내부 모두 선언 가능
  • 타입을 반드시 명시해야 하며(추론 적용되지 않음), 대문자+언더스코어로 표기하는 것이 관례입니다.
  • 위 코드 중 100_000에서 _는 천 단위 구분 기호인 ,를 대체하는 기호입니다.
  • const는 컴파일 타임에 값이 결정됨

참고: let로 선언한 변수는 mut로 변경 가능하지만, const는 절대 변경되지 않습니다.

fn main() {
    const MAX_POINTS: u32 = 100_000;
    MAX_POINTS += 10; // 상수는 변경할 수 없음, 이 줄은 오류를 발생시킴
    println!("최대 점수: {}", MAX_POINTS);
}

MAX_POINTS를 변경하려고 하면 값을 할당할 수 없다는 에러가 발생합니다.


🔷 튜플 (Tuples)

튜플은 다양한 타입의 값을 하나의 그룹으로 묶습니다.

fn main() {
    let person: (&str, u32) = ("Alice", 30);
    let (name, age) = person;

    println!("이름: {}, 나이: {}", name, age);
    println!("튜플 직접 접근: {}", person.0);
}
  • 괄호 안에 콤마로 구분되는 값들의 목록을 작성하여 튜플을 만듭니다.
  • 고정된 길이와 순서를 가지며, 서로 다른 타입 허용
    위 예에서는 &str, 다시 말해 스트링 슬라이스와 u32 정수 타입이 섞여 있습니다.
    타입을 입력하지 않으면 추론되는데, 정수는 i32가 기본 타입이므로 i32로 추론됩니다.
  • . 문법으로 인덱스로 튜플의 요소에 접근 가능
    위 예에서 person.0은 첫번째 값인 이름을 가리키고, .1을 하면 나이를 가리키게 됩니다.

튜플의 구조해체(destructuring)

튜플의 속성인 그룹을 해체하여 각각의 값을 개별 변수에 할당하는 것을 말합니다.

위 예에서 let (name, age) = person; 란 구문을 사용했는데,
person이란 튜플의 첫번째 요소는 name에, 두번째 요소는 age 변수에 할당하는 것입니다.
다시 말해 튜플은 구조해체 또는 .인덱스를 이용해 요소에 접근할 수 있습니다.


🔷 배열 (Arrays)

배열은 동일한 타입의 값고정된 크기로 저장합니다.

fn main() {
    let scores: [i32; 3] = [90, 85, 78];

    println!("첫 번째 점수: {}", scores[0]);

    for score in scores.iter() {
        println!("점수: {}", score);
    }
}
  • 대괄호 안에 값들을 콤마로 구분하여 나열해서 배열을 만듭니다.
  • [i32; 3]와 같이 타입 뒤에 ;(:이 아님)을 붙이고 숫자를 쓰면, i32 타입 3개의 배열 의미
  • let scores = [30; 10]; 이라고 입력하면 scores 배열에 정수 30을 10개 입력한 것이 됩니다.
  • scores[0]처럼 대괄호안에 인덱스를 입력하여 배열의 요소에 접근 가능
  • for와 .iter()를 이용해서 반복 가능

배열(Array)과 벡터(vector)

배열이 유용할 때는 항상 고정된 크기의 요소를 갖는데 비해서 벡터 타입은 유사 집합체로 표준 라이브러리에서 제공되며 크기를 확장 또는 축소가 가능합니다. 배열이나 벡터 중에 뭘 선택해야 할지 확실하지 않은 상황이라면 벡터를 사용하라고 합니다.

유효하지 않은 배열 요소에 대한 접근

아래에서 a배열의 가장 큰 인덱스가 4인데, 10으로 지정하고 cargo run을 하면

fn main() {
    let a = [1, 2, 3, 4, 5];

    let element = a[10];

    println!("The value of element is: {}", element);
}

아래와 같이 길이가 5인데, 인덱스가 10이라는 경계를 벗어난 인덱스 에러가 발행합니다.


🧠 요약

항목설명
const변경 불가능한 상수, 타입 명시 필수
튜플다양한 타입을 그룹화, 순서 중요
배열동일한 타입, 고정된 크기, 인덱스로 접근