#7 구조체와 메서드

Pt J·2020년 8월 17일
0

[完] Rust Programming

목록 보기
9/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

구조체는 서로 다른 자료형의 자료들을 한 데 묶어서 만들 수 있는 자료형이다.
그런 측면에서 이것은 튜플하고도 닮아 있다.
하지만 몇 번째 자료인지를 통해 접근하는 튜플과 달리 구조체는
각각의 내부 자료에 이름이 있어 그 순서에 의존하지 않고 접근할 수 있다.

구조체 Struct

구조체를 어떻게 사용할 수 있는지 알아보도록 하자.

구조체 정의

구조체는 struct 키워드를 통해 정의할 수 있다.
구조체의 내부 자료들은 필드Field라고 하며, 이름: 자료형 형태로 정의된다.
구조체 이름은 변수나 함수와 달리 대문자로 시작하며
여러 단어로 이루어져 있을 경우 각 단어의 시작을 대문자로 작성함으로써 단어를 구분한다.
// 이러한 표기법을 Camel Case라고 한다는 건 여담.
이름은 짓기 나름이지만 그룹화된 자료들을 잘 표현할 수 있는 이름으로 짓는 게 좋다.

예를 들어, 사용자 계정 정보를 저장하는 구조체를 다음과 같이 정의할 수 있다.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

인스턴스 생성

정의한 구조체를 사용하려면 구조체 인스턴스를 생성해야 한다.
인스턴스를 생성할 땐 구조체 이름과 중괄호를 사용하며
중괄호 안에 필드에 대입할 값을 필드이름: 값 형태로 전달한다.
각각의 필드는 이름을 통해 구분되므로 정의한 순서와 같을 필요는 없다.
생성한 구조체에서 원하는 값을 읽으려면 인스턴스이름:필드이름으로 접근할 수 있는데
가변성을 띈 인스턴스의 경우 같은 방법으로 값을 대입할 수 있다.
인스턴스도 변수와 마찬가지로 기본적으로 불변성을 띄며 mut 키워드로 가변성을 부여한다.

앞서 정의한 User 구조체의 가변 인스턴스를 생성하여 값을 변경하고자 한다면
다음과 같이 적성할 수 있다.

let mut user_pt = User {
    email: String::from("edenjint3927@konkuk.ac.kr"),
    username: String::from("peter"),
    active: true,
    sign_in_count: 1,
};

user_pt.email = String::from("peter.j@kakao.com");

구조체 인스턴스의 가변성은 필드가 아닌 인스턴스 단위로 적용된다는 것을 잊지 말자.

구조체를 반환하는 함수를 통해 구조체 인스턴스를 생성할 수도 있다.
필드 중 일부는 생성되는 시점에서 항상 동일하다면 그것은 고정해놓고
나머지 부분만 인자로 받아 전달해주는 방식에 유용하다.

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_value: 1,
    }
}

이 때, 인자로 전달된 변수와 그것을 대입할 필드의 이름이 같다면
다음과 같이 축약하여 작성하는 것을 허용한다.

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_value: 1,
    }
}

또한, 이미 생성되어 있는 구조체 인스턴스가 있을 때
그것을 이용하여 새로운 인스턴스를 생성할 수도 있다.
필드이름: 값을 작성할 때 필드이름: 기존인스턴스이름.필드이름을 사용하는 방식이다.
그런데 기존의 인스턴스와 겹치는 부분이 많다면
겹치지 않는 부분을 작성하고 나머지를 일괄적으로 기존의 인스턴스의 값을 사용할 수 있다.
..기존인스턴스이름을 통해 일괄 대입이 가능하다.

let user_jw = User {
    email: String::from("neont21@gmail.com");
    username: String::from("joowon");
    ..user_pt
};

튜플 구조체 Tuple Struct

