- 구조체(struct)는 여러 값을 묶어서 어떤 의미를 갖는 데이터 단위를 정의하는 데에 사용합니다.
- 객체지향 언어에 익숙하신 분들은 구조체를 "객체가 갖는 데이터 속성"과 같은 개념으로 이해하셔도 좋습니다.
- 이번 장에선 앞서 배운 튜플과 구조체 간 비교, 구조체 사용법, 구조체의 데이터와 연관된 동작을 표현하는 메소드, 연관함수(associated functions) 정의 방법을 다룹니다.
- 필요한 데이터 형식을 작성할 때 구조체나 열거형(6장에서 배울 예정입니다)을 이용하면, 여러분이 직접 만든 타입에도 러스트의 컴파일 시점 타입 검사 기능을 최대한 활용할 수 있습니다.
5.1 구조체 정의 및 인스턴트화
- 구조체는 3장에서 배운 튜플과 비슷합니다.
- 튜플처럼 구조체의 구성 요소들은 각각 다른 타입이 될 수 있습니다.
- 그리고 여기에 더해서, 구조체는 각각의 구성 요소에 이름을 붙일 수 있습니다.
- 따라서 각 요소가 더 명확한 의미를 갖게 되고, 특정 요소에 접근할 때 순서에 의존할 필요도 사라집니다.
- 결론적으로, 튜플보다 유연하게 사용할 수 있습니다.
구조체를 정의할 땐 struct
키워드와 해당 구조체에 지어줄 이름을 입력 하면 됩니다. 이때 구조체 이름은 함께 묶을 데이터의 의미에 맞도록 지어주세요. 이후 중괄호 안에서는 필드(field)라 하는 각 구성 요소의 이름 및 타입을 정의합니다.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
- 정의한 구조체를 사용하려면 해당 구조체 내 각 필드의 값을 정해 인스턴스(instance)를 생성해야 합니다.
- 구조체 정의는 대충 해당 구조체에 무엇이 들어갈지를 정해둔 양식이며, 인스턴스는 거기에 실제 값을 넣은 것이라고 생각하시면 됩니다. 예시로 확인해 보죠
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
user1.email = String::from("anotheremail@example.com");
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
- 구조체 내 특정 값은 점(.) 표기법으로 얻어올 수 있습니다. 사용자의 이메일 주소를 얻어야 한다치면
user1.email
처럼 사용할 수 있죠. - 변경 가능한 인스턴스라면, 같은 방식으로 특정 필드의 값을 변경할 수도 있습니다.
- 가변성은 해당 인스턴스 전체가 지니게 됩니다.(일부 필드만 변경 가능하도록 만들 수는 없음)
- 구조체도 다른 표현식과 마찬가지로 함수 마지막 표현식에서 암묵적 으로 새 인스턴스를 생성하고 반환할 수 있습니다.
변수명과 필드명이 같을 때 간단하게 필드 초기화하기
변수명과 구조체 필드명이 같을 땐, 필드 초기화 축약법(field init shorthand)을 사용해서 더 적은 타이핑으로 같은 기능을 구현할 수 있습니다.
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
기존 인스턴스를 이용해 새 인스턴스를 만들 때 구조체 갱신법 사용하기
기존에 있던 인스턴스에서 대부분의 값을 유지한 채로 몇몇 값만 바꿔 새로운 인스턴스를 생성하게 되는 경우가 간혹 있습니다. 그럴 때 유용한 게 바로 구조체 갱신법(struct update syntax)입니다.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 구조체 갱신법 X
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
// 구조체 갱신법 O
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
}
필드명이 없는, 타입 구분용 튜플 구조체
- 구조체를 사용해 튜플과 유사한 형태의 튜플 구조체(tuple structs)를 정의할 수도 있습니다.
- 튜플 구조체는 필드의 이름을 붙이지 않고 필드 타입 만을 정의하며, 구조체 명으로 의미를 갖는 구조체입니다.
- 이는 튜플 전체에 이름을 지어주거나 특정 튜플을 다른 튜플과 구분 짓고 싶은데, 그렇다고 각 필드명을 일일이 정해 일반적인 구조체를 만드는 것은 배보다 배꼽이 더 큰 격이 될 수 있을 때 유용합니다.
튜플 구조체의 정의는 일반적인 구조체처럼 struct
키워드와 구조체 명으로 시작되나, 그 뒤에는 타입들로 이루어진 튜플이 따라옵니다.
fn main() {
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
black
,origin
이 서로 다른 튜플 구조체의 인스턴스이므로, 타입이 서로 달라진다는 점이 중요합니다. 구조체 내 필드 구성이 같더라도 각각의 구조체는 별도의 타입이기 때문이죠.- 즉,
Color
타입과Point
타입은 둘 다i32
값 3 개로 이루어진 타입이지만,Color
타입을 매개변수로 받는 함수에Point
타입을 인자로 넘겨주는 건 불가능합니다.
필드가 없는 유사 유닛 구조체
- 필드가 아예 없는 구조체를 정의할 수도 있습니다.
- 유닛 타입인
()
과 비슷하게 동작하므로 유사 유닛 구조체(unit-like structs) 라 지칭 - 어떤 타입을 내부 데이터 저장 없이 10장에서 배울 트레잇을 구현하기만 하는 용도로 사용할 때 주로 활용됩니다.
구조체 데이터의 소유권
-
User
구조체 정의에서는 의도적으로&str
문자열 슬라이스 대신 구조체가 소유권을 갖는String
타입을 사용했습니다. - 구조체 인스턴스가 유효한 동안 인스턴스 내의 모든 데이터가 유효하도록 만들기 위해서죠. - 참조자를 이용해 구조체가 소유권을 갖지 않는 데이터도 저장할 수는 있지만, - 이는 10장에서 배울 라이프타임(lifetime)을 활용해야 합니다. - 라이프타임을 사용하면 구조체가 존재하는 동안에 구조체 내 참조자가 가리키는 데이터의 유효함을 보장받을 수 있기 때문이죠. -
만약 라이프타임을 명시하지 않고 참조자를 저장하고자 하면 다음처럼 문제가 발생합니다.
-
위 에러를 해결하여 구조체에 참조자를 저장하는 방법은 10장에서 알아볼 겁니다.
-
지금 당장은
&str
대신String
을 사용함으로써 넘어가도록 하죠.