콘텐츠로 이동

Rust 구조체

구조체(struct)는 서로 관련된 값들을 이름 붙은 하나의 타입으로 묶어요. 튜플이 이름 없이 빠르게 묶는 꾸러미라면, 구조체는 필드 하나하나에 이름을 붙여 줘요. 덕분에 구조체를 쓰는 코드가 훨씬 읽기 쉬워져요.

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{}님은 {}살이에요", alice.name, alice.age);
}

실행 결과:

Alice님은 30살이에요
▶ 직접 실행해 보기

튜플 ("Alice", 30)과 비교해 보세요. 구조체에서는 alice.name과 alice.age만 봐도 무슨 뜻인지 바로 알 수 있어요. 0번 필드가 이름이고 1번 필드가 나이라는 걸 외울 필요가 없죠.

변수와 마찬가지로 구조체 인스턴스도 기본적으로 불변이에요. 만든 뒤에 필드를 바꾸고 싶다면 mut를 붙이세요.

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{}님은 이제 {}살이에요", alice.name, alice.age);
}
▶ 직접 실행해 보기

impl 블록을 쓰면 구조체에 함수를 붙일 수 있어요. 그 안에서 &self는 메서드를 호출한 바로 그 인스턴스를 가리켜요.

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("안녕하세요, 저는 {}, {}살이에요", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

실행 결과:

안녕하세요, 저는 Alice, 30살이에요
▶ 직접 실행해 보기

impl 안에 인스턴스를 만들어 주는 new 함수를 두는 패턴을 자주 써요. 그러면 호출하는 쪽에서 필드를 일일이 다 적지 않아도 돼요.

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{}님은 {}살이에요", bob.name, bob.age);
}
▶ 직접 실행해 보기

Person { name: name.to_string(), age } 안에 age가 혼자 쓰인 걸 보세요. 변수 이름이 필드 이름과 똑같을 때는 age: age라고 쓰지 않고 그냥 age라고만 써도 돼요. Rust에서는 이걸 필드 초기화 축약법(field init shorthand)이라고 불러요.

위에서 만든 구조체에 println!("{:?}", alice)를 써 보면 컴파일되지 않아요. 직접 만든 타입을 디버그 출력하는 방법을 Rust에게 알려 주지 않았기 때문이에요. 구조체 위에 #[derive(Debug)]를 붙이면 그 기능을 공짜로 얻을 수 있어요.

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

실행 결과:

Person { name: "Alice", age: 30 }
▶ 직접 실행해 보기

#[derive(...)]는 타입에 흔히 쓰는 기능을 자동으로 만들어 주는 Rust의 방법이에요. Debug를 비롯해, 나중에 배울 Clone 같은 것들도 이렇게 추가하는 모습을 정말 자주 보게 될 거예요.

width와 height 필드(u32)를 가진 Rectangle 구조체를 만들어 보세요. 넓이를 돌려주는 area와, 두 변의 길이가 같으면 true를 돌려주는 is_square, 두 메서드를 추가하세요. 힌트: 두 메서드 모두 필드를 읽기만 하니까 &self를 받으면 돼요.

정답 보기
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("wide: 넓이 {}, 정사각형? {}", wide.area(), wide.is_square());
println!("square: 넓이 {}, 정사각형? {}", square.area(), square.is_square());
}

실행 결과:

wide: 넓이 24, 정사각형? false
square: 넓이 25, 정사각형? true
▶ 직접 실행해 보기

owner(String)와 센트 단위의 balance(u32)를 가진 Account 구조체를 만들어 보세요. 잔액을 0으로 시작하는 Account::new("Ana")를 작성하세요. 그다음 필드를 바꿔서 50센트와 25센트를 입금하고, 계좌를 두 가지 방법으로 출력해 보세요. 평범한 문장으로 한 번, {:?}로 한 번이요. 힌트: account.balance를 바꾸려면 변수가 let mut여야 하고, {:?}를 쓰려면 #[derive(Debug)]가 필요해요.

정답 보기
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{}님의 잔액은 {}센트예요", account.owner, account.balance);
println!("{:?}", account);
}

실행 결과:

Ana님의 잔액은 75센트예요
Account { owner: "Ana", balance: 75 }
▶ 직접 실행해 보기

new가 직접 balance: 0을 설정하기 때문에, 호출하는 쪽에서 잊어버릴 일이 없어요. 생성자 함수를 이렇게 많이 쓰는 큰 이유 중 하나예요.

Person 값 세 개를 벡터에 넣어 보세요. Ana(34살), Ben(16살), Chen(52살)이에요. 18살 이상인 사람의 이름을 모두 출력하고, 그다음 가장 나이가 많은 사람을 출력하세요. 힌트: for person in &people로 반복하고, let mut oldest = &people[0];로 지금까지 가장 나이가 많은 사람을 기억해 두세요.

정답 보기
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{}님은 성인이에요", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("최연장자: {} ({}살)", oldest.name, oldest.age);
}

실행 결과:

Ana님은 성인이에요
Chen님은 성인이에요
최연장자: Chen (52살)
▶ 직접 실행해 보기

oldest는 복사본이 아니라 벡터 안을 가리키는 참조자(&Person)라서, 아무것도 복제되지 않아요. 더 나이 많은 사람을 찾을 때마다 oldest가 그 사람을 가리키도록 바꾸기만 하면 돼요.

  • 구조체는 이름 붙은 필드들을 하나의 타입으로 묶어요: struct Person { name: String, age: u32 }.
  • 인스턴스는 Person { name: ..., age: ... }로 만들어요. 나중에 필드를 바꾸려면 mut를 붙이세요.
  • impl 블록으로 동작을 추가해요. &self는 메서드를 호출한 인스턴스를 가리켜요.
  • #[derive(Debug)]를 붙이면 구조체를 {:?}로 출력할 수 있어요.

이해도 점검

1. 튜플과 비교했을 때 구조체의 가장 큰 장점은 무엇인가요?

2. 구조체에 #[derive(Debug)]를 붙이면 무엇을 할 수 있나요?

3. Person { name: name.to_string(), age }에서 혼자 쓰인 age는 무슨 역할을 하나요?

점수: 0 / 3