튜플구조체는 튜플과 구조체 사이의 무언가라고 볼 수 있다.
구조체처럼 필드들을 그룹화하여 이름 붙일 수 있지만 튜플처럼 각 필드는 이름을 갖지 않는다.
그리고 각 필드에 접근할 때는 튜플에서와 같이 접근할 수 있다.
이것은 필드의 순서에 따른 의미가 있을 뿐 그것에 의미를 붙이는 게 불필요하거나
어떤 튜플을 하나의 자료형처럼 다른 튜플들과 구분하고자 할 때 사용할 수 있다.
튜플 구조체는 그 필드의 자료형이 완전히 일치하더라도 이름이 다르면 다른 자료형으로 인식된다.

예를 들어 다음과 같은 코드가 있다고 하면

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

ColorPoint는 서로 다른 자료형이며
이에 따라 blackorigin도 서로 다른 자료형의 인스턴스다.

유사 유닛 구조체 Unit-like Struct

Rust는 필드가 존재하지 않는 구조체도 만들 수 있는데 그런 녀석을 유사 유닛 구조체라고 한다.
필드가 존재하지 않는 구조체가 왜 필요한가 싶을 수 있는데
이것은 나중에 트레이트에 대해 다룰 때 알 수 있을 것이다.
...라고는 했지만 사실 트레이트보다는 그 이후 트레이트 객체를 다룰 때 보게 될 것이다.

구조체 사용하기

구조체 사용법을 익힐 겸, 실습을 통해
구조체가 어떤 상황에서 유용하게 쓰일 수 있는지 알아보자.
직사각형의 가로와 세로의 값을 받아 그 면적을 구하는 예제다.
먼저 일반 변수를 통해 구현해보고, 이를 튜플로 구현하면 어떤지 확인해본 후
다시 구조체로 구현해보는 순서로 진행하겠다.

peter@hp-laptop:~/rust-practice$ mkdir chapter05
peter@hp-laptop:~/rust-practice$ cd chapter05
peter@hp-laptop:~/rust-practice/chapter05$ cargo new rectangles
     Created binary (application) `rectangles` package
peter@hp-laptop:~/rust-practice/chapter05$ cd rectangles/
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ vi src/main.rs

src/main.rs

