구조체 정의 및 인스턴스화

Beginner

This tutorial is from open-source community. Access the source code

소개

구조체 정의 및 인스턴스화에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 Rust 에서 구조체를 정의하고 인스턴스화하는 방법을 배웁니다. 구조체는 여러 관련 값을 보유하며 명명된 필드를 가질 수 있어 데이터를 보다 유연하게 사용하고 액세스할 수 있습니다.

구조체 정의 및 인스턴스화

구조체는 "튜플 타입"에서 논의된 튜플과 유사하며, 둘 다 여러 관련 값을 보유합니다. 튜플과 마찬가지로 구조체의 구성 요소는 서로 다른 타입일 수 있습니다. 튜플과 달리 구조체에서는 각 데이터 조각의 이름을 지정하여 값의 의미를 명확하게 알 수 있습니다. 이러한 이름을 추가하면 구조체가 튜플보다 더 유연해집니다. 인스턴스의 값을 지정하거나 액세스하기 위해 데이터의 순서에 의존할 필요가 없습니다.

구조체를 정의하려면 struct 키워드를 입력하고 전체 구조체의 이름을 지정합니다. 구조체의 이름은 함께 그룹화되는 데이터 조각의 중요성을 설명해야 합니다. 그런 다음 중괄호 안에 데이터 조각의 이름과 타입을 정의하는데, 이를 *필드 (fields)*라고 합니다. 예를 들어, Listing 5-1 은 사용자 계정에 대한 정보를 저장하는 구조체를 보여줍니다.

파일 이름: src/main.rs

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

Listing 5-1: User 구조체 정의

구조체를 정의한 후 사용하려면 각 필드에 대한 구체적인 값을 지정하여 해당 구조체의 *인스턴스 (instance)*를 생성합니다. 구조체의 이름을 지정한 다음 중괄호 안에 키:값 쌍을 포함하여 인스턴스를 생성합니다. 여기서 키는 필드의 이름이고 값은 해당 필드에 저장하려는 데이터입니다. 구조체에서 선언한 것과 동일한 순서로 필드를 지정할 필요는 없습니다. 즉, 구조체 정의는 타입에 대한 일반적인 템플릿과 같으며, 인스턴스는 해당 템플릿을 특정 데이터로 채워 해당 타입의 값을 생성합니다. 예를 들어, Listing 5-2 와 같이 특정 사용자를 선언할 수 있습니다.

파일 이름: src/main.rs

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Listing 5-2: User 구조체의 인스턴스 생성

구조체에서 특정 값을 가져오려면 점 표기법을 사용합니다. 예를 들어, 이 사용자의 이메일 주소에 액세스하려면 user1.email을 사용합니다. 인스턴스가 가변적이면 점 표기법을 사용하고 특정 필드에 할당하여 값을 변경할 수 있습니다. Listing 5-3 은 가변 User 인스턴스의 email 필드에서 값을 변경하는 방법을 보여줍니다.

파일 이름: src/main.rs

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

Listing 5-3: User 인스턴스의 email 필드에서 값 변경

전체 인스턴스가 가변적이어야 합니다. Rust 는 특정 필드만 가변으로 표시하는 것을 허용하지 않습니다. 모든 표현식과 마찬가지로 함수 본문의 마지막 표현식으로 구조체의 새 인스턴스를 생성하여 해당 새 인스턴스를 암시적으로 반환할 수 있습니다.

Listing 5-4 는 주어진 이메일과 사용자 이름으로 User 인스턴스를 반환하는 build_user 함수를 보여줍니다. active 필드는 true 값을 얻고, sign_in_count1 값을 얻습니다.

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

Listing 5-4: 이메일과 사용자 이름을 받아 User 인스턴스를 반환하는 build_user 함수

함수 매개변수의 이름을 구조체 필드와 동일한 이름으로 지정하는 것이 합리적이지만, emailusername 필드 이름과 변수를 반복해야 하는 것은 약간 지루합니다. 구조체에 더 많은 필드가 있는 경우 각 이름을 반복하는 것이 더욱 짜증날 것입니다. 다행히 편리한 약어가 있습니다!

