구조체와 열거형의 복잡한 패턴 매칭과 가변 바인딩

구조체(Struct)와 열거형(Enum)의 여러가지 패턴 매칭(Match) 방법과 가변 바인딩(mutable binding)에 대해 알아보겠습니다. 구조체 분해후 바인딩, if let, _, .., | 등의 사용법과 참조와 Shadowing에 대해서도 알아봅니다.

1. 구조체(Struct)에서 복잡한 패턴 매칭과 가변 바인딩

가. 구조체 필드 일부만 매칭하기:

구조체 패턴에서 모든 필드를 반드시 매칭하지 않아도 되며, ..을 써서 나머지 필드는 무시할 수 있습니다.

struct Point {
x: i32,
y: i32,
z: i32,
}

fn main() {
let p = Point { x: 1, y: 2, z: 3 };

match p {
Point { x, .. } => println!("x 값에만 관심 있음: {}", x),
}
}

나. 가변 바인딩 (mutable binding)

기본적으로 Rust의 match에서 바인딩된 변수는 불변입니다. 가변으로 만들려면 mut 키워드를 변수 바인딩 전에 붙입니다.

let mut p = Point { x: 1, y: 2, z: 3 };

match &mut p { // 구조체를 가변 참조로 매칭
Point { x, y, .. } => {
*x += 10; // 가변 참조로 접근 가능
*y += 20;
println!("변경된 x: {}, y: {}", x, y);
}
}
  • 위 예에서 &mut p로 가변 참조를 패턴 매칭했고, 패턴 안의 x와 y는 가변 참조(&mut i32)가 되어 값을 변경할 수 있습니다.

다. 구조체 분해 후 변수 재바인딩

let p = Point { x: 10, y: 20, z: 30 };
let Point { x: a, y: b, z: c } = p;
println!("a: {}, b: {}, c: {}", a, b, c);

a, b, c가 각각 p의필드 값에 바인딩됩니다.

2. 열거형(Enum)에서 복잡한 패턴 매칭과 가변 바인딩

가. 열거형 variant에 포함된 여러 필드 매칭

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let mut msg = Message::Move { x: 10, y: 20 };

match &mut msg {
Message::Move { x, y } => {
*x += 5; // 가변 참조로 내부 값 변경 가능
*y += 5;
println!("이동 좌표 변경: x={}, y={}", x, y);
}
_ => {}
}
}
  • match &mut msg를 통해 열거형 값을 가변 참조로 매칭하면, 내부 필드도 가변 참조로 바인딩되어 값 변경이 가능합니다.

나. 특정 variant만 언패킹하기

if let Message::Write(text) = msg {
println!("메시지: {}", text);
}
  • if let 문법을 활용해 관심 있는 variant 한 가지만 매칭해 변수 바인딩 후 처리할 수 있습니다.

다. 여러 variant를 한 패턴에 묶기

match msg {
Message::Quit | Message::Write(_) => println!("Quit 또는 Write variant"),
_ => println!("다른 variant"),
}
  • 여러 variant를 하나로 합쳐 처리할 수도 있습니다.

3. 패턴 바인딩 시 참조와 가변성

  • 기본적으로 match 패턴 내 변수 바인딩은 값 소유권을 가져오거나 복사하지만, 참조 및 가변 참조로도 바인딩할 수 있습니다.
let p = Point { x: 10, y: 20, z: 0 };

match &p { // 불변 참조 패턴 매칭
Point { x, y, .. } => println!("x={}, y={}", x, y),
}

let mut p2 = Point { x: 0, y: 0, z: 0 };

match &mut p2 { // 가변 참조 패턴 매칭
Point { x, y, .. } => {
*x += 1;
*y += 1;
println!("수정된 값 x={}, y={}", x, y);
}
}
  • 참조에 따라 변수 바인딩 시 가변성 여부가 달라지고, 이를 통해 구조체 또는 열거형 내부 값을 안전하게 변경할 수 있습니다.

4. 변수 이름 재사용(shadowing)과 패턴 매칭

패턴 매칭 시, 이전에 바인딩된 변수 이름과 동일한 이름을 써도 새로운 바인딩이 생성됩니다.

let x = 5;
let p = Point { x: 10, y: 20, z: 30 };

match p {
Point { x, y, .. } => {
// 여기 x는 패턴 내의 새 바인딩, 밖의 x와 다른 변수
println!("패턴 매칭 내의 x: {}, 외부 x: {}", x, 5);
}
}

5. 요약 정리

기능예시 (일부만)설명
구조체 부분 패턴 매칭Point { x, .. }특정 필드만 분해, 나머지는 무시
가변 참조 매칭match &mut p와 Point { x, y, .. }구조체 필드를 가변 참조로 바인딩해 값 수정 가능
열거형 가변 매칭match &mut msg와 Message::Move { x, y }enum 내부 필드를 가변 참조로 바인딩해 값 수정 가능
if let 구문if let Message::Write(text) = msg특정 variant만 골라 간단히 바인딩 후 처리
패턴 내 변수 이름 재사용Point { x, y } 내 x는새로운 바인딩기존 변수와 이름이 동일해도 새 변수로 처리

이상으로 구조체와 열거형에서 변수 바인딩 시 사용하는 다양한 패턴 매칭 기법과 가변 바인딩 방법에 대해 설명드렸습니다.

데이터 타입에 따른 변수 바인딩(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별 패턴 매칭을 통해 내부 데이터를 변수에 바인딩하는 방식으로 동작합니다.