Rust Chapter9 : Struct
- 프로그래밍 언어/Rust
- 2025. 1. 4.
들어가기 전
이 글은 프로그래밍 러스트 2판을 공부하며 작성한 글입니다.
9. 스트럭트 (Struct)
- Struct 자바, 파이썬에서 제공하는 클래스와 거의 유사한 개념이다.
- Struct는 크게 다음 세 종류가 존재한다.
- 필드형 Struct : 주로 많이 사용함.
- 튜플형 Struct : 타입 검사를 좀 더 Strict 하게 하기 위해 NewType을 생성할 때 주로 사용.
- 유닛형 Struct : 주로 Trait과 함께 사용할 때 유용하다고 한다.
필드형 Struct
- 필드는 일부만 public, private으로 만들 수 있음.
- private 필드 : 같은 모듈 내에서는 참조 가능.
- public 필드 : 외부 모듈에서도 참조 가능.
- Struct가 private 필드로만 구성되어있으면, 외부 모듈에서는 직접 Struct를 생성할 수 없다. 따라서 관습적으로 Struct를 생성하기 위해 new() 메서드를 구현 및 제공해야한다.
- .. EXPR
- 구조체 생성 과정에서 이름 있는 필드 뒤에 .. EXPR이 오면, 나머지 필드들이 Assign 된다.
- 소유권이 있는 필드는 소유권이 이동, Copy Trait을 구현한 필드는 Copy, 레퍼런스는 레퍼런스가 복사된다.
pub struct GrayscaleMap {
// 필드를 private으로 하는 경우.
pixels: Vec<u8>,
size: (usize, usize)
// 필드를 public으로로 하는 경우
// pub pixels: Vec<u8>,
// pub size: (usize, usize),
}
impl GrayscaleMap {
// 필드가 private인 경우에는 Struct를 생성조차 할 수 없다.
// 따라서 관습적으로 New() 메서드를 제공한다.
fn new(pixels: Vec<u8>, size: (usize, usize)) -> GrayscaleMap {
assert_eq!(pixels.len(), size.0 * size.1);
GrayscaleMap { size, pixels }
}
}
위 코드는 필드가 private 일 때, 외부에서 구조체 표현식으로 구조체를 생성할 수 없어서 new() 메서드가 추가되는 것을 설명한다.
struct Broom {
name: String,
height: u32,
health: u32,
position: (f32, f32, f32),
intent: BroomIntent
}
...
fn chop(broom: Broom) -> (Broom, Broom) {
let mut broom1 = Broom { height: broom.height/2, ..broom };
let mut broom2 = Broom { name: broom1.name.clone(), ..broom1 };
broom1.name.push_str(" I");
broom2.name.push_str(" II");
(broom1, broom2)
}
위 코드는 .. EXPR을 이용해 새로운 구조체를 생성하는 것을 보여준다.
- broom1이 생성될 때, broom의 필드 name이 broom1로 Move한다. 따라서 broom은 무효화된다.
- broom2를 생성할 때, name은 따로 지정하고 broom1의 값을 .. EXPR로 전달했다. 이 때, 각 타입은 Copy Trait을 구현했기 때문에 broom1은 무효화되지 않고 broom2가 생성된다.
튜플형 스트럭트
- 튜플형 스트럭트는 튜플과 유사한 형식으로 데이터를 받는다.
- 튜플형 스트럭트의 필드 역시 private, public을 지정할 수 있다.
- struct.0, struct.1 같은 방식으로 필드에 접근할 수 있다.
- 튜플은 필드의 이름이 없기 때문에 의미가 혼동될 수 있다. 따라서 튜플형 스트럭트보단 이름있는 필드를 가진 스트럭트를 많이 사용한다.
- 튜플형 스트럭트는 New Type에서 주로 사용된다. 이는 타입 검사의 Scope을 좁히고, 코드에 문맥을 추가하는 것을 의미한다.
struct Bounds(usize, usize);
// 필드 일부만 public
// struct Bounds(pub usize, usize);
// 필드 전체 public
// struct Bounds(pub usize, pub usize);
fn t() {
let image_bounds = Bounds(1024, 768);
assert_eq!(image_bounds.0, 1024);
assert_eq!(image_bounds.1, 768);
}
- 위 코드는 튜플형 스트럭트의 필드를 public, private을 사용할 수 있음을 보여준다.
- 위 코드는 튜플형 스트럭트의 각 필드에 struct.0, struct.1 방식으로 접근할 수 있음을 보여준다.
struct Ascii(Vec<u8>);
fn t1() {
let v1: Vec<u8> = vec![0, 1, 2, 3];
// 이 코드만으로는 Vec<u8> 타입이 뭘 의미하는지 알 수 없다.
let vec1: Vec<u8> = vec![0, 1, 2, 3];
let v2: Vec<u8> = vec![0, 1, 2, 3];
// Ascii 타입이란 것을 알려주면서, 이 값이 무엇을 의미하는지 더 명확해졌다.
let ascii = Ascii(v2);
}
- 위 코드는 튜플형 스트럭트를 이용해 NewType을 선언한 것을 보여준다.
- Vec<u8> 타입 자체는 어떤 문맥도 나타낼 수 있다.
- 그러나 Ascii<Vec<u8>> 타입은 Vec<u8>이 아스키 문자라는 문맥을 보여준다. 또한, Ascii 타입인지를 검사할 수도 있으므로 타입 검사 Scope이 Vec<u8>에서 좀 더 줄어드는 효과를 가져온다.
유닛형 스트럭트
- 유닛형 스트럭트는 어떠한 필드도 가지지 않는 스트럭트를 의미한다.
- 유닛형 스트럭트는 Trait과 같이 쓸 때 유용하다.
// 유닛형 스트럭트.
struct Onesuch;
let o = Onesuch;
스트럭트 레이아웃
- Struct의 필드는 모두 스택 메모리에 저장된다.
- Struct의 필드가 여러 개 있을 때, 스택 메모리에 어떤 순서로 저장되는지는 정의되어있지 않다.
위 이미지는 아래 Struct가 메모리에 어떻게 저장되는지를 보여준다.
- pixels, size는 각각 스택 프레임에 저장된다.
- pixels는 힙 메모리에 저장된 데이터를 가리키는 포인터, 그리고 길이를 스택 프레임에 저장한다.
pub struct GrayscaleMap {
pixels: Vec<u8>,
size: (usize, usize)
}
let size = (1024, 956);
let pixels = vec![0; size.0 * size.1];
let image = GrayscaleMap { size: size, pixels: pixels };
Impl로 메서드 정의하기
- struct 내에서는 필드만 정의할 수 있다. struct의 메서드는 Impl 블록 내에서 구현해야한다.
- Impl 블록에 구현된 메서드는 아래 형태가 올 수 있다.
- 아무것도 없는 경우 : new() 함수 같은 것들 만들 때
- self : 소유권 자체를 이동하는 경우 (소유권이 이동하기 때문에 Struct 자체는 무효화 될 것이다)
- &self : 내부 필드를 참조만 하는 경우
- &mut self : 내부 필드의 변경이 필요한 경우
- 만약 메서드가 첫번째 인자로 &mut self를 원한다고 하더라도, 호출하는 쪽에서 (&mut self).method(...) 같은 형식으로 호출하지 않아도 된다. 자동으로 변환해주기 때문이다.
pub struct Queue {
older: Vec<char>,
younger: Vec<char>,
}
impl Queue {
// 아무것도 오지 않는 경우
pub fn new() -> Queue {
Queue { older: vec![], younger: vec![]}
}
// self - 메서드가 자기 자신의 소유권을 가져야 하는 경우
pub fn split(self) -> (Vec<char>, Vec<char>) {
(self.older, self.younger)
}
// &mut self - 내부 필드 수정이 필요한 경우
pub fn push(&mut self, c: char) {
self.younger.push(c);
}
pub fn pop(&mut self) -> Option<char> {
...
self.older.pop()
}
}
fn he() {
let mut q = Queue::new();
// push()는 &mut self를 원한다. self.push()를 이용하더라도, 자동으로 수정해서 전달한다.
// 여기서 사용자가 굳이 (&mut q).push()를 호출하지 않아도 된다.
q.push('a');
q.push('b');
assert_eq!(q.pop(), Some('a'));
assert_eq!(q.pop(), Some('b'));
// 여기서 q는 미초기화 상태가 됨. split()이 self를 받기 때문임.
let (older, younger) = q.split();
}
- 위 코드는 impl 블록 내에 메서드를 구현하는 것을 보여준다.
- 위 코드는 메서드에 첫번째 인자로 올 수 있는 타입을 각각 보여준다.
- 메서드를 호출할 때, &mut self 같은 인자가 필요하더라도 호출부에서 굳이 (&mut self).push(...)로 호출하지 않아도 되는 것을 보여준다.
타입 연관 함수
- impl 블록 내에 선언된 함수이지만, self 인수를 받지 않는 함수를 의미한다. 주로 new() 같은 생성자 함수를 의미한다.
- 메서드는 아니지만, 그 타입과 연관된 함수이기 때문에 타입 연관 함수라고 부른다.
pub struct Queue {
older: Vec<char>,
younger: Vec<char>,
}
impl Queue {
// 타입 연관 함수 new()
pub fn new() -> Queue {
Queue { older: vec![], younger: vec![]}
}
}
타입 연관 상수
- 자바에서는 클래스 내부에 static final을 이용해 타입 연관 상수를 선언할 수 있다.
- Impl 블록 내부에 const 키워드를 이용해 타입 연관 상수를 선언할 수 있다.
pub struct Vector2 {
x: f32, y: f32
}
impl Vector2 {
const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
const UNIT: Vector2 = Vector2 { x: 1.0, y: 1.0 };
const NAME: &'static str = "Vector2";
const ID: u32 = 18;
}
- 위 코드는 impl 블록 내부에 const 키워드를 이용해 상수를 선언한 것을 보여준다.
Generic Struct
- Generic 매개변수를 이용해 Generic Struct를 구현할 수 있다.
- struct, impl에 각각 제네릭 매개변수를 사용하면 된다. 아래 예시에서는 제네릭 매개변수 T를 이용했다.
- 대문자 Self 키워드를 사용할 수 있음.
- 반환 타입으로 Queue<T> 대신 Self를 사용할 수 있음.
- 구조체 생성 시, Queue { ... } 대신 Self { ... } 를 사용할 수 있음.
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>,
}
impl<T> Queue<T> {
// Queue<T> 대신 Self 타입을 사용할 수 있음.
pub fn new() -> Self {
// Queue 대신 Self를 사용할 수 있음.
Self { older: vec![], younger: vec![]}
}
pub fn split(self) -> (Vec<T>, Vec<T>) {
(self.older, self.younger)
}
pub fn push(&mut self, c: T) {
self.younger.push(c);
}
pub fn pop(&mut self) -> Option<T> {
if self.older.is_empty() {
if self.younger.is_empty() {
return None;
}
use std::mem::swap;;
swap(&mut self.older, &mut self.younger);
self.older.reverse();
}
self.older.pop()
}
}
Lifetime을 가지는 Generic Struct
- Struct가 레퍼런스를 필드로 가지면, 레퍼런스의 유효 라이프타임이 반드시 표현되어야 한다.
struct Extrema<'elt> {
greatest: &'elt i32,
least: &'elt i32,
}
상수 매개변수를 가지는 Generic Struct
- 상수 제네릭 매개변수를 Generic으로 가지는 Generic Struct도 지원된다.
- 상수 제네릭 매개변수에는 연산이 허용되지는 않는다. (예시 : N+1)
- 아래 코드에서는 new()에 주어진 coefficients의 길이 N을 보고, 생성된 Polynomial의 타입이 추론된다.
- Struct가 여러 종류의 제네릭 매개변수를 받을 때는 다음 순서로 배치되어야 한다.
- Lifetime
- 일반 제네릭 타입매개변수
- Const 제네릭 타입 매개변수
struct Polynomial<const N: usize> {
coefficients: [f64; N]
}
impl<const N: usize> Polynomial<N> {
pub fn new(coefficients: [f64; N]) -> Polynomial<N> {
Self { coefficients }
}
}
fn t() {
let coefficients = [0.0; 6];
let poloynomial = Polynomial::new(coefficients);
}
- 위 코드에서 coefficients의 길이는 6이다. 따라서 생성된 struct의 타입은 Polynomial<6>으로 추론된다.
struct LumpOfReferences<'a, T, const N: usize> {
the_lump: [&'a T; N],
}
- 위 코드에서는 다양한 제네릭 매개변수가 왔다. 이 경우, 라이프타임 - 타입 제네릭 - const 제네릭 순서로 와야한다.
Struct 타입에 공통 Trait 구현하기
Struct끼리 같은 값을 가지는지는 어떻게 비교할 수 있을까? 혹은 Struct끼리의 대소비교는 어떻게 할 수 있을까? 이것과 관련된 주제가 공통 Trait이다.
- Rust는 Debug, PartialEq, PartialOrder 같은 Common Trait을 제공함.
- #[derive()]에 구조체가 구현하고자 하는 Common Trait을 명시하면, 자동으로 구현됨. 이 때, 구조체의 각 필드는 해당 Common Trait을 구현하고 있어야 함.
- #[derive()]로 명시하지 않는 한, 각 Trait API는 private이다. 이 정책이 default가 된 것은 기본적으로 public API라면 확장이 어렵기 때문이다.
#[derive(PartialEq, Debug)]
struct Point {
x: i32,
y: i32,
}
fn equal() {
let a = Point{ x: 123, y: 456 };
let b = Point{ x: 123, y: 456 };
let eq = a==b;
}
내부가변성 (Interior Mutability)
- 외부에서는 불변의 값처럼 보이길 원하지만, 내부에서는 가변적인 동작이 필요한 경우가 있다. 이 때 사용하는 것이 내부가변성이다.
- 예를 들어 외부에서 메서드 호출 시, &self (Shared Reference)를 얻어서 외부에서 바라봤을 때는 불변이라고 생각할 수 있다. 그러나 내부 메서드에서는 값이 수정되기를 원하는 경우를 의미한다.
- 주로 다음 스마트포인터를 이용해서 내부가변성을 구현한다.
- Cell<T> : 값을 복사해서 받아오고, 갈아끼우는 형태로 동작한다.
- RefCell<T> : 참조값을 빌려오고 업데이트하는 형태로 동작한다.
- 일반적인 참조자는 Borrow Checker를 통해서 컴파일 타임에 에러를 검출한다.
- 내부가변성에 사용되는 Cell, RefCell은 참조자 규칙을 지키지 않을 경우 런타임에 패닉을 발생시킨다. (불변 + 가변 참조자가 동시에 존재한다거나 하는...)
struct Cache {
store : RefCell<Option<i32>>
}
impl Cache {
fn new() -> Cache {
Cache { store: RefCell::new(None) }
}
// &self를 반환하기 때문에 외부에서는 불변으로 기대함.
fn get_or_default(&self, compute: impl FnOnce() -> i32) -> i32 {
if self.store.borrow().is_none() {
let result = compute();
// 그러나 내부에서는 가변으로 동작함.
*self.store.borrow_mut() = Some(result);
}
self.store.borrow().unwrap()
}
}
fn main() {
let cache = Cache::new();
let result = cache.get_or_default(|| {
println!("Computing...");
42
});
println!("{}", result);
}
'프로그래밍 언어 > Rust' 카테고리의 다른 글
Rust Chapter11: Trait (1) | 2025.01.25 |
---|---|
Rust : 레퍼런스(Reference) (1) | 2024.12.21 |
Rust : 소유와 이동 (2) | 2024.12.14 |