필드 초기화 단축 구문 사용

Listing 5-4 에서 매개변수 이름과 구조체 필드 이름이 정확히 동일하므로 필드 초기화 단축 구문 (field init shorthand) 구문을 사용하여 build_user를 다시 작성할 수 있습니다. 이렇게 하면 동일하게 작동하지만 Listing 5-5 에 표시된 것처럼 usernameemail이 반복되지 않습니다.

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

Listing 5-5: usernameemail 매개변수가 구조체 필드와 동일한 이름을 갖기 때문에 필드 초기화 단축 구문을 사용하는 build_user 함수

여기서는 email이라는 필드가 있는 User 구조체의 새 인스턴스를 생성하고 있습니다. email 필드의 값을 build_user 함수의 email 매개변수의 값으로 설정하려고 합니다. email 필드와 email 매개변수가 동일한 이름을 가지므로 email: email 대신 email만 작성하면 됩니다.

구조체 업데이트 구문을 사용하여 다른 인스턴스에서 인스턴스 생성

다른 인스턴스의 대부분의 값을 포함하지만 일부 값을 변경하는 구조체의 새 인스턴스를 생성하는 것이 유용한 경우가 많습니다. *구조체 업데이트 구문 (struct update syntax)*을 사용하여 이를 수행할 수 있습니다.

먼저, Listing 5-6 에서는 업데이트 구문 없이 user2에서 새 User 인스턴스를 일반적인 방식으로 생성하는 방법을 보여줍니다. email에 대한 새 값을 설정하지만, 그 외에는 Listing 5-2 에서 생성한 user1의 동일한 값을 사용합니다.

파일 이름: src/main.rs

fn main() {
    --snip--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

Listing 5-6: user1의 값 중 하나를 사용하여 새 User 인스턴스 생성

구조체 업데이트 구문을 사용하면 Listing 5-7 에 표시된 것처럼 더 적은 코드로 동일한 효과를 얻을 수 있습니다. 구문 ..은 명시적으로 설정되지 않은 나머지 필드가 지정된 인스턴스의 필드와 동일한 값을 가져야 함을 지정합니다.

파일 이름: src/main.rs

fn main() {
    --snip--


    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Listing 5-7: 구조체 업데이트 구문을 사용하여 User 인스턴스에 대한 새 email 값을 설정하지만 user1의 나머지 값을 사용

Listing 5-7 의 코드는 또한 email에 대해 다른 값을 갖지만 user1username, active, 및 sign_in_count 필드에 대해 동일한 값을 갖는 user2의 인스턴스를 생성합니다. ..user1은 나머지 필드가 user1의 해당 필드에서 값을 가져야 함을 지정하기 위해 마지막에 와야 하지만, 구조체의 정의에서 필드의 순서에 관계없이 원하는 만큼의 필드에 대한 값을 원하는 순서로 지정할 수 있습니다.

구조체 업데이트 구문은 할당과 마찬가지로 =를 사용합니다. 이는 "변수와 데이터의 상호 작용: 이동 (Move)"에서 살펴본 것처럼 데이터를 이동하기 때문입니다. 이 예제에서는 user1username 필드에 있는 Stringuser2로 이동되었으므로 user2를 생성한 후에는 더 이상 user1을 사용할 수 없습니다. user2emailusername 모두에 대한 새 String 값을 제공하여 user1에서 activesign_in_count 값만 사용했다면, user2를 생성한 후에도 user1은 여전히 유효합니다. activesign_in_count는 모두 Copy 트레이트를 구현하는 타입이므로 "스택 전용 데이터: 복사 (Copy)"에서 논의한 동작이 적용됩니다.

명명된 필드 없이 튜플 구조체를 사용하여 다른 타입 생성

Rust 는 또한 *튜플 구조체 (tuple structs)*라고 하는 튜플과 유사한 구조체를 지원합니다. 튜플 구조체는 구조체 이름이 제공하는 의미를 추가로 가지지만 필드와 관련된 이름은 없습니다. 대신 필드의 타입만 있습니다. 튜플 구조체는 전체 튜플에 이름을 지정하고 튜플을 다른 튜플과 다른 타입으로 만들고, 일반 구조체에서처럼 각 필드의 이름을 지정하는 것이 장황하거나 중복될 때 유용합니다.

튜플 구조체를 정의하려면 struct 키워드와 구조체 이름으로 시작한 다음 튜플의 타입을 입력합니다. 예를 들어, 여기서는 ColorPoint라는 두 개의 튜플 구조체를 정의하고 사용합니다.

파일 이름: src/main.rs

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 값은 서로 다른 튜플 구조체의 인스턴스이므로 서로 다른 타입입니다. 정의하는 각 구조체는 자체 타입이며, 구조체 내의 필드가 동일한 타입을 가질 수 있습니다. 예를 들어, Color 타입의 매개변수를 사용하는 함수는 두 타입 모두 세 개의 i32 값으로 구성되어 있더라도 Point를 인수로 사용할 수 없습니다. 그렇지 않으면 튜플 구조체 인스턴스는 개별 조각으로 분해할 수 있고, 인덱스 다음에 .을 사용하여 개별 값에 액세스할 수 있다는 점에서 튜플과 유사합니다.

필드가 없는 유닛과 유사한 구조체

필드가 없는 구조체도 정의할 수 있습니다! 이러한 구조체는 "튜플 타입"에서 언급한 유닛 타입 ()과 유사하게 동작하기 때문에 *유닛과 유사한 구조체 (unit-like structs)*라고 합니다. 유닛과 유사한 구조체는 일부 타입에 대해 트레이트 (trait) 를 구현해야 하지만 타입 자체에 저장할 데이터가 없을 때 유용할 수 있습니다. 트레이트는 10 장에서 논의할 것입니다. 다음은 AlwaysEqual이라는 유닛 구조체를 선언하고 인스턴스화하는 예입니다.

파일 이름: src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

AlwaysEqual을 정의하려면 struct 키워드, 원하는 이름, 세미콜론을 사용합니다. 중괄호나 괄호는 필요하지 않습니다! 그런 다음 정의한 이름을 사용하여 중괄호나 괄호 없이 subject 변수에서 AlwaysEqual의 인스턴스를 얻을 수 있습니다. 나중에 이 타입에 대한 동작을 구현하여 AlwaysEqual의 모든 인스턴스가 다른 타입의 모든 인스턴스와 항상 같도록 할 수 있다고 상상해 보세요. 아마도 테스트 목적으로 알려진 결과를 얻기 위해서일 것입니다. 해당 동작을 구현하는 데는 어떤 데이터도 필요하지 않을 것입니다! 10 장에서 유닛과 유사한 구조체를 포함하여 모든 타입에 대해 트레이트를 정의하고 구현하는 방법을 볼 수 있습니다.

구조체 데이터의 소유권

Listing 5-1 의 User 구조체 정의에서 &str 문자열 슬라이스 타입 대신 소유된 String 타입을 사용했습니다. 이는 이 구조체의 각 인스턴스가 모든 데이터를 소유하고 전체 구조체가 유효한 동안 해당 데이터가 유효하도록 하기 위한 의도적인 선택입니다.

구조체가 다른 항목이 소유한 데이터에 대한 참조를 저장하는 것도 가능하지만, 그렇게 하려면 10 장에서 논의할 Rust 기능인 *라이프타임 (lifetimes)*을 사용해야 합니다. 라이프타임은 구조체가 참조하는 데이터가 구조체가 유효한 동안 유효하도록 보장합니다. src/main.rs에서 다음과 같이 라이프타임을 지정하지 않고 구조체에 참조를 저장하려고 하면 작동하지 않습니다.

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

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

컴파일러는 라이프타임 지정자가 필요하다고 불평할 것입니다.

$ `cargo run`
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

10 장에서는 이러한 오류를 수정하여 구조체에 참조를 저장할 수 있도록 하는 방법을 논의하지만, 지금은 &str과 같은 참조 대신 String과 같은 소유된 타입을 사용하여 이러한 오류를 수정할 것입니다.

요약

축하합니다! 구조체 정의 및 인스턴스화 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.