Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

구조체 정의와 인스턴스 생성

구조체는 “튜플 타입” 절에서 다룬 튜플과 비슷합니다. 둘 다 여러 관련 값을 함께 담는다는 점이 그렇습니다. 튜플처럼 구조체 안의 각 요소도 서로 다른 타입일 수 있습니다. 하지만 튜플과 달리 구조체에서는 각 데이터 조각에 이름을 붙이므로, 그 값이 무엇을 뜻하는지 더 명확해집니다. 이런 이름 덕분에 구조체는 튜플보다 더 유연합니다. 인스턴스의 값을 지정하거나 접근할 때 데이터의 순서에 의존할 필요가 없기 때문입니다.

구조체를 정의하려면 struct 키워드를 쓰고 구조체 전체의 이름을 붙입니다. 구조체 이름은 함께 묶인 데이터 조각들이 무엇을 의미하는지 설명할 수 있어야 합니다. 그런 다음 중괄호 안에 데이터 조각의 이름과 타입을 정의하는데, 이것들을 필드(fields) 라고 부릅니다. 예를 들어 목록 5-1은 사용자 계정 정보를 저장하는 구조체를 보여 줍니다.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
Listing 5-1: User 구조체 정의

구조체를 정의한 뒤에는 각 필드에 구체적인 값을 지정해 그 구조체의 인스턴스(instance) 를 만들 수 있습니다. 인스턴스를 만들려면 구조체 이름 뒤에 중괄호를 쓰고 그 안에 key: value 쌍을 넣습니다. 여기서 키는 필드 이름이고 값은 그 필드에 저장할 데이터입니다. 필드를 구조체 정의와 같은 순서로 적을 필요는 없습니다. 다시 말해 구조체 정의는 타입을 위한 일반적인 템플릿 같은 것이고, 인스턴스는 그 템플릿에 구체적인 데이터를 채워 넣어 해당 타입의 값을 만들어 냅니다. 예를 들어 특정 사용자를 목록 5-2처럼 선언할 수 있습니다.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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 을 사용합니다. 인스턴스가 가변이라면, 점 표기법으로 특정 필드에 대입하여 값을 바꿀 수도 있습니다. 목록 5-3은 가변 User 인스턴스의 email 필드를 바꾸는 방법을 보여 줍니다.

Filename: src/main.rs
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");
}
Listing 5-3: User 인스턴스의 email 필드 값 바꾸기

인스턴스 전체가 가변이어야 한다는 점에 주의하세요. 러스트는 일부 필드만 따로 가변으로 표시하는 것을 허용하지 않습니다. 또한 다른 식들과 마찬가지로, 함수 본문의 마지막 식으로 구조체의 새 인스턴스를 만들어 암묵적으로 반환할 수도 있습니다.

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-4: 이메일과 사용자 이름을 받아 User 인스턴스를 반환하는 build_user 함수

함수 매개변수 이름을 구조체 필드 이름과 같게 두는 것은 자연스럽지만, emailusername 필드 이름과 변수 이름을 둘 다 반복해서 써야 하는 것은 약간 번거롭습니다. 구조체에 필드가 더 많아지면 이런 반복은 더 성가셔질 것입니다. 다행히도 이를 줄일 수 있는 편리한 축약 문법이 있습니다!

필드 초기화 축약 문법 사용하기

목록 5-4에서는 매개변수 이름과 구조체 필드 이름이 정확히 같기 때문에, 필드 초기화 축약(field init shorthand) 문법을 사용하여 build_user 를 목록 5-5처럼 다시 쓸 수 있습니다. 동작은 완전히 같지만 usernameemail 을 반복해서 적지 않아도 됩니다.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-5: usernameemail 매개변수 이름이 구조체 필드와 같을 때 필드 초기화 축약 문법을 사용하는 build_user 함수

여기서 우리는 email 이라는 필드를 가진 새 User 구조체 인스턴스를 만들고 있습니다. 그리고 build_user 함수의 email 매개변수에 들어 있는 값을 구조체 필드 email 의 값으로 넣고 싶습니다. 필드 이름과 매개변수 이름이 같기 때문에 email: email 대신 그냥 email 이라고만 써도 됩니다.

구조체 갱신 문법으로 인스턴스 만들기

같은 타입의 다른 인스턴스에 있는 값 대부분을 그대로 사용하되, 일부만 바꾼 새 인스턴스를 만들고 싶을 때가 자주 있습니다. 이때 구조체 갱신 문법(struct update syntax)을 사용할 수 있습니다.

먼저 목록 5-6에서는 갱신 문법 없이 평범한 방식으로 user2 라는 새 User 인스턴스를 만드는 방법을 보여 줍니다. email 에는 새 값을 넣고, 나머지 값은 목록 5-2에서 만든 user1 과 같은 값을 사용합니다.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

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

    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 인스턴스 만들기

구조체 갱신 문법을 사용하면, 목록 5-7처럼 더 적은 코드로 같은 효과를 낼 수 있습니다. .. 문법은 명시적으로 설정하지 않은 나머지 필드들이 주어진 인스턴스의 같은 이름의 필드와 동일한 값을 갖도록 하라는 뜻입니다.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

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

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}
Listing 5-7: 새 email 값을 갖되 나머지 값은 user1 에서 가져오도록 구조체 갱신 문법 사용하기

목록 5-7의 코드는 email 값만 다른 새 인스턴스 user2 를 만들고, username, active, sign_in_count 필드는 모두 user1 의 값을 사용합니다. ..user1 은 남은 필드들이 user1 의 대응하는 필드에서 값을 가져오도록 지정하는 문법이므로 반드시 마지막에 와야 합니다. 다만 구조체 정의에서 필드가 선언된 순서와 상관없이, 원하는 만큼의 필드에 대해 어떤 순서로든 값을 직접 지정할 수 있습니다.

구조체 갱신 문법이 대입문처럼 = 를 사용하는 데 주목하세요. 이것은 “이동(move)으로 변수와 데이터가 상호작용하는 방식” 절에서 보았듯이 데이터가 이동되기 때문입니다. 이 예제에서는 user1username 필드 안에 있던 Stringuser2 로 이동했으므로, user2 를 만든 뒤에는 user1 을 더 이상 사용할 수 없습니다. 만약 user2email 뿐 아니라 username 도 새 String 값을 넣고, user1 에서 activesign_in_count 값만 사용했다면, user2 생성 이후에도 user1 은 여전히 유효했을 것입니다. activesign_in_count 는 둘 다 Copy 트레이트를 구현하는 타입이기 때문에, “스택에만 있는 데이터: Copy 절에서 설명한 동작이 적용되기 때문입니다. 이 예제에서는 user1.email 도 계속 사용할 수 있습니다. 그 값은 user1 밖으로 이동되지 않았기 때문입니다.

튜플 구조체로 서로 다른 타입 만들기

러스트는 튜플과 비슷하게 생긴 구조체도 지원하는데, 이것을 튜플 구조체(tuple struct) 라고 부릅니다. 튜플 구조체는 구조체 이름이 부여하는 의미는 가지지만, 각 필드에 이름이 붙지는 않습니다. 대신 필드의 타입만 가집니다. 튜플 구조체는 튜플 전체에 이름을 붙여 다른 튜플과 구별되는 타입으로 만들고 싶을 때, 그리고 일반 구조체처럼 각 필드에 이름을 붙이는 것이 장황하거나 중복일 때 유용합니다.

튜플 구조체를 정의하려면 struct 키워드와 구조체 이름을 쓰고, 뒤에 괄호 안에 필드들의 타입을 나열합니다. 예를 들어 다음 코드는 ColorPoint 라는 두 튜플 구조체를 정의하고 사용합니다.

Filename: 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 를 인수로 받을 수 없습니다. 이 점을 제외하면 튜플 구조체 인스턴스는 일반 튜플과 비슷해서, 각 부분으로 구조분해할 수도 있고 . 와 인덱스로 개별 값에 접근할 수도 있습니다. 다만 구조분해할 때는 일반 튜플과 달리 튜플 구조체 이름을 명시해야 합니다. 예를 들어 origin 안의 값을 x, y, z 변수로 구조분해하려면 let Point(x, y, z) = origin; 처럼 씁니다.

유닛 유사 구조체 정의하기

필드가 전혀 없는 구조체도 정의할 수 있습니다! 이런 구조체는 “튜플 타입” 절에서 언급한 유닛 타입 () 와 비슷하게 동작하기 때문에 유닛 유사 구조체(unit-like structs) 라고 부릅니다. 유닛 유사 구조체는 어떤 타입에 트레이트를 구현해야 하지만, 그 타입 자체 안에 저장하고 싶은 데이터는 없을 때 유용합니다. 트레이트는 10장에서 다룹니다. 다음은 AlwaysEqual 이라는 유닛 구조체를 선언하고 인스턴스를 만드는 예입니다.

Filename: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

AlwaysEqual 을 정의할 때는 struct 키워드와 원하는 이름 뒤에 세미콜론만 적습니다. 중괄호도 괄호도 필요 없습니다! 그런 다음 subject 변수 안에 AlwaysEqual 인스턴스를 만들 수 있는데, 이 역시 중괄호나 괄호 없이 이름만 사용하는 비슷한 방식입니다. 나중에 이 타입에, 어떤 AlwaysEqual 인스턴스든 다른 어떤 인스턴스와도 항상 같다고 판단하는 동작을 구현할 수도 있다고 상상해 봅시다. 예를 들어 테스트할 때 항상 예측 가능한 결과를 얻고 싶을 수 있습니다. 그런 동작을 구현하는 데는 별도의 데이터가 필요하지 않습니다! 10장에서는 유닛 유사 구조체를 포함해 어떤 타입이든 트레이트를 정의하고 구현하는 방법을 배우게 됩니다.

구조체 데이터의 소유권

목록 5-1의 User 구조체 정의에서는 문자열 슬라이스 타입 &str 대신, 소유권을 가지는 String 타입을 사용했습니다. 이것은 의도적인 선택입니다. 구조체의 각 인스턴스가 자신의 데이터를 전부 소유하고, 구조체 전체가 유효한 동안 그 데이터도 유효하길 원하기 때문입니다.

구조체가 다른 무언가가 소유한 데이터에 대한 참조를 저장하도록 만드는 것도 가능합니다. 하지만 그렇게 하려면 10장에서 다룰 러스트 기능인 라이프타임 을 사용해야 합니다. 라이프타임은 구조체가 참조하는 데이터가 구조체가 살아 있는 동안 유효함을 보장합니다. 예를 들어 src/main.rs 에서 다음처럼 라이프타임 없이 구조체 안에 참조를 저장하려 하면, 이 코드는 동작하지 않습니다.

Filename: 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,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

10장에서는 구조체 안에 참조를 저장할 수 있도록 이런 오류를 어떻게 해결하는지 다룹니다. 하지만 지금은 이런 오류를 피하기 위해 &str 같은 참조 대신 String 같은 소유 타입을 사용하겠습니다.