JS, TS랑 비교하며 Rust 공부하기 (3)
구조체
구조체는 여러 값을 묶고 이름을 지어서 의미 있는 묶음을 정의하는 데에 사용한다. (JS 객체와 비슷)
struct
키워드로 정의하며 힙 메모리에 저장된다.
구조체 내 특정 값은 점 표기법으로 가져올 수 있다.
컴파일러가 메모리 정렬을 최적화해주기 때문에 필드 순서는 신경쓰지 않아도 된다.
인스턴스를 생성할 때 필드의 순서는 정의했을 때와 동일하지 않아도 된다.
일부 필드만 가변 혹은 불변으로 만들 수 없고 해당 인스턴스 전체에 지정해줘야 한다.
참조자를 이용해 구조체가 소유권을 갖지 않는 데이터도 저장할 수 있지만, 라이프타임을 활용해야 한다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
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");
}
변수명과 구조체 필드명이 같을 땐 필드 초기화 축약법을 사용해서 더 적은 타이핑으로 기능을 구현할 수 있다.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username, // 필드 초기화 축약법
email: email,
sign_in_count: 1,
}
}
구조체 업데이트 문법
기존의 인스턴스로 새로운 인스턴스를 생성하려면 구조체 업데이트 문법을 사용하면 된다.
..
문법은 따로 명시된 필드를 제외한 나머지 필드를 주어진 인스턴스의 필드 값으로 설정하며, 제일 끝에 적어야 한다.
만약 이동하는 데이터 중에 힙 데이터가 포함되어 있다면, 기존의 인스턴스는 사용할 수 없다.
fn main() {
// --생략--
let user2 = User {
email: String::from("another@example.com"),
..user1 // user1의 username 필드가 user2로 이동했기 때문에 user1은 더 이상 사용불가
};
}
튜플 구조체
튜플 구조체는 구조체 자체에는 이름을 지어 의미를 주지만 이를 구성하는 필드에는 이름을 붙이지 않고 타입만 적어 넣은 형태이다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
유사 유닛 구조체
유사 유닛 구조체는 어떤 타입에 대해 트레이트를 구현하고 싶지만 타입 내부에 어떤 데이터를 저장할 필요는 없을 경우 유용하다.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
구조체의 자리표시자
구조체에는 자리표시자와 함께 사용하기 위한 Display
구현체가 기본 제공되지 않는다.
구조체 정의 바로 이전에 외부 속성(#[derive(Debug)]
)을 작성해주고 {:?}
를 사용하면 출력할 수 있다.
{:?}
대신 {:#?}
를 사용하면 더 읽기 편한 형태로 출력된다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
dbg!
매크로는 표현식의 소유권을 가져와서, (참조자를 사용하는 println!
과는 다르다)
코드에서 dbg!
매크로를 호출한 파일 및 라인 번호를 결과값과 함께 출력하고 다시 소유권을 반환한다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
메서드
메서드는 함수와 달리 구조체와 관련된 동작을 정의할 때 사용하며, 첫 번째 매개변수로 항상 self
를 사용해야 한다.
Self
타입의 self
라는 이름의 매개변수를 첫 번째 매개변수로 사용해야 self
형태의 축약형을 사용할 수 있다. (&self
는 실제로는 self: &Self
를 줄인 것이다.)
impl
블록 내에서 Self
는 impl
블록의 대상이 되는 타입의 별칭이다.
일반적으로 &self
를 통해 데이터를 읽어오고 &mut self
를 통해 데이터를 변경하면된다. self
만을 쓰면 소유권을 가져오기 때문에 원본 인스턴스를 다른 무언가로 변환하고 그 이후에는 원본 인스턴스의 사용을 막고자 할 때 사용한다.
struct Rectangle {
width: u32,
height: u32,
}
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
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}
연관 함수
impl
블록 내에 구현된 모든 함수를 연관 함수라고 부른다.
동작하는 데 해당 타입의 인스턴스가 필요하지 않다면, self
를 첫 매개변수로 갖지 않는 메서드가 아닌 연관 함수를 정의할 수 있다.
메서드가 아닌 연관 함수는 구조체의 새 인스턴스를 반환하는 생성자로 자주 활용된다.
연관 함수를 호출할 땐 구조체 이름에 ::
구문을 붙여서 호출한다.
JS의 정적 메서드라고 생각하면 된다.
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
열거형
열거형은 미리 정의된 여러 값 중 하나일 수 있다는 걸 표현한 타입이다.
각 값을 배리언트라고 부르며, 열거형의 값은 이 배리언트들 중 오직 하나만 가질 수 있다.
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
열거형은 각 배리언트에 서로 다른 타입이나 개수의 데이터를 직접 담을 수 있어, 같은 구조체로는 표현하기 어려운 다양한 경우를 간결하게 나타낼 수 있다.
배리언트의 내부 값은 match
혹은 if let
문법을 통해서만 접근할 수 있다.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
let quit_msg = Message::Quit;
let move_msg = Message::Move { x: 10, y: 20 };
let write_msg = Message::Write(String::from("write"));
let change_color_msg = Message::ChangeColor(1, 2, 3);
let text = if let Message::Write(t) = &write_msg {
t
} else {
String::new()
};
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
열거형에도 impl
을 사용해서 메서드를 정의할 수 있다.
impl Message {
fn call(&self) {
println!("{:?}", &self);
match &self {
Message::Write(text) => {
println!("Message::Write contains: {}", text);
}
_ => {
println!("This variant is not Message::Write");
}
}
}
}
let m = Message::Write(String::from("hello"));
m.call();
Option
열거형
러스트에는 null
이 없지만, 값의 존재 혹은 부재의 개념을 표현할 수 있는 Option
열거형이 있다.
Some
값을 얻게 되면, 값이 존재한다는 것과 해당 값이 Some
내에 있다는 것을 알 수 있다.
None
값을 얻게 되면, 얻은 값이 유효하지 않다는, 어떤 면에서는 null
과 같은 의미를 갖는다.
값의 타입이 Option<T>
가 아닌 모든 곳은 값이 null
이 아니라고 안전하게 가정할 수 있다.
enum Option<T> {
None,
Some(T),
}
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None; // None 값만으로는 알 수 없어서 타입을 명시
컴파일러는 Option<T>
에서 값을 꺼내 사용하기 전에, Some
인지 확인하는 코드를 작성하도록 강제한다. 그렇지 않으면 컴파일 에러가 발생한다.
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // error
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
부족한 부분이 생기면 추가할 예정이다.