오늘은 Rust의 구조체(struct)에 대해 알아보고자 합니다.
구조체
구조체는 여러 개의 연관된 값을 가질 수 있다는 측면에서 볼 때 튜플 타입절에서 살펴본 튜플과 비슷합니다.
일반적인 C언어의 구조체를 생각하시면 편합니다.
구조체의 구성 요소들은 각각 다른 타입이 될 수 있고, 각각의 구성 요소에 이름을 붙일 수 있어 각 요소가 더 명확한 의미를 가지게 되고, 특정 요소에 접근할 때 순서에 의존할 필요가 사라지게 되어, 튜플보다 유연하게 사용할 수 있습니다.
구조체를 정의하려면 struct 키워드와 해당 구조체에 지어줄 이름을 입력합니다.
구조체의 중괄호 안에서는 필드(field)라고 부르는 각 구성 요소의 이름 및 타입을 정의합니다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
정의한 구조체를 사용하려면 해당 구조체의 각 필드에 대한 구체적인 값을 정하여 구조체의 인스턴스(instance)를 생성해야 합니다.
인스턴스를 생성하려면 구조체의 이름을 적고, 중괄호를 열고, 그 안에 필드의 이름(key)과 해당 필드에 저장할 값을 키:값 쌍의 형태로 추가해야 합니다.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
구조체 내 특정 값은 점(.) 표기법으로 얻어올 수 있습니다.
가변 인스턴스라면 다음과 같은 방식으로 특정 필드의 값을 변경할 수도 있습니다.
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
가변성은 해당 인스턴스 전체가 지니게 되고, 일부 필드만 가변으로 만들 수 없습니다.
다음 예제는 email, username을 입력받고 User 인스턴스를 반환하는 모습을 보여줍니다.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
현재 예제에서는 입력받은 email, username은 User필드의 email, username에 주입하는 귀찮음이 발생합니다.
구조체의 필드 개수가 많아지면 많아질수록 이런 귀찮음은 더 커질겁니다.
필드 초기화 축약법 사용하기
필드 초기화 축약법(field init shorthand)을 사용해서 더 적은 타이핑으로 같은 기능을 구현할 수 있습니다.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
해당 예제는 이전 예제와 동일하게 작동하는 함수입니다.
기존 인스턴스를 이용해 새 인스턴스를 만들 때 구조체 업데이트 문법 사용하기
다른 인스턴스에서 대부분의 값을 유지한 채로 새로운 인스턴스를 만들게 되는 경우, 유용하게 사용할 수 있는 방법으로 구조체 업데이트 문법(struct update syntax)가 있습니다.
구조체 업데이트 문법을 사용하지 않고 만드는 방법은 다음과 같습니다.
fn main() {
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
해당 예제에 구조체 업데이트 문법을 사용하면 다음과 같습니다.
fn main() {
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
변경되는 부분인 email만 선언해주고, 나머지 부분은 ..user1을 통해 user1의 값과 동일하게 설정할 수 있습니다.
구조체 업데이트 문법이 대입처럼 =을 사용하여 데이터를 이동시킵니다.
그래서, user2를 생성한 이후에는 user1의 username 필드가 user2로 이동하여 더 이상 사용할 수 없습니다.
그러나, user1에서의 username 필드를 제외한 나머지 필드들은 user2가 생성된 이후에도 사용할 수 있습니다.
명명된 필드 없는 튜플 구조체를 사용하여 다른 타입 만들기
rust는 튜플과 유사항 형태의 튜플 구조체(tuple structs)도 지원합니다.
튜플 구조체는 구조체 자체에 이름을 지어 의미를 주지만, 이를 구성하는 필드에는 이름을 붙이지 않고 타입만 적어 넣는 형태입니다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!("{}", black.0);
}
필드가 없는 유사 유닉 구조체
필드가 아예 없는 구조체를 정의할 수도 있습니다.
유닛 타입 ()와 비슷하게 동작하므로 유사 유닛 구조체(unit-like structs)라고 지칭합니다.
유사 유닉 구조체는 어떤 타입에 대해 트레이트를 구현하고 싶지만 타입 내부에 어떤 데이터를 저장할 필요는 없을 경우 유용합니다.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
구조체 사용해보기
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
기존에 변수의 너비와 높이를 구해서 넓이를 구하는 코드를 작성해보았습니다.
여기서 개선할 수 있는 점으로는 area 변수에서 width: u32, height: u32 2개의 변수를 받고 있습니다.
그로 인하여, 이 두값이 명확하게 연관이 있다라는 점을 찾아보기 힘듭니다.
튜플로 리팩토링하기
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
rect1 이라는 튜플을 통해 2개의 값이 연관되어져 있다는 것을 표현하였습니다.
그러나, 이 두 값중 어느 값이 weight이고, 어느 값이 height인지 구분을 할 수 없습니다.
구조체로 리팩토링하기
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Rectangle 구조체에 height, weight 필드를 u32 타입으로 정의하였습니다.
area 함수에서는 Rectangle만 값을 받고 있고, height, weight에 대한 구분도 명확합니다.
또한 &으로 값을 전달하여, 함수에 전달 이후에도 rect1 값을 main에서 사용할 수 있습니다.
트레이트 파생으로 유용한 기능 추가
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
다음과 같이 rect1의 값을 출력하는 코드를 작성하였습니다.
해당 코드를 컴파일하면 다음과 같은 에러가 발생합니다.
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!의 기본 형식인 {}으로 지정할 때는 Display라는 최종 사용자를 위한 출력 형식을 사용합니다.
여태까지 사용했던 기본 타입들은 Display가 기본적으로 구현되어져 있습니다.
구조체의 경우에는 중간중간 쉼표를 사용해야 할 수도 있고, 중괄호를 출력해야 할 수도 있고, 필드 일부를 생략해야 할 수도 있는 등 여러 가지가 가능합니다.
rust는 이런 애매한 상황에서 우리가 원하는 걸 임의로 예상해서 제공하려 하지 않기 때문에 println! 및 {} 자리표시자와 함께 사용하기 위한 Display구현체가 기본적으로 제공되지 않습니다.
에러를 추가로 더 읽다보면 다음과 같은 도움말을 찾을 수 있습니다.
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
{}이 아닌 {:?}를 사용하여 표현해보라고 합니다.
{} 내에 :?를 추가하는 것은 pirntln!에 Debug라는 출력 형식을 사용하고 싶다고 전달하는 것과 같습니다.
Debug라는 트레이트는 최종 사용자가 아닌, 개발자에게 유용한 방식으로 출력하여 디버깅하는 동안 값을 볼 수 있게 해주는 트레이트 입니다.
{:?}와 같이 변경후에도 마찬가지로 에러가 발생합니다.
error[E0277]: `Rectangle` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
rust는 디버깅 정보를 출력하는 기능을 자체적으로 가지고 있어, 우리가 만든 구조체에 해당 기능을 적용하려면 #[derive(Debug)] 외부 속성(outer attribute)을 작성해줘야 합니다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
다음 코드를 실행하면 에러가 나타나지 않고 다음과 같이 출력됩니다.
rect1 is Rectangle { width: 30, height: 50 }
Rectangle의 출력 형태가 예븐 출력 형태라고는 할 수 없지만, 필드 내의 값을 모두 보여주므로 디버깅 하는 동안 유용하게 사용할 수 있습니다.
필드가 더 많아지는 경우 읽기 편한 형태가 필요할 텐데, 이럴 경우 {:?} 대신에 {:#?}를 사용하면 됩니다.
rect1 is Rectangle {
width: 30,
height: 50,
}
다음과 같이 작성하는 방법 외에 Debug 포맷을 사용하는 방법으로는 dbg! 매크로를 사용하는 방법이 있습니다.
Rectangle의 width의 값만 관심이 있는 경우 다음과 같이 작성할 수 있습니다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
해당 코드를 실행하는 경우 다음과 같이 출력됩니다.
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
30 * scale의 대한 값과 해당 debug 위치가 어느 함수의 몇번째 줄에 위치해 있는지를 보여줍니다.
정리
rust에서의 구조체는 C언어와 동일하게 키:값 형식으로 field:value로 만들어 사용할 수 있다.
기존 인스턴스의 값들로 새로운 인스턴스를 생성할 때에는 ..을 사용하여 가능하다. 단, 새로운 인스턴스가 생성된 이후에 기존 인스턴스 값들은 기존 인스턴스에서 사용할 수 없다.
튜플 구조체로 field명이 없는 튜플 형태의 구조체를 선언하여 사용할 수 있다.
dbg! 을 통하여 특정 변수에 대하여 디버깅을 선언하여 확인할 수 있습니다.
'Rust' 카테고리의 다른 글
Rust 설치부터 실행까지 (열거형, Option) - 13 (0) | 2025.01.12 |
---|---|
Rust 설치부터 실행까지 (메서드) - 12 (0) | 2025.01.09 |
Rust 설치부터 실행까지 (슬라이스) - 10 (0) | 2025.01.06 |
Rust 설치부터 실행까지 (참조자) - 9 (0) | 2025.01.05 |
Rust 설치부터 실행까지 (소유권) - 8 (0) | 2025.01.02 |
댓글