User 구조체 업데이트에서 명시 필드, untouched 필드, Copy 필드, 이동 필드와 업데이트 뒤 user1의 사용 가능성을 대응한 필드 전달 장부
STRUCT UPDATE · FIELD TRANSFER
..base는 같은 구조체 타입에서 명시하지 않은 필드만 채웁니다. 각 필드의 타입에 따라 복사하거나 이동하며 자동으로 복제하지 않습니다.
let user2 = User {
email: new_email,
..user1
};
명시한 필드 표현식을 먼저 평가
여러 필드를 명시했다면 소스에 작성한 순서대로 왼쪽에서 오른쪽으로 평가합니다. 이 예제에서는 new_email이 먼저 평가됩니다.
마지막에 base 표현식을 평가
..user1은 문법상 마지막이며, 명시하지 않은 username, active, sign_in_count만 제공합니다.
email: new_email
초기화 명시한 새 값으로 user2.email을 만듭니다.
전달user1.email은 base에서 읽지 않아 그대로 남습니다.
이후user1.email을 계속 사용할 수 있습니다.
username: String
초기화 생략한 필드이므로 ..user1에서 가져옵니다.
전달String은 이 예제에서 복사되지 않고 user2.username으로 이동합니다.
이후user1.username과 user1 전체는 사용할 수 없습니다.
active: bool
초기화 생략한 필드이므로 ..user1에서 가져옵니다.
전달bool이 Copy를 구현하므로 값이 복사됩니다.
이후 원래 user1.active도 계속 사용할 수 있습니다.
sign_in_count: u64
초기화 생략한 필드이므로 ..user1에서 가져옵니다.
전달u64가 Copy를 구현하므로 값이 복사됩니다.
이후 원래 user1.sign_in_count도 계속 사용할 수 있습니다.
이 User 예제의 partial move 경계는 필드 단위입니다. 이동된 username 때문에 user1 전체는 사용할 수 없지만, 명시해서 건드리지 않은 email과 복사된 두 필드는 남아 있습니다.
다른 인스턴스에서 대부분의 값을 유지한 채로 몇 개의 값만 바꿔 새로운 인스턴스를 생성하게 되는 경우가 간혹 있습니다.
그럴 때 유용한 게 바로 구조체 업데이트 문법(struct update syntax) 입니다.
먼저 예제 4-6에서는 구조체 업데이트 문법을 사용하지 않고 새로운 User 인스턴스 user2를 만드는 방법을 보여줍니다.
email에는 새로운 값을 설정했지만, 나머지 값들에는 예제 4-2에서 만들었던 user1의 값과 동일합니다.
예제 4-6: user1의 값 중 하나를 다르게 한
새로운 User 인스턴스 생성하기
src/main.rs
fn main() { // --생략-- let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, };}
구조체 업데이트 문법을 사용하면 다음 예제 4-7처럼 더 적은 코드로 같은 효과를 낼 수 있습니다.
.. 문법은 따로 명시된 필드를 제외한 나머지 필드를 주어진 인스턴스의 필드 값으로 설정합니다.
예제 4-7: 새로운 email 값으로 User 구조체의
인스턴스를 생성하되, 나머지 필드는 구조체 업데이트 문법으로 user1의 필드
값을 사용하기
src/main.rs
fn main() { // --생략-- let user2 = User { email: String::from("another@example.com"), ..user1 };}
예제 4-7의 코드 또한 email 값이 다른 user2 인스턴스를 생성하지만 username, active, sign_in_count는 user1의 필드와 같은 값을 갖게 합니다.
user1의 값들과 동일한 값들로 나머지를 채우려면 ..user1을 제일 끝에 적어야 하지만, 다른 필드들은 구조체의 정의 내에 있는 필드들의 순서와는 상관없이 우리 마음대로 몇 개든 임의 순서로 적을 수 있습니다.
구조체 표현식에서 명시한 필드의 초기화 표현식은 소스에 작성한 순서대로 왼쪽에서 오른쪽으로 평가되고, 마지막의 ..user1 base 표현식은 그다음에 평가됩니다.
base는 새로 만들 구조체와 같은 타입이어야 하며, 명시하지 않은 필드만 base에서 가져옵니다. 이때 필드 타입이 Copy를 구현하면 값이 복사되고, 그렇지 않으면 이동 규칙이 적용됩니다. 구조체 업데이트 문법이 모든 필드를 자동으로 복제하는 것은 아닙니다.
이 예제에서는 명시하지 않은 user1.username의 String이 user2로 이동합니다. 따라서 이후에 user1 전체나 user1.username은 사용할 수 없지만, 새 값으로 명시한 email은 base에서 가져오지 않았으므로 user1.email은 계속 사용할 수 있습니다. active와 sign_in_count도 Copy 트레이트를 구현한 타입이므로 ‘스택에만 저장되는 데이터: 복사’절에서 살펴본 것처럼 원래 필드를 계속 사용할 수 있습니다.
user2에 email과 username의 String을 모두 명시하고 ..user1에서는 active와 sign_in_count만 가져온다면, 이동되는 필드가 없으므로 user2를 만든 이후에도 user1 전체가 유효합니다.
예제 4-1의 User 구조체 정의에서는 의도적으로
&str 문자열 슬라이스 대신 구조체가 소유권을 갖는 String 타입을 사용했습니다.
구조체 인스턴스가 유효한 동안 각 인스턴스 내의
모든 데이터가 유효하도록 만들기 위해서죠.
참조자를 이용해 구조체가 소유권을 갖지 않는 데이터도 저장할 수는 있지만,
이는 10장에서 배울 라이프타임(lifetime) 을 활용해야 합니다.
라이프타임을 사용하면 구조체가 존재하는 동안에
구조체 내 참조자가 가리키는 데이터의 유효함을 보장받을 수 있기 때문이죠.
만약 라이프타임을 명시하지 않고 참조자를 저장하고자 하면 다음처럼 문제가 발생합니다.
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, };}
이 정의에는 참조자와 구조체 인스턴스의 관계를 나타내는 라이프타임이 없으므로, 컴파일러는 E0106 범주의 오류를 보고합니다. 구체적인 줄 번호와 도움말 표현은 컴파일러 버전에 따라 달라질 수 있습니다.
error[E0106]: missing lifetime specifier
위 에러를 해결하여 구조체에 참조자를 저장하는 방법은 10장에서 알아보겠습니다.
지금 당장은 &str 대신 String을 사용하는 것으로
넘어가도록 하죠.
구조체를 고를 때는 이름 있는 필드, 튜플 구조체, 유사 유닛 구조체 중 어떤 형태가 의도를 가장 잘 드러내는지 함께 판단하면 좋습니다.
구조체 형태는 값을 생성하고 읽는 문법을 정하지만, 데이터의 소유·대여 관계는 각 필드 타입이 정합니다. 이름 있는 구조체와 튜플 구조체 모두 소유한 값이나 참조자를 필드로 가질 수 있습니다.
이름 있는 구조체, 튜플 구조체, 유사 유닛 구조체의 생성과 접근 문법을 비교하고 String 소유 필드와 참조 필드의 라이프타임 관계를 별도 축으로 설명
TWO INDEPENDENT AXES
세 형태는 모두 고유한 명목 타입을 만듭니다. 구조체의 형태는 생성·접근 문법을 정하고, 필드 타입은 데이터의 소유·대여 관계를 정하므로 두 선택을 하나의 분류로 섞지 않습니다.
AXIS 1 · FORM AND ACCESS
이름 있는 필드
struct User { email: String }
생성·접근User { email }처럼 필드명으로 초기화하고 user.email로 읽습니다. 생성식의 필드 순서는 자유지만 필요한 필드는 모두 채워야 합니다.
선택 각 값의 이름이 읽기 계약을 전달할 때 사용합니다.
튜플 구조체
struct Point(i32, i32);
생성·접근Point(10, 20)처럼 위치 순서대로 만들고 point.0 또는 Point(x, y) 패턴으로 읽습니다.
선택 필드명은 불필요하지만 일반 튜플과 구별되는 고유 타입이 필요할 때 사용합니다.
유사 유닛 구조체
struct AlwaysEqual;
생성·접근AlwaysEqual로 값을 만들며 읽을 필드가 없습니다.
선택 인스턴스별 저장 상태 없이 고유 타입과 트레이트 구현 지점이 필요할 때 사용합니다.
AXIS 2 · FIELD OWNERSHIP
이름 있는 구조체와 튜플 구조체 모두 아래 두 관계 중 필요한 필드 타입을 가질 수 있습니다.
String · 소유
인스턴스가 문자열 값을 소유합니다. 구조체가 유효한 동안 그 필드의 데이터도 함께 관리됩니다.
값을 다른 곳으로 넘길 때는 해당 타입의 이동·복사 규칙이 적용됩니다.
&'a str · 대여
인스턴스는 외부 문자열을 참조하며 소유하지 않습니다.
참조가 구조체보다 먼저 무효가 되지 않도록 'a 라이프타임 관계를 타입에 표현해야 합니다.
Copy는 별도의 ownership 형태가 아닙니다. 값을 사용할 때 복사할지 이동할지를 정하는 특성이며, 구조체의 named·tuple 형태와도 독립적입니다.
먼저 필드 이름과 위치가 의도를 어떻게 드러낼지에 따라 형태를 고르고, 그다음 각 필드가 값을 소유할지 외부 값을 빌릴지 타입으로 결정합니다. 유사 유닛 구조체는 필드 데이터가 없습니다.