[0x03] Using Structs to Structure Related Data

30101 단어 RustRust
  • struct 키워드를 이용해서 구조체를 만들 수 있다.

1. Defining and Instantiating Structs

1.1 Structure

  • tuple 과 다르게 각 필드에 대해 이름을 붙일 수 있다.
struct User {
  active: bool,
  username: String,
  email: String,
  sign_in_count: u64,
}
  • 구조체를 만든 다음에는 key : value 구조로 instance 를 만들어서 사용할 수 있다.
  • 인스턴스를 만들 때, 필드의 순서를 바꿔서 적을 수도 있다.
fn main() {
  let user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someone"),
    active: true,
    sign_in_count: 1,
  };
}
  • 구조체에서 . 을 이용해 특정 값을 얻어올 수 있다.
  • 인스턴스가 mutable 하게 선언되었다면, 값을 변경하는 것도 가능하다.
fn main() {
  let mut user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
  };
  
  user1.email = String::from("[email protected]");
}
  • 함수를 이용해서 변경하는 것도 가능하다.
fn build_user(email:String, username:String) -> User {
  User {
    email: email,
    username: username,
    active: true,
    sign_in_count: 1,
  }
}

1.1.1 Using the Field Init Shorthand

  • Rust 에서는 필드와 함수 인자의 이름이 같을 경우, 생략할 수 있다.
fn build_user(email: String, username: String) -> User {
  User {
    email,
    username,
    active: true,
    sign_in_count: 1,
  }
}

1.1.2 Creating a new Instance w/ other Instance

  • 또, 다른 struct 로 부터의 값을 이용해서 새로운 인스턴스를 만들 수 있다.
fn main() {
  // --skip--
  
  let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("[email protected]"),
    sign_in_count: user1.sign_in_count,
  };
}
  • Rust 에서는 이것도 줄일 수 있는 방법으로 .. 문법을 제공한다.
fn main() {
  // --skip--
  
  let user2 = User {
    email: String::from("[email protected]"),
    ..user1
  };
}
  • 한 가지 주목할 점은 user2 인스턴스를 생성하며, = 연산자를 사용한 점이다.
  • 이는 Ownership 에 대해 설명할 때 살펴본 move를 했기 때문이다.
  • 따라서, user1user2를 생성한 후 소유권을 잃고 사용할 수 없게 된다.
  • 구체적으로 말하면, 힙 영역을 사용하는 데이터들(email, username)이 move 되어버렸기 때문이다.
  • 따라서, user2username도 따로 선언해준다면 copy 만 일어나게 되고, 결과적으로user1도 문제 없이 사용할 수 있다.

1.2 Tuple Structure

  • tuple structure 에서는 각 필드에 대한 이름이 없다.
  • tuple 에 이름을 붙일 때 유용하게 사용할 수 있다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
  let black = Color(0, 0, 0);
  let origin = Point(0, 0, 0);
}
  • blackorigin 은 각각 서로 다른 tuple structure의 인스턴스이므로 서로 다른 타입이다.
  • 즉, 필드가 같은 형태로 구성되어 있다고 하더라도 이름이 다르면 서로 다른 타입이다.
  • tuple structuretuple과 마찬가지로 아래 두 가지가 가능하다.
    • destructuring
    • . 연산자로 값 참조

1.3 Unit-Like structs

  • 필드가 아예 없는 structure를 만들 수도 있다.
  • 이들은 unit-like structs라 불리고, ()와 비슷하게 작동한다.
  • unit-like structstrait을 구현할 때 특히 유용하다.
    • 특정 type 이 타입 자체에 저장하려는 데이터가 없을 때, ()를 사용할 수 있다.
struct AlwaysEqual;

fn main() {
  let subject = AlwaysEqual;
}

Ownership Of Struct Data

  • 위에서 살펴본 User 구조체에서 &str 대신에 String 타입을 사용한 것은 Rustlifetime 개념에 대한 이해가 필요하다.

2. Structs Example

  • 직사각형의 넓이를 구하는 코드를 작성하며 struct에 대한 이해를 해보자.

[1]

  • struct 없이, 단순한 코드를 작성한다.
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
}

[2] +Tuple

  • tuple 을 사용하여 코드를 작성한다.
fn main() {
  let rect1 = (30, 50);
  
  println!(
    "The area of the rectangle is {} square pixels.",
    area(rect1)
  );
}

fn area(dimensions: (u32, u32)) -> u32 {
  dimensions.0 * dimensions.1
}

[3] +Structs

  • structs 를 사용하여 보다 의미 있는 코드를 작성한다.
struct Rectangle {
  width: u32,
  height: u32,
}

fn main() {
  let rect1 = Rectangle {
    width: 30,
    height: 50,
  }
  
  println!(
    "The area of the rectangle is {} square pixels.",
    area(&rect1)
  );
}

fn area(rectangle: &Rectangle) -> u32 {  // one parameter!
  rectangle.width * rectangle.height
}

[4] +Debugging

  • 코딩 중에 디버깅을 하는 것이 필요한 순간이 있다.
  • 이 때, println! 을 통해 데이터 값을 체크 할 수 있는데, 이 때 Display라는 format 을 사용한다.
  • td::fmt::Display 이다.
  • struct 의 경우에는 출력하는 포맷의 모호함 때문에 따로 Display 의 구현을 제공하지 않는다.
  • struct 의 내용을 출력하고 싶으면 {} 대신에 {:?} 또는 {:#?} (for pretty-print) 를 쓰면 된다.
  • 또, structDebug 라는 trait 이 구현되어 있지 않으면 #[derive(Debug)] 를 적어주어야 한다.
#[derive(Debug)]  // implements Debug trait
struct Rectangle {  
  width: u32,       
  height: u32,      
}

fn main() {
  let a = Rectangle{
    width: 30,      
    height: 20,     
  };

  println!("The info of area: {:?}", a); // formatting for structs     
  println!("The size of area is {}", area(a));
}

fn area(rectangle: Rectangle) -> u32 {        
  rectangle.width * rectangle.height
}
  • 또 다른 방법은 dbg! 매크로를 사용하는 것이다.
  • dbg! 를 사용하면 코드 라인까지 출력해준다.

[5] misc

  • Rust 에서는 Debug 외에도 여러가지 trait 을 제공한다.
  • 이러한 trait 들은 structs와 같은 custom type 을 만들 때 많이 유용하다.

3. Method Syntax

  • method 는 함수와 비슷하지만, structs 의 컨텍스트 내부에서 정의된다.
  • 또한, 첫 번째 인자로 self 를 받는다.
  • self 는 해당 메소드를 호출하는 structsinstance를 가리킨다.

3.1 Defining Method

  • method 의 정의는 아래 코드를 참고하자.
#[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!(
    "The area of the rectangle is {} square pixels.",
    rect1.area()
  );
}
  • 여기서 &self 는 사실 self:&Self 의 축약이다.
  • Selfimpl 블록의 타입을 의미한다.
  • 일반적으로 method 들은 self 의 소유권을 가져오는 경우가 드물기 때문에, 대부분 &self를 사용한다.
  • method 를 사용하는 가장 큰 이유는 특정 타입에 대한 함수들을 한 곳에 모아두기 위함이다.
  • 다른 언어들에서는 getter 라 불리는 method를 많이 구현해준다.
  • 이는, 필드 값을 read-only 로 바로 참조할 수 있도록 해주는 method 이다.
  • 하지만, Rust 에서는 이를 자동으로 구현해주지 않는다.

3.2 Methods w/ more parameters

  • 예제 코드는 아래와 같다.
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
  }
}

3.3 Associated Functions

  • 모든 methodAssociated Function 이라고 부를 수 있다.
  • self 를 첫 번째 인자로 갖지 않는, association function을 구현하는 것이 가능하다.
  • 사실 String 에서도 String::from() 에는 self가 없다.
  • 보통 이런 함수는 constructor 에 쓸 수 있다.
impl Rectangle {
  fn square(size: u32) -> Rectangle {
    Rectangle {
      width: size,
      height: size,
    }
  }
}
  • 이러한 association function:: 연산자로 호출할 수 있다.
  • 예를 들어, let sq = Rectangle::square(3) 이런 식이다.

3.4 Multiple impl Blocks

  • 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
  }
}

좋은 웹페이지 즐겨찾기