JS, TS랑 비교하며 Rust 공부하기 (3)

구조체

구조체는 여러 값을 묶고 이름을 지어서 의미 있는 묶음을 정의하는 데에 사용한다. (JS 객체와 비슷)
일부 필드만 가변 혹은 불변으로 만들 수 없고 해당 인스턴스 전체에 지정해줘야 한다.
참조자를 이용해 구조체가 소유권을 갖지 않는 데이터도 저장할 수 있지만, 라이프타임을 활용해야 한다.

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);
}

메서드

메서드는 함수와 달리 구조체 컨텍스트에 정의되고, 첫 번째 매개변수가 항상 self이다.
impl 블록 내에서 Self는 impl 블록의 대상이 되는 타입의 별칭이다.
메서드는 Self 타입의 self라는 이름의 매개변수를 첫 번째 매개변수로 가져야 하는데, 그렇게 해야 첫 번째 매개변수 자리에 적어 넣은 self 형태의 축약형을 사용할 수 있다. (&self는 실제로는 self: &Self를 줄인 것이다.)
메서드는 다른 매개변수가 그런 것처럼 self의 소유권을 가져올 수도, self를 불변으로 빌려올 수도, 가변으로 빌려올 수도 있다.

#[derive(Debug)]
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 블록 내에 구현된 모든 함수를 연관 함수라고 부르는데, 이는 impl 뒤에 나오는 타입과 모두 연관된 함수이기 때문이다.
동작하는 데 해당 타입의 인스턴스가 필요하지 않다면, self를 첫 매개변수로 갖지 않는 (따라서 메서드가 아닌) 연관 함수를 정의할 수도 있다.
메서드가 아닌 연관 함수는 구조체의 새 인스턴스를 반환하는 생성자로 자주 활용된다.
연관 함수를 호출할 땐 let sq = Rectangle::square(3);처럼 구조체 명에 :: 구문을 붙여서 호출한다.
JS의 정적 메서드라고 생각하면 된다.

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

여러 개의 impl 블록

각 구조체는 여러 개의 impl블록을 가질 수 있다.


열거형

열거형은 어떤 값이 여러 개의 가능한 값의 집합 중 하나라는 것을 나타내는 방법을 제공한다.
열거형의 값은 여러 배리언트 중 하나만 될 수 있다.

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

배리언트에 데이터를 직접 넣는 방식을 사용해서 열거형을 구조체의 일부로 사용하는 방식보다 더 간결하게 동일한 개념을 표현할 수 있다.
배리언트의 이름이 해당 열거형 인스턴스의 생성자 함수처럼 된다.
배리언트는 다른 타입과 다른 양의 연관된 데이터를 가질 수 있으며, 구조체로는 이렇게 할 수 없다.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

열거형에도 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과 같은 의미를 갖는다.

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> 값을 명백하게 유효한 값처럼 사용하지 못하도록 하기 때문에, null보다는 낫다.
값의 타입이 Option<T>가 아닌 모든 곳은 값이 null이 아니라고 안전하게 가정할 수 있다.

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y; // error

match 표현식과 열거형의 조합

match와 열거형을 조합하는 것은 다양한 경우에 유용하다.

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,
    }
}
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);

부족한 부분이 생기면 추가할 예정이다.