소개
구조체를 사용하는 예제 프로그램에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 너비와 높이에 대한 별도의 변수를 사용했던 초기 코드를 리팩토링하여 구조체를 사용하여 직사각형의 면적을 계산하는 프로그램을 작성합니다.
구조체를 사용하는 예제 프로그램에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 너비와 높이에 대한 별도의 변수를 사용했던 초기 코드를 리팩토링하여 구조체를 사용하여 직사각형의 면적을 계산하는 프로그램을 작성합니다.
구조체를 사용해야 하는 경우를 이해하기 위해 직사각형의 면적을 계산하는 프로그램을 작성해 보겠습니다. 먼저 단일 변수를 사용한 다음, 구조체를 대신 사용할 때까지 프로그램을 리팩토링합니다.
Cargo 를 사용하여 픽셀 단위로 지정된 직사각형의 너비와 높이를 가져와 직사각형의 면적을 계산하는 rectangles라는 새로운 바이너리 프로젝트를 만들어 보겠습니다. Listing 5-8 은 프로젝트의 src/main.rs에서 정확히 그렇게 하는 한 가지 방법을 보여주는 짧은 프로그램입니다.
파일 이름: src/main.rs
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
}
Listing 5-8: 별도의 너비 및 높이 변수로 지정된 직사각형의 면적 계산
이제 cargo run을 사용하여 이 프로그램을 실행합니다.
The area of the rectangle is 1500 square pixels.
이 코드는 각 치수를 사용하여 area 함수를 호출하여 직사각형의 면적을 계산하는 데 성공하지만, 이 코드를 더 명확하고 읽기 쉽게 만들기 위해 더 많은 작업을 수행할 수 있습니다.
이 코드의 문제는 area의 시그니처에서 분명하게 드러납니다.
fn area(width: u32, height: u32) -> u32 {
area 함수는 하나의 직사각형의 면적을 계산하도록 되어 있지만, 우리가 작성한 함수는 두 개의 매개변수를 가지며, 매개변수가 관련되어 있다는 것을 프로그램 어디에서도 명확하게 알 수 없습니다. 너비와 높이를 함께 그룹화하는 것이 더 읽기 쉽고 관리하기 쉬울 것입니다. "튜플 타입"에서 튜플을 사용하여 그렇게 할 수 있는 한 가지 방법을 이미 논의했습니다.
Listing 5-9 는 튜플을 사용하는 프로그램의 또 다른 버전을 보여줍니다.
파일 이름: src/main.rs
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
1 area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
2 dimensions.0 * dimensions.1
}
Listing 5-9: 튜플로 직사각형의 너비와 높이 지정
어떤 면에서는 이 프로그램이 더 좋습니다. 튜플을 사용하면 약간의 구조를 추가할 수 있으며, 이제 하나의 인수만 전달합니다 [1]. 그러나 다른 면에서는 이 버전이 덜 명확합니다. 튜플은 요소의 이름을 지정하지 않으므로 튜플의 부분에 인덱싱해야 [2] 하므로 계산이 덜 명확해집니다.
너비와 높이를 혼동하는 것은 면적 계산에는 문제가 되지 않지만, 화면에 직사각형을 그리려면 문제가 될 것입니다! width가 튜플 인덱스 0이고 height가 튜플 인덱스 1임을 기억해야 합니다. 다른 사람이 우리 코드를 사용한다면 이를 파악하고 기억하는 것이 훨씬 더 어려울 것입니다. 코드에서 데이터의 의미를 전달하지 않았기 때문에 이제 오류를 도입하기가 더 쉬워졌습니다.
데이터에 레이블을 지정하여 의미를 추가하기 위해 구조체를 사용합니다. Listing 5-10 과 같이 전체 이름과 부분 이름을 사용하여 사용 중인 튜플을 구조체로 변환할 수 있습니다.
파일 이름: src/main.rs
1 struct Rectangle {
2 width: u32,
height: u32,
}
fn main() {
3 let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
4 fn area(rectangle: &Rectangle) -> u32 {
5 rectangle.width * rectangle.height
}
Listing 5-10: Rectangle 구조체 정의
여기서 구조체를 정의하고 이름을 Rectangle로 지정했습니다 [1]. 중괄호 안에서 필드를 width와 height로 정의했으며, 둘 다 u32 타입입니다 [2]. 그런 다음 main에서 너비가 30이고 높이가 50인 Rectangle의 특정 인스턴스를 생성했습니다 [3].
이제 area 함수는 하나의 매개변수로 정의되었으며, 이를 rectangle이라고 명명했으며, 해당 타입은 Rectangle 인스턴스의 불변 차용 (immutable borrow) 입니다 [4]. 4 장에서 언급했듯이, 소유권을 가져가는 대신 구조체를 차용하려고 합니다. 이렇게 하면 main이 소유권을 유지하고 rect1을 계속 사용할 수 있으며, 이것이 함수 시그니처에서 &를 사용하고 함수를 호출하는 이유입니다.
area 함수는 Rectangle 인스턴스의 width 및 height 필드에 접근합니다 [5] (차용된 구조체 인스턴스의 필드에 접근해도 필드 값이 이동하지 않음에 유의하세요. 이것이 구조체의 차용을 자주 볼 수 있는 이유입니다). 이제 area에 대한 함수 시그니처는 우리가 의미하는 바를 정확히 말합니다. 즉, Rectangle의 width 및 height 필드를 사용하여 면적을 계산합니다. 이는 너비와 높이가 서로 관련되어 있음을 전달하고, 튜플 인덱스 값 0과 1을 사용하는 대신 값에 대한 설명적인 이름을 제공합니다. 이는 명확성을 위한 승리입니다.
프로그램을 디버깅하는 동안 Rectangle의 인스턴스를 출력하고 모든 필드의 값을 볼 수 있으면 유용할 것입니다. Listing 5-11 은 이전 장에서 사용했던 것처럼 println! 매크로를 사용하려고 시도합니다. 그러나 이것은 작동하지 않습니다.
파일 이름: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
Listing 5-11: Rectangle 인스턴스 출력을 시도
이 코드를 컴파일하면 다음과 같은 핵심 메시지가 포함된 오류가 발생합니다.
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println! 매크로는 다양한 종류의 형식을 지정할 수 있으며, 기본적으로 중괄호는 println!에게 Display라고 하는 형식을 사용하도록 지시합니다. 이는 최종 사용자에게 직접 소비하기 위한 출력입니다. 지금까지 살펴본 기본 타입은 기본적으로 Display를 구현합니다. 1 또는 다른 기본 타입을 사용자에게 표시하는 방법이 하나뿐이기 때문입니다. 그러나 구조체의 경우 println!이 출력을 형식화하는 방법은 더 명확하지 않습니다. 표시 가능성이 더 많기 때문입니다. 쉼표를 원하십니까? 중괄호를 인쇄하시겠습니까? 모든 필드를 표시해야 합니까? 이러한 모호성 때문에 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
해봅시다! println! 매크로 호출은 이제 println!("rect1 is {:?}", rect1);처럼 보일 것입니다. 중괄호 안에 :? 지정자를 넣으면 println!에게 Debug라는 출력 형식을 사용하도록 지시합니다. Debug 트레이트를 사용하면 코드를 디버깅하는 동안 값을 볼 수 있도록 개발자에게 유용한 방식으로 구조체를 출력할 수 있습니다.
이 변경 사항으로 코드를 컴파일합니다. 젠장! 여전히 오류가 발생합니다.
error[E0277]: `Rectangle` doesn't implement `Debug`
하지만 다시 컴파일러는 유용한 메모를 제공합니다.
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `Debug`
Rust 는 디버깅 정보를 출력하는 기능을 포함하지만, 구조체에 해당 기능을 사용할 수 있도록 명시적으로 옵트인해야 합니다. 이를 위해 Listing 5-12 와 같이 구조체 정의 바로 앞에 외부 속성 #[derive(Debug)]를 추가합니다.
파일 이름: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
Listing 5-12: Debug 트레이트를 파생하고 디버그 형식을 사용하여 Rectangle 인스턴스를 출력하는 속성 추가
이제 프로그램을 실행하면 오류가 발생하지 않으며 다음과 같은 출력을 볼 수 있습니다.
rect1 is Rectangle { width: 30, height: 50 }
좋아요! 가장 예쁜 출력은 아니지만, 이 인스턴스의 모든 필드의 값을 표시하므로 디버깅 중에 확실히 도움이 될 것입니다. 더 큰 구조체가 있는 경우, 출력을 조금 더 쉽게 읽을 수 있는 것이 유용합니다. 이러한 경우 println! 문자열에서 {:?} 대신 {:#?}를 사용할 수 있습니다. 이 예에서 {:#?} 스타일을 사용하면 다음과 같이 출력됩니다.
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug 형식을 사용하여 값을 출력하는 또 다른 방법은 dbg! 매크로를 사용하는 것입니다. 이 매크로는 표현식의 소유권을 가져와 (참조를 사용하는 println!과 반대) 코드에서 해당 dbg! 매크로 호출이 발생하는 파일 및 줄 번호와 해당 표현식의 결과 값을 출력하고 값의 소유권을 반환합니다.
참고:
dbg!매크로를 호출하면 표준 오류 콘솔 스트림 (stderr) 에 출력됩니다.println!은 표준 출력 콘솔 스트림 (stdout) 에 출력됩니다. "표준 출력 대신 표준 오류에 오류 메시지 쓰기"에서stderr및stdout에 대해 자세히 설명합니다.
다음은 width 필드에 할당되는 값과 rect1의 전체 구조체 값에 관심이 있는 예입니다.
파일 이름: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
1 width: dbg!(30 * scale),
height: 50,
};
2 dbg!(&rect1);
}
dbg!를 표현식 30 * scale [1] 주위에 넣을 수 있으며, dbg!는 표현식 값의 소유권을 반환하므로 width 필드는 dbg! 호출이 없었을 때와 동일한 값을 얻게 됩니다. dbg!가 rect1의 소유권을 가져가도록 하고 싶지 않으므로 다음 호출에서 rect1에 대한 참조를 사용합니다 [2]. 이 예의 출력은 다음과 같습니다.
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
첫 번째 출력은 [1]에서 30 * scale 표현식을 디버깅하고 결과 값은 60입니다 (정수에 대해 구현된 Debug 형식은 값만 출력하는 것입니다). [2]의 dbg! 호출은 Rectangle 구조체인 &rect1의 값을 출력합니다. 이 출력은 Rectangle 타입의 예쁜 Debug 형식을 사용합니다. dbg! 매크로는 코드가 무엇을 하고 있는지 파악하려는 경우 정말 도움이 될 수 있습니다!
Debug 트레이트 외에도 Rust 는 사용자 정의 타입에 유용한 동작을 추가할 수 있는 derive 속성과 함께 사용할 수 있는 여러 트레이트를 제공했습니다. 해당 트레이트와 해당 동작은 부록 C 에 나열되어 있습니다. 이러한 트레이트를 사용자 지정 동작으로 구현하는 방법과 고유한 트레이트를 만드는 방법을 10 장에서 다룹니다. derive 외에도 다른 많은 속성이 있습니다. 자세한 내용은 *https://doc.rust-lang.org/reference/attributes.html*의 Rust 참조의 "속성" 섹션을 참조하십시오.
area 함수는 매우 구체적입니다. 직사각형의 면적만 계산합니다. 이 동작을 다른 타입에서는 작동하지 않으므로 Rectangle 구조체에 더 가깝게 연결하는 것이 도움이 될 것입니다. area 함수를 Rectangle 타입에 정의된 area 메서드로 변환하여 이 코드를 계속 리팩토링하는 방법을 살펴보겠습니다.
축하합니다! 구조체를 사용하는 예제 프로그램 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.