![Rust 걸음마 떼기 (5) - Rust의 구조체](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FByifw%2FbtsAD4N4syO%2FTr8x6oR59hZsg1DC6bEqFK%2Fimg.png)
2023.11.21 - [백엔드/Rust] - Rust 걸음마 떼기 (1) - Rust 설치 및 실행
2023.11.21 - [백엔드/Rust] - Rust 걸음마 떼기 (2) - 변수 선언, 입력, 비교
2023.11.21 - [백엔드/Rust] - Rust 걸음마 떼기 (3) - 일반 프로그래밍 개념을 rust에서는 어떻게 다루는가
2023.11.21 - [분류 전체보기] - Rust 걸음마 떼기 (4) - 소유권 (러스트의 메모리 관리)
튜플과 구조체의 차이점을 살펴보고, 구조체의 데이터와 관련된 동작을 정의하는 메서드와 연관 함수를 알아본다.
구조체 정의와 인스턴스 생성
러스트는 메모리를 컴파일러가 컴파일 시점에서 다양한 규칙으로 이루어진 소유권 시스템으로 관리한다.
소유권과 관련된 기능들은 실행 성능에 아무런 영향을 끼치지 않는다.
(1) 구조체 정의
struct User {
username : String,
email: String,
sign_in_count : u64,
active: bool
}
구조체 선언 방법은 위와 같다.
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someoneusername123"),
active: true,
sign_in_count: 1,
};
구조체 인스턴스 생성은 위와같이한다.
구조체에서 원하는 값을 읽으려면 . 을 찍으면 된다.
그리고 필드값을 바꾸려면 아래와 같이 한다.
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someoneusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
주의할 점은 구조체의 인스턴스 자체가 가변이라는 점이다.
러스트는 구조체의 몇몇 필드만을 가변데이터로 표시하는것을 지원하지않는다.
구조체의 새로운 인스턴스도 함수를 이용해 생성할 수 있다.
이때 함수의 마지막 표현식은 묵시적으로 새 인스턴스를 리턴해야한다.
다음은 함수의 예시이다.
fn build_user(email: String, username :String) -> User {
User {
email: String::from("someone@example.com"),
username: String::from("someoneusername123"),
active: true,
sign_in_count: 1,
}
}
(1) 같은 이름의 필드와 변수를 편리하게 활용하기
아래의 예제를 보면 함수의 매개변수 이름과 구조체의 필드 이름이 같다!
필드 초기화 단축 문법을 이용해 email이나 username등 변수이름을 일일이 반복해서 입력하지 않아도 된다.
fn build_user(email: String, username :String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
email과 username은 매개변수로 전달된것을 그대로 활용한다는 의미이다.
(2) 기존의 인스턴스로 부터 새 인스턴스 생성하기
이미 존재하는 인스턴스에서 몇가지 필드만 수정한 새 구조체 인스턴스가 필요할 때가 있다.
그럴때는 구조체 갱신문법을 이용하면 된다.
아래는 user1 인스턴스의 일부를 이용해 User 구조체의 새 인스턴스를 생성하는것이다.
let user2 = User {
email: String::from("someone@example.com"),
username: String::from("someoneusername123"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
아래는 더 쉽게 구조체 갱신 문법을 이용해 email과 username필드에 새로운 값을 대입하고 나머지필드에는 user1 변수에 저장된 값을 대입하는 새 인스턴스 생성 코드다.
let user2 = User {
email: String::from("someone@example.com"),
username: String::from("someoneusername123"),
..user1
};
(3) 이름 없는 필드를 가진 튜플 구조체로 다른 타입 생성하기
튜플과 유사하게 생긴 구조체 선언이 가능하다.
이런 구조체를 튜플 구조체라한다.
튜플 구조체는 일반 구조체처럼 필드에 이름 부여하는게 귀찮거나, 불필요하고 튜플 자체에만 이름부여해서 쓰고싶을때 유용하다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
중요한 점은 black과 origin이 서로 다른 타입이라는 점이다.
(4) 필드가 없는 유사 유닛 구조체
러스트에서는 심지어 필드가 하나도 없는 구조체를 선언 할 수 있다.
유사 유닛 구조체라고 한다.
유사유닛 구조체는 어떤 타입의 트레이트를 구현해야하지만, 타입에 저장할 데이터가 없을때 유용하다.
트레이트는 10장에서 나오니 일단 스킵!
구조체를 사용하는 예제 프로그램
사각형의 면적을 구하는 프로그램
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"사각형의 면적: {} 제곱 픽셀", area(width1,height1)
);
}
fn area(width: u32, height:u32) ->u32 {
width * height
}
(1) 튜플을 이용한 리팩토링
fn main() {
let rect1 = (30,50);
println!(
"사각형의 면적: {} 제곱 픽셀", area(rect)
);
}
fn area(dimensions: (u32,u32)) ->u32 {
dimensions.0 * dimensions.1
}
좀 더 나아지긴 했으나 튜플의 요소에 인덱스로 접근해야한다는 점에서 의미가 명확하지 않을 수 있다.
(2) 구조체를 이용한 리팩토링
데이터에 이름을 부여해서 의미를 반영하기 위해 구조체를 사용한다.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {width:30, height:50};
println!(
"사각형의 면적: {} 제곱 픽셀", area(&rect1)
);
}
fn area(rectangle: &Rectangle) ->u32 {
rectangle.width * rectangle.height
}
area 함수는 이제 rectangle이라는 하나의 매개변수만을 사용하고,
매개변수의 타입은 Rectangle 구조체의 불변 인스턴스에 대한 대여(borrow)다.
이러면 main함수는 rectangle을 여전히 소유권을 가지고 계속 쓸 수 있다.
(3) 트레이트를 상속해서 유용한 기능 추가하기
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {width:30, height:50};
println!("rect1: {}",rect1);
}
println! 매크로는 다양한 형식으로 문자열을 출력 할 수 있으며, 중괄호는 기본적으로 println! 매크로가 Display 형식을 출력하도록 한다.
위는 다음과 같은 에러를 발생시킨다.
'Rectangle' cannot be formatted with the default formatter
= note: in format strings you may be able to use '{:?}' (or {:#?} for pretty-print) instead
위에서 하라는 대로 {}안에 :?를 넣어도 다른 에러가 난다 그 이유는 아래와 같다.
러스트는 디버깅 정보를 출력하는 기능을 제공하지만 구조체는 이 기능을 명시적으로 구현해야한다.
그러려면 #[derive(Debug)] 애노테이션을 구조체 정의에 추가해야한다.
아래는 Rectangle 구조체가 디버깅 정보를 출력할 수 있도록 Debug 트레이트를 상속하는 애노테이션 추가한 예시이다
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main(){
let rect1 = Rectangle {width:30, height:50};
println!("rect1: {:?}",rect1);
}
rect1: Rectangle { width: 30, height: 50 }
:? 대신 :#?을 이용하면 아래와 같이 출력한다.
rect1: Rectangle {
width: 30,
height: 50
}
메서드 문법
매서드는 함수와 유사하다.
함수와 마찬가지루 fn 키워드를 이용해 정의하며, 이름 매개변수, 리턴타입을 정의 할 수 있다.
하지만 메서드는 함수와 달리 구조체의 컨텍스트 안에 정의하며, 첫번째 변수는 항상 메서드를 호출할 구조체의 인스턴스를 표현하는 self여야한다.
(1) 메서드 정의하기
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main(){
let rect1 = Rectangle {width:30, height:50};
println!(
"사각형의 면적: {} 제곱 픽셀", rect1.area()
);
}
Rectangle 타입의 컨텍스트 안에 함수를 정의하려면 Impl 블록을 이용한다.
area 메서드를 보면 첫번째 매개변수가 &self다.
Rectangle 컨텍스트 안에 존재하므로 self는 Rectangle 타입인걸 알 고 있다.
(2) 더 많은 매개변수를 갖는 메서드
fn main(){
let rect1 = Rectangle {width:30, height:50};
let rect2 = Rectangle {width:10, height:50};
let rect3 = Rectangle {width:60, height:45};
println!("rect1은 rect2를 포함하는가? {}", rect1.can_hold(&rect2)) //rect1은 rect2의 값을 "전달"해서 "읽기"만한다.
println!("rect1은 rect3를 포함하는가? {}", rect1.can_hold(&rect3))
}
impl Rectangle{
fn area(&self) -> u32{
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
여러개의 매개변수를 사용하려면 self 매개변수 이후에 원하는 만큼의 매개변수를 추가하면 된다.
(3) 연관 함수
impl 블록의 또 다른 유용한 기능은 self 매개변수를 사용하지 않는 다른 함수도 정의 할 수 있다는 점이다.
지금까지 수차례 사용했던 String::from 함수가 연관함수의 좋은 예이다.
(self를 받지않아서 함수라고 칭하나 싶다)
연관함수는 새로운 인스턴스를 리턴하는 생성자를 구현할때 자주 사용한다.
이런 경우를 방지하기 위해서 러스트는 하나의 디테일이 더 있는데 예제로 확인할 수 있다.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height:size }
}
}
연관함수 호출을 하려면 구조체의 이름과 함께 :: 문법을 쓰면된다.
let sq = Rectangle::square(3); 과 같이 사용가능하다.
(4) 여러개의 impl 블록
impl Rectangle{
fn area(&self) -> u32{
self.width * self.height
}
}
impl Rectangle{
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
|
각 구조체는 여러개의 impl 블록을 선언할 수 있다.
굳이 여러개의 impl블록을 선언할 이유는 없지만, 문법적으로는 아무런 문제가 없다.
'백엔드 > Rust' 카테고리의 다른 글
Rust with Flutter Tutorial (4) | 2023.11.21 |
---|---|
Rust 걸음마 떼기 (4) - 소유권 (러스트의 메모리 관리) (2) | 2023.11.21 |
Rust 걸음마 떼기 (3) - 일반 프로그래밍 개념을 rust에서는 어떻게 다루는가 (1) | 2023.11.21 |
Rust 걸음마 떼기 (2) - 변수 선언, 입력, 비교 (2) | 2023.11.21 |
Rust 걸음마 떼기 (1) - Rust 설치 및 실행 (0) | 2023.11.21 |
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.