fn main() {
    let width = 30;
    let height = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width, height)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ cargo run
   Compiling rectangles v0.1.0 (/home/peter/rust-practice/chapter05/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ 

이렇게 구현해도 요구사항을 반하지는 않는다.
다만, widthheight는 같은 직사각형의 값이므로 연관된 변수인데
연관성이 드러나지 않는다.
만약 N개의 직사각형의 면적을 구하게 될 경우 실수로 잘못된 조합의 계산을 할지도 모른다.

그런 의미에서 우리는 튜플을 이용하여 가로와 세로를 연관지을 수 있다.

peter@hp-laptop:~/rust-practice/chapter05/rectangles$ vi src/main.rs

src/main.rs

fn main() {
    let rect = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ cargo run
   Compiling rectangles v0.1.0 (/home/peter/rust-practice/chapter05/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ 

아까와 마찬가지로 우리가 원하는 값을 얻었다.
그리고 이제 가로와 세로가 연관된 값임을 알 수 있고,
둘 중 하나가 소유권을 잃으면 나머지 하나도 같이 잃는 구조가 되었다.
그런데 막상 이렇게 바꾸고나니 무엇이 가로고 무엇이 세로인지 모르겠다.

그런데 우리에겐 변수를 그룹화하면서도 그들을 이름으로 식별할 수 있는 구조체가 있다!
이것을 사용하면 가로와 세로를 묶으면서도 그것들이 각각 무엇인지 알 수 있을 것이다.

peter@hp-laptop:~/rust-practice/chapter05/rectangles$ vi src/main.rs

src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ cargo run
   Compiling rectangles v0.1.0 (/home/peter/rust-practice/chapter05/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
peter@hp-laptop:~/rust-practice/chapter05/rectangles$ 

훨씬 가독성 높고 관리하기 수월한 코드가 되었다.

트레이트 상속

트레이트에 대한 건 나중에 다루겠지만 하나만 미리 알고 넘어가자.

우리는 어떤 변수의 값을 println!을 통해 출력해보곤 했다.
구조체의 값도 그렇게 출력할 수 있다면 좋겠지만 그러려면 각각의 필드를 개별적으로 출력해야 한다.
우리가 지금까지 출력했던 값들은 출력 형태가 명확하기 때문에 Rust가 출력해주었지만
구조체의 경우 이것을 쉼표로 구분할지, 개행을 해야 할지 등등 경우의 수가 너무 많다.
이렇게 둘 이상의 출력 형태를 가진 값에 대해서는 Rust는 미리 출력 형태를 정해두지 않았다.
따라서 println!으로 출력하려고 하면 다음과 같이 오류를 볼 수 있다.

peter@hp-laptop:~/rust-practice/chapter05/rectangles$ cd ..
peter@hp-laptop:~/rust-practice/chapter05$ cargo new rectangles_debug
     Created binary (application) `rectangles_debug` package
peter@hp-laptop:~/rust-practice/chapter05$ cd rectangles_debug/
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ vi src/main.rs

src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect: {}", rect);
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ cargo run   Compiling rectangles_debug v0.1.0 (/home/peter/rust-practice/chapter05/rectangles_debug)
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
  --> src/main.rs:12:26
   |
12 |     println!("rect: {}", rect);
   |                          ^^^^ `Rectangle` cannot be formatted with the default formatter
   |
   = 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
   = note: required by `std::fmt::Display::fmt`
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `rectangles_debug`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ 

역시 컴파일이 되지 않는다.
rust-analyzer와 같은 도구를 사용하고 있다면 컴파일을 시도도 하기 전에
오류가 있다는 밑줄과 그 원인을 확인할 수 있을 것이다.
그런데 가만, 오류 메시지를 보다보면 {:?}을 사용해보라는 말이 있다.
한 번 이 부분만 수정해서 다시 컴파일 해보자.

peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ vi src/main.rs

src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect: {:?}", rect);
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ cargo run
   Compiling rectangles_debug v0.1.0 (/home/peter/rust-practice/chapter05/rectangles_debug)
error[E0277]: `Rectangle` doesn't implement `std::fmt::Debug`
  --> src/main.rs:12:28
   |
12 |     println!("rect: {:?}", rect);
   |                            ^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `std::fmt::Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
   = note: required by `std::fmt::Debug::fmt`
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `rectangles_debug`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ 

그런데 막상 수정하고 보니 Rectangle은 이걸 사용할 수 없다고 한다.
Rust가 우리를 농락한 걸까?

물론 그럴 리 없다. 새로 오류 메시지를 살펴보자.
std::fmt::Debug라는 트레이트가 구현되어 있지 않아서 그런다고 한다.
{:?}는 디버그 출력 형태로 결과를 출력하는데
std::fmt::Debug 트레이트가 구현되어 있어야 그것이 가능해진다.
우리는 구조체 정의 앞에 #[derive(Debug)] 애노테이션을 추가함으로써 그것을 할 수 있다.

peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ vi src/main.rs

src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect: {:?}", rect);
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ cargo run   Compiling rectangles_debug v0.1.0 (/home/peter/rust-practice/chapter05/rectangles_debug)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/rectangles_debug`
rect: Rectangle { width: 30, height: 50 }
peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ 

이제 Rectangle 인스턴스인 rect의 값이 출력되는 걸 확인할 수 있다.
만약 {:?} 대신 {:#?}을 사용한다면 필드 단위로 개행되어 출력되는 것을 볼 수 있다.

트레이트 중 일부는 이렇게 #[deriv()] 애노테이션을 통해
사용자가 직접 만든 구조체에 유용한 동작을 쉽게 적용할 수 있도록 한다.
궁금하다면 공식문서를 참고하도록 하자.

메서드 Method

Rust에는 함수와 유사한 메서드라는 녀석이 존재한다.
어떤 언어에서는 함수와 메서드가 같은 것을 의미하는 것으로 혼용되고
어떤 언어에서는 둘 중 하나만 존재하기도 하지만
Rust에서는 함수와 메서드의 구분이 확실하다.

메서드는 구조체와 함께 사용된다.
// 사실 구조체 말고도 메서드와 함께 사용할 수 있는 건 더 있지만.
메서드의 첫번째 매개변수는 그것을 호출할 구조체 인스턴스를 표현하는 self여야 하며
그것은 인자로 직접 전달하는 게 아니라 그것을 호출한 인스턴스가 자동으로 들어간다.
메서드는 구조체이름.메서드이름();과 같이 호출할 수 있다.

직접 작성해보는 게 이해하기 더 수월할테니 예제를 통해 알아보자.
우리가 앞서 작성해본 직사각형 면적 구하기 예제로 돌아가보면,
직사각형의 면적을 구하는 area 함수는 Rectangle 구조체하고만 사용된다.
이러한 경우 우리는 areaRectangle의 메서드로 만들 수 있다.
메서드를 정의할 땐 impl 구조체이름 {} 블록 내에 정의한다.

peter@hp-laptop:~/rust-practice/chapter05/rectangles_debug$ cd ..
peter@hp-laptop:~/rust-practice/chapter05$ cargo new rectangles_method
     Created binary (application) `rectangles_method` package
peter@hp-laptop:~/rust-practice/chapter05$ cp rectangles/src/main.rs rectangles_method/src/main.rs 
peter@hp-laptop:~/rust-practice/chapter05$ cd rectangles_method/
peter@hp-laptop:~/rust-practice/chapter05/rectangles_method$ vi src/main.rs

src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect.area()
    );
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}
peter@hp-laptop:~/rust-practice/chapter05/rectangles_method$ cargo run
   Compiling rectangles_method v0.1.0 (/home/peter/rust-practice/chapter05/rectangles_method)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/rectangles_method`
The area of the rectangle is 1500 square pixels.
peter@hp-laptop:~/rust-practice/chapter05/rectangles_method$ 

메서드의 첫번째 매개변수는 self라고 했지만 사실 &self&mut self도 가능하다.
정확히는 오히려 self를 사용하는 경우가 더 드물다.
self를 사용할 경우 이 메서드를 호출하면 구조체 인스턴스에 대한 소유권을 잃는다.
일반적으로 메서드 호출 후에도 구조체 인스턴스를 사용할 수 있어야 하므로
self보다는 &self나 가변성을 더해 &mut self를 사용하는 것이다.

매개변수가 더 있는 경우에는 호출할 때 두번째 매개변수에 해당하는 인자부터 차례로 전달한다.
그리고 메서드가 여러 개일 경우 여러 개의 impl 블록을 만들 수도 있고
하나의 impl 블록에 모두 작성할 수도 있긴 하지만
굳이 여러 개로 나눌 이유는 없으니 하나의 impl 블록에 작성하는 것이 일반적이다.

연관 함수 Associated Function

impl 블록에는 메서드가 아닌 다른 함수도 존재할 수 있다.
이 함수는 메서드와 달리 self로 시작하지 않지만 그 구조체와 연관되어 있다.
이런 함수를 우리는 연관 함수라고 부른다.

구조체의 새로운 인스턴스를 생성하여 반환하는 생성자를 연관 함수로 구현하는 것이 일반적이다.
예를 들어, 우리가 자주 사용한 String::fromString 인스턴스를 생성하는 연관 함수다.
연관함수는 구조체이름::함수이름();과 같이 사용한다.

우리가 앞서 작성하던 예제에서는 다음과 같은 연관 함수들을 작성할 수 있다.

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

이 포스트의 내용은 공식문서의 5장 Using Structs to Structure Related Data에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글