구조체 정의와 인스턴스화
구조체 정의와 인스턴스화 (Defining and Instantiating Structs)
구조체는 "튜플 타입" 절에서 다뤘던 튜플과 비슷해요. 둘 다 여러 관련 값을 담는다는 점이 공통점이죠. 튜플처럼 구조체의 조각들도 서로 다른 타입일 수 있어요. 하지만 튜플과 달리 구조체에서는 각 데이터 조각에 이름을 붙이기 때문에 값이 무엇을 뜻하는지 분명해요. 이 이름 덕분에 구조체는 튜플보다 더 유연해요. 인스턴스의 값을 지정하거나 접근할 때 데이터의 순서에 의존하지 않아도 되거든요.
출처: The Rust Book
구조체를 정의하려면 키워드 struct를 쓰고 구조체 전체의 이름을 지어요. 구조체의 이름은 함께 묶인 데이터 조각들의 의미를 잘 드러내야 해요. 그런 다음 중괄호 안에서 데이터 조각들의 이름과 타입을 정의하는데, 이 조각들을 **필드(field)**라고 불러요. 예를 들어 Listing 5-1은 사용자 계정에 대한 정보를 저장하는 구조체를 보여줘요.
Filename: src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
Listing 5-1: User 구조체 정의
구조체를 정의한 뒤에 사용하려면, 각 필드에 구체적인 값을 지정해서 그 구조체의 인스턴스를 만들어야 해요. 인스턴스를 만들려면 구조체의 이름을 쓰고 중괄호 안에 *key: value* 쌍을 넣으면 돼요. 여기서 키는 필드의 이름이고, 값은 그 필드에 저장하고 싶은 데이터예요. 필드의 순서는 구조체에서 선언한 순서와 같을 필요는 없어요. 다시 말해 구조체 정의는 그 타입에 대한 일반적인 템플릿 같은 것이고, 인스턴스는 그 템플릿에 특정 데이터를 채워서 그 타입의 값을 만들어내는 거죠. 예를 들어 Listing 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("[email protected]"),
sign_in_count: 1,
};
}
Listing 5-2: User 구조체의 인스턴스 만들기
구조체에서 특정 값을 가져오려면 점 표기법(dot notation)을 사용해요. 예를 들어 이 사용자의 이메일 주소에 접근하려면 user1.email을 쓰면 돼요. 인스턴스가 가변(mutable)이라면 점 표기법으로 특정 필드에 값을 할당해서 그 값을 바꿀 수 있어요. Listing 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("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
}
Listing 5-3: User 인스턴스의 email 필드 값 바꾸기
인스턴스 전체가 가변이어야 한다는 점에 주의하세요. Rust는 특정 필드만 가변으로 표시하는 것을 허용하지 않아요. 다른 표현식과 마찬가지로, 함수 본문의 마지막 표현식으로 구조체의 새 인스턴스를 구성하면 그 새 인스턴스를 암시적으로 반환할 수 있어요.
Listing 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("[email protected]"),
String::from("someusername123"),
);
}
Listing 5-4: 이메일과 사용자 이름을 받아 User 인스턴스를 반환하는 build_user 함수
함수 매개변수의 이름을 구조체 필드와 같은 이름으로 짓는 건 합리적이지만, email과 username 필드 이름과 변수를 반복해서 적어야 한다는 게 조금 번거로워요. 구조체에 필드가 더 많다면 각 이름을 반복하는 일은 더욱 귀찮아지겠죠. 다행히 편리한 축약 문법이 있어요!
필드 초기화 축약(field init shorthand) 사용하기
Listing 5-4에서 매개변수 이름과 구조체 필드 이름이 정확히 같으므로, 필드 초기화 축약 문법을 사용해 build_user를 다시 작성할 수 있어요. 동작은 완전히 같되 username과 email의 반복이 없어져요. Listing 5-5를 보세요.
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("[email protected]"),
String::from("someusername123"),
);
}
Listing 5-5: username과 email 매개변수가 구조체 필드와 이름이 같아서 필드 초기화 축약을 사용한 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과 같은 값을 사용해요.
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("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
Listing 5-6: user1의 값 하나를 제외하고 모두 사용해 새 User 인스턴스 만들기
구조체 업데이트 문법을 사용하면 Listing 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("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("[email protected]"),
..user1
};
}
Listing 5-7: 구조체 업데이트 문법으로 User 인스턴스의 email 값은 새로 설정하고 나머지 값은 user1에서 가져오기
Listing 5-7의 코드도 user2에 email 값은 다르고 username, active, sign_in_count 필드는 user1에서 가져온 인스턴스를 만들어요. ..user1은 반드시 맨 마지막에 와야 해요. 남은 필드들의 값을 user1의 대응하는 필드에서 가져오겠다는 뜻이니까요. 다만 원하는 필드들을 어떤 순서로든 몇 개든 지정할 수 있고, 구조체 정의의 필드 순서와는 상관없어요.
구조체 업데이트 문법이 =를 할당처럼 사용한다는 점에 주목하세요. "변수와 데이터의 상호작용: 이동(Move)" 절에서 봤듯이 데이터를 이동시키기 때문이에요. 이 예시에서는 user2를 만든 뒤에 user1을 더 이상 사용할 수 없어요. user1의 username 필드에 있던 String이 user2로 이동됐기 때문이죠. 만약 email과 username 양쪽에 새 String 값을 넣어서 user1의 active와 sign_in_count 값만 사용했다면, user2를 만든 뒤에도 user1은 여전히 유효했을 거예요. active와 sign_in_count는 둘 다 Copy 트레이트를 구현하는 타입이라, "스택 전용 데이터: Copy" 절에서 다룬 동작이 적용되거든요. 이 예시에서 user1.email을 여전히 사용할 수 있는 것도, 그 값이 user1에서 이동되지 않았기 때문이에요.
튜플 구조체로 여러 타입 만들기
Rust는 튜플과 비슷하게 생긴 구조체도 지원하는데, 이를 *튜플 구조체(tuple struct)*라고 불러요. 튜플 구조체는 구조체 이름이 주는 추가적인 의미는 있지만, 필드에 이름이 연결되어 있지는 않아요. 필드의 타입만 있을 뿐이죠. 튜플 전체에 이름을 붙여서 다른 튜플과 다른 타입으로 만들고 싶을 때, 그리고 일반 구조체처럼 각 필드에 이름을 붙이는 게 장황하거나 중복적일 때 튜플 구조체가 유용해요.
튜플 구조체를 정의하려면 struct 키워드와 구조체 이름을 쓰고 뒤에 튜플의 타입들을 나열하면 돼요. 예를 들어 여기서는 Color와 Point라는 이름의 튜플 구조체 두 개를 정의하고 사용해요.
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);
}
black과 origin 값은 서로 다른 튜플 구조체의 인스턴스이므로 서로 다른 타입이라는 점에 유의하세요. 여러분이 정의하는 각 구조체는 필드가 같은 타입일지라도 각자 고유한 타입이에요. 예를 들어 Color 타입의 매개변수를 받는 함수는, Point 타입이 세 개의 i32 값으로 이루어졌다 하더라도 Point를 인자로 받을 수 없어요. 그 외에는 튜플 구조체 인스턴스가 튜플과 비슷해서, 각 조각으로 분해(destructure)할 수 있고 인덱스와 함께 .을 사용해 개별 값에 접근할 수 있어요. 튜플과 달리 튜플 구조체는 분해할 때 구조체의 타입도 함께 이름을 적어야 해요. 예를 들어 origin 지점의 값을 x, y, z라는 변수로 분해하려면 let Point(x, y, z) = origin;이라고 작성해요.
유닛-라이크 구조체(Unit-Like Struct) 정의하기
필드가 아예 없는 구조체를 정의할 수도 있어요! 이런 구조체를 *유닛-라이크 구조체(unit-like struct)*라고 불러요. "튜플 타입" 절에서 언급했던 유닛 타입 ()과 비슷하게 동작하기 때문이죠. 유닛-라이크 구조체는 어떤 타입에 트레이트를 구현해야 하는데 그 타입 자체에 저장하고 싶은 데이터가 없을 때 유용해요. 트레이트에 대한 자세한 내용은 10장에서 다룰게요. 여기서는 AlwaysEqual이라는 이름의 유닛 구조체를 선언하고 인스턴스화하는 예시를 볼게요.
Filename: src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
AlwaysEqual을 정의하려면 struct 키워드와 원하는 이름을 쓰고 세미콜론을 붙이면 돼요. 중괄호나 괄호는 필요 없어요! 그러면 subject 변수에 AlwaysEqual의 인스턴스를 비슷한 방식으로 만들 수 있어요. 정의한 이름을 중괄호나 괄호 없이 그대로 쓰면 돼요. 나중에 이 타입이 테스트 목적의 알려진 결과를 위해, 모든 AlwaysEqual 인스턴스가 어떤 다른 타입의 모든 인스턴스와 항상 같아지는 동작을 구현한다고 상상해 보세요. 그 동작을 구현하는 데 데이터는 전혀 필요 없을 거예요! 10장에서 트레이트를 정의하고 유닛-라이크 구조체를 포함한 어떤 타입에든 구현하는 방법을 보게 될 거예요.
구조체 데이터의 소유권
Listing 5-1의 User 구조체 정의에서 우리는 &str 문자열 슬라이스 타입 대신 소유된 String 타입을 사용했어요. 이것은 의도적인 선택이에요. 각 인스턴스가 자신의 모든 데이터를 소유하고, 그 데이터가 구조체 전체가 유효한 동안 내내 유효하기를 원하니까요.
구조체가 다른 무언가가 소유한 데이터에 대한 참조를 저장하는 것도 가능해요. 하지만 그러려면 10장에서 다룰 Rust 기능인 *수명(lifetime)*을 사용해야 해요. 수명은 구조체가 참조하는 데이터가 구조체만큼 오래 유효하다는 것을 보장해 줘요. 수명을 지정하지 않고 구조체에 참조를 저장하려고 하면, 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: "[email protected]",
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 같은 소유된 타입으로 이런 에러를 해결할 거예요.