구조체 정의와 인스턴스 생성
구조체는 “튜플 타입” 절에서 다룬 튜플과 비슷합니다. 둘 다 여러 관련 값을 함께 담는다는 점이 그렇습니다. 튜플처럼 구조체 안의 각 요소도 서로 다른 타입일 수 있습니다. 하지만 튜플과 달리 구조체에서는 각 데이터 조각에 이름을 붙이므로, 그 값이 무엇을 뜻하는지 더 명확해집니다. 이런 이름 덕분에 구조체는 튜플보다 더 유연합니다. 인스턴스의 값을 지정하거나 접근할 때 데이터의 순서에 의존할 필요가 없기 때문입니다.
구조체를 정의하려면 struct 키워드를 쓰고 구조체 전체의 이름을 붙입니다. 구조체
이름은 함께 묶인 데이터 조각들이 무엇을 의미하는지 설명할 수 있어야 합니다. 그런
다음 중괄호 안에 데이터 조각의 이름과 타입을 정의하는데, 이것들을 필드(fields)
라고 부릅니다. 예를 들어 목록 5-1은 사용자 계정 정보를 저장하는 구조체를 보여 줍니다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
User 구조체 정의구조체를 정의한 뒤에는 각 필드에 구체적인 값을 지정해 그 구조체의 인스턴스(instance)
를 만들 수 있습니다. 인스턴스를 만들려면 구조체 이름 뒤에 중괄호를 쓰고 그 안에
key: value 쌍을 넣습니다. 여기서 키는 필드 이름이고 값은 그 필드에 저장할
데이터입니다. 필드를 구조체 정의와 같은 순서로 적을 필요는 없습니다. 다시 말해
구조체 정의는 타입을 위한 일반적인 템플릿 같은 것이고, 인스턴스는 그 템플릿에
구체적인 데이터를 채워 넣어 해당 타입의 값을 만들어 냅니다. 예를 들어 특정 사용자를
목록 5-2처럼 선언할 수 있습니다.
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,
};
}
User 구조체의 인스턴스 생성구조체 안의 특정 값에 접근하려면 점 표기법을 사용합니다. 예를 들어 이 사용자의
이메일 주소에 접근하려면 user1.email 을 사용합니다. 인스턴스가 가변이라면,
점 표기법으로 특정 필드에 대입하여 값을 바꿀 수도 있습니다. 목록 5-3은 가변 User
인스턴스의 email 필드를 바꾸는 방법을 보여 줍니다.
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");
}
User 인스턴스의 email 필드 값 바꾸기인스턴스 전체가 가변이어야 한다는 점에 주의하세요. 러스트는 일부 필드만 따로 가변으로 표시하는 것을 허용하지 않습니다. 또한 다른 식들과 마찬가지로, 함수 본문의 마지막 식으로 구조체의 새 인스턴스를 만들어 암묵적으로 반환할 수도 있습니다.
목록 5-4는 주어진 이메일과 사용자 이름으로 User 인스턴스를 반환하는 build_user
함수를 보여 줍니다. active 필드는 true, sign_in_count 필드는 1 값을
갖습니다.
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"),
);
}
User 인스턴스를 반환하는 build_user 함수함수 매개변수 이름을 구조체 필드 이름과 같게 두는 것은 자연스럽지만, email 과
username 필드 이름과 변수 이름을 둘 다 반복해서 써야 하는 것은 약간 번거롭습니다.
구조체에 필드가 더 많아지면 이런 반복은 더 성가셔질 것입니다. 다행히도 이를 줄일 수
있는 편리한 축약 문법이 있습니다!
필드 초기화 축약 문법 사용하기
목록 5-4에서는 매개변수 이름과 구조체 필드 이름이 정확히 같기 때문에, 필드 초기화
축약(field init shorthand) 문법을 사용하여 build_user 를 목록 5-5처럼 다시
쓸 수 있습니다. 동작은 완전히 같지만 username 과 email 을 반복해서 적지 않아도
됩니다.
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"),
);
}
username 과 email 매개변수 이름이 구조체 필드와 같을 때 필드 초기화 축약 문법을 사용하는 build_user 함수여기서 우리는 email 이라는 필드를 가진 새 User 구조체 인스턴스를 만들고
있습니다. 그리고 build_user 함수의 email 매개변수에 들어 있는 값을 구조체
필드 email 의 값으로 넣고 싶습니다. 필드 이름과 매개변수 이름이 같기 때문에
email: email 대신 그냥 email 이라고만 써도 됩니다.
구조체 갱신 문법으로 인스턴스 만들기
같은 타입의 다른 인스턴스에 있는 값 대부분을 그대로 사용하되, 일부만 바꾼 새 인스턴스를 만들고 싶을 때가 자주 있습니다. 이때 구조체 갱신 문법(struct update syntax)을 사용할 수 있습니다.
먼저 목록 5-6에서는 갱신 문법 없이 평범한 방식으로 user2 라는 새 User 인스턴스를
만드는 방법을 보여 줍니다. email 에는 새 값을 넣고, 나머지 값은 목록 5-2에서 만든
user1 과 같은 값을 사용합니다.
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,
};
}
user1 의 값 대부분을 사용해 새 User 인스턴스 만들기구조체 갱신 문법을 사용하면, 목록 5-7처럼 더 적은 코드로 같은 효과를 낼 수 있습니다.
.. 문법은 명시적으로 설정하지 않은 나머지 필드들이 주어진 인스턴스의 같은 이름의
필드와 동일한 값을 갖도록 하라는 뜻입니다.
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
};
}
email 값을 갖되 나머지 값은 user1 에서 가져오도록 구조체 갱신 문법 사용하기목록 5-7의 코드는 email 값만 다른 새 인스턴스 user2 를 만들고, username,
active, sign_in_count 필드는 모두 user1 의 값을 사용합니다. ..user1 은
남은 필드들이 user1 의 대응하는 필드에서 값을 가져오도록 지정하는 문법이므로
반드시 마지막에 와야 합니다. 다만 구조체 정의에서 필드가 선언된 순서와 상관없이,
원하는 만큼의 필드에 대해 어떤 순서로든 값을 직접 지정할 수 있습니다.
구조체 갱신 문법이 대입문처럼 = 를 사용하는 데 주목하세요. 이것은
“이동(move)으로 변수와 데이터가 상호작용하는 방식” 절에서
보았듯이 데이터가 이동되기 때문입니다. 이 예제에서는 user1 의 username 필드
안에 있던 String 이 user2 로 이동했으므로, user2 를 만든 뒤에는 user1 을
더 이상 사용할 수 없습니다. 만약 user2 에 email 뿐 아니라 username 도 새
String 값을 넣고, user1 에서 active 와 sign_in_count 값만 사용했다면,
user2 생성 이후에도 user1 은 여전히 유효했을 것입니다. active 와
sign_in_count 는 둘 다 Copy 트레이트를 구현하는 타입이기 때문에,
“스택에만 있는 데이터: Copy” 절에서 설명한 동작이
적용되기 때문입니다. 이 예제에서는 user1.email 도 계속 사용할 수 있습니다.
그 값은 user1 밖으로 이동되지 않았기 때문입니다.
튜플 구조체로 서로 다른 타입 만들기
러스트는 튜플과 비슷하게 생긴 구조체도 지원하는데, 이것을 튜플 구조체(tuple struct) 라고 부릅니다. 튜플 구조체는 구조체 이름이 부여하는 의미는 가지지만, 각 필드에 이름이 붙지는 않습니다. 대신 필드의 타입만 가집니다. 튜플 구조체는 튜플 전체에 이름을 붙여 다른 튜플과 구별되는 타입으로 만들고 싶을 때, 그리고 일반 구조체처럼 각 필드에 이름을 붙이는 것이 장황하거나 중복일 때 유용합니다.
튜플 구조체를 정의하려면 struct 키워드와 구조체 이름을 쓰고, 뒤에 괄호 안에
필드들의 타입을 나열합니다. 예를 들어 다음 코드는 Color 와 Point 라는
두 튜플 구조체를 정의하고 사용합니다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
black 과 origin 값은 서로 다른 타입의 인스턴스이기 때문에 타입도 다르다는 점에
주목하세요. 구조체 내부 필드 타입이 같더라도, 여러분이 정의한 각 구조체는 그 자체로
고유한 타입입니다. 예를 들어 Color 타입을 받는 함수는, 둘 다 세 개의 i32 값으로
이루어져 있더라도 Point 를 인수로 받을 수 없습니다. 이 점을 제외하면 튜플 구조체
인스턴스는 일반 튜플과 비슷해서, 각 부분으로 구조분해할 수도 있고 . 와 인덱스로
개별 값에 접근할 수도 있습니다. 다만 구조분해할 때는 일반 튜플과 달리 튜플 구조체
이름을 명시해야 합니다. 예를 들어 origin 안의 값을 x, y, z 변수로
구조분해하려면 let Point(x, y, z) = origin; 처럼 씁니다.
유닛 유사 구조체 정의하기
필드가 전혀 없는 구조체도 정의할 수 있습니다! 이런 구조체는 “튜플 타입”
절에서 언급한 유닛 타입 () 와 비슷하게 동작하기 때문에 유닛 유사 구조체(unit-like
structs) 라고 부릅니다. 유닛 유사 구조체는 어떤 타입에 트레이트를 구현해야 하지만,
그 타입 자체 안에 저장하고 싶은 데이터는 없을 때 유용합니다. 트레이트는 10장에서
다룹니다. 다음은 AlwaysEqual 이라는 유닛 구조체를 선언하고 인스턴스를 만드는
예입니다.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
AlwaysEqual 을 정의할 때는 struct 키워드와 원하는 이름 뒤에 세미콜론만
적습니다. 중괄호도 괄호도 필요 없습니다! 그런 다음 subject 변수 안에
AlwaysEqual 인스턴스를 만들 수 있는데, 이 역시 중괄호나 괄호 없이 이름만
사용하는 비슷한 방식입니다. 나중에 이 타입에, 어떤 AlwaysEqual 인스턴스든 다른
어떤 인스턴스와도 항상 같다고 판단하는 동작을 구현할 수도 있다고 상상해 봅시다.
예를 들어 테스트할 때 항상 예측 가능한 결과를 얻고 싶을 수 있습니다. 그런 동작을
구현하는 데는 별도의 데이터가 필요하지 않습니다! 10장에서는 유닛 유사 구조체를
포함해 어떤 타입이든 트레이트를 정의하고 구현하는 방법을 배우게 됩니다.
구조체 데이터의 소유권
목록 5-1의 User 구조체 정의에서는 문자열 슬라이스 타입 &str 대신, 소유권을
가지는 String 타입을 사용했습니다. 이것은 의도적인 선택입니다. 구조체의 각
인스턴스가 자신의 데이터를 전부 소유하고, 구조체 전체가 유효한 동안 그 데이터도
유효하길 원하기 때문입니다.
구조체가 다른 무언가가 소유한 데이터에 대한 참조를 저장하도록 만드는 것도 가능합니다. 하지만 그렇게 하려면 10장에서 다룰 러스트 기능인 라이프타임 을 사용해야 합니다. 라이프타임은 구조체가 참조하는 데이터가 구조체가 살아 있는 동안 유효함을 보장합니다. 예를 들어 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
같은 소유 타입을 사용하겠습니다.