구조체 (Struct)
Rust는 객체 지향형 프로그래밍(OOP) 언어가 아니다. Rust에서 일련의 연관된 데이타들의 그룹을 만들기 위해 구조체(struct)를 사용하며, 이 구조체에 연관되어 있는 함수(메서드)들을 구조체 밖에서 구현한다.
struct 선언
구조체를 정의하고 선언하기 위해서는 struct 키워드를 사용하여 구조체의 이름과 구조체 내의 필드들을 정의하면 된다.
예를 들어, 아래는 Member 라는 구조체를 정의한 것인데, Member는 fname, lname, age, active 라는 4개의 필드들을 가지고 있으며, 각 필드의 타입들을 함께 정의하고 있다. 구조체는 일종의 복합적인 Custom 데이타 타입이라고 볼 수 있다. Rust의 구조체는 struct 정의 안에 필드만을 가지며, 메서드를 정의하지 않는다.
// 구조체 선언 struct Member { fname: String, lname: String, age: u16, active: bool } fn main() { // 구조체 초기화 let mem1 = Member { active: true, fname: String::from("Tom"), lname: String::from("Lee"), age: 35 }; // 구조체 필드 읽기 println!("{}: {}", mem1.fname, mem1.active); }
구조체를 선언한 후, 구조체를 사용하기 위해서는 먼저 구조체의 초기 필드 데이타를 지정한다. 위의 예에서 구조체 초기화 부분에서 보면, Member 구조체의 각 필드에 원하는 초기값을 지정하고 있다. 예를 들어, active 필드에는 true 값을, fname에는 String 타입의 "Tom"을 지정하고 있다.
struct 데이타 엑세스
구조체의 필드값을 엑세스 위해서는 "{구조체변수}.{필드명}" 와 같이 점(.)을 찍고 필드명을 적으면 된다. 예를 들어, 구조체 변수 mem1의 fname 필드를 엑세스 하기 위해서는 mem1.fname 와 같이 표현이다.
초기화된 구조체의 데이타를 수정하기 위해서는 구조체 변수 앞에 mut를 써서 Mutable 구조체로 만들어 준다. 아래 예제에서 mem2 변수 앞에 mut 키워드가 지정하여 구조체 필드를 수정할 수 있게 하였다.
fn main() { // mut 를 사용하여 Mutable 구조체 변수로 let mut mem2 = Member { active: true, fname: String::from("Justin"), lname: String::from("Kim"), age: 35 }; // 구조체 필드 수정 mem2.active = false; // 구조체 필드 읽기 println!("{}: {}", mem2.fname, mem2.active); }
Tuple Struct
튜플처럼 필드명을 갖지 않고 필드들을 튜플로 묶어 정의하는 구조체를 Tuple Struct라고 한다. 일반적인 구조체는 struct 구조체명 { 필드명:필드타입, ... } 와 같이 정의되지만, Tuple Struct는 { ... } 블럭 대신 튜플을 사용하여 struct 구조체명(필드타입1, 필드타입2, ...) 와 같이 정의한다.
Tuple Struct는 튜플에 구조체명을 붙여 새로운 타입을 만든다. 예를 들어, 아래와 같이 (u8, u8, u8)을 묶을 튜플을 Color 라는 타입으로 만들어, 튜플 데이타의 집합이 무엇을 위한 것인 명료하게 한다.
// Tuple Struct 정의 struct Color(u8, u8, u8); fn main() { // 변수 초기화 let red = Color(255, 0, 0); // 필드 사용 println!("R={},G={},B={}", red.0, red.1, red.2); }
Unit-like Struct
Unit 타입은 () 으로 아무런 데이타를 갖지 않는 상태를 의미한다. 구조체를 정의할 때, 구조체의 필드들을 정의하지 않는 것을 Unit-like Struct 라고 한다.
fn main() { struct Dummy; let _dummy = Dummy; //... }
struct의 편리한 기능들
struct 필드 단축 초기화 : 만약 함수에 전달되는 파라미터명과 struct 필드명이 동일하다면, struct 초기화시 "필드명:파라미터명" 대신, "필드명"과 같이 ":파라미터명"을 생략할 수 있다.
아래 예제에서 get_member() 함수는 fname, lname, age 등 3개의 파라미터를 받아들이는데, 이들 파라미터명은 struct의 필드명과 동일하다 (보통 이런 경우가 많다). 이때, Member 구조체를 초기화할 때, "fname: fname," 처럼 앞의 필드명(fname)과 뒤의 파라미터명(fname)을 함께 지정할 수도 있고, 좀 더 간편하게 "fname,"과 같이 한번만 지정해도 된다.
fn main() { let mem = get_member("Tom".to_owned(), "Lee".to_owned(), 33); //... } fn get_member(fname: String, lname: String, age: u16) -> Member { Member { fname: fname, // 필드명: 파라미터명 지정 (원래 표현) lname, // 필드명만 지정해도 OK age, // 필드명만 지정 active: true } }
Struct Update 문법 : 만약 구조체 전체를 복제한 후 그 중 일부만을 변경하려 한다면, Struct Update 문법을 사용하면 편리하다. Struct Update 문법(struct update syntax)은 "..{구조체변수}" 으로, 복제할 이전 구조체 변수 앞에 .. 을 써서 나머지를 이전 구조체의 데이타로 채우는 것이다.
아래 예제를 보면, adm1 은 Admin 구조체를 초기화한 변수이고, adm2는 이 adm1 중에서 name 필드만을 변경하고 있다. 이를 위해 adm2의 각 필드를 adm1 필드들로 지정할 수 있지만, 필드가 많은 경우에는 불편하므로, 아래와 같이 adm2에서 수정할 필드만을 먼저 지정하고, 나머지는 모드 adm1에서 가져오게 한다. let adm2 = Admin { } 구문의 마지막에 "..adm1" 을 쓰면, 나머지 필드 데이타는 adm1에서 가져온다는 것을 의미한다. 이때 Struct Update 문법 즉 "..adm1"은 다른 값을 지정한 후 항상 마지막에 와야 한다.
struct Admin { name: String, group: String, active: bool } fn main() { let adm1 = Admin { name: String::from("Tom"), group: String::from("IT"), active: true }; let adm2 = Admin { name: String::from("Kim"), ..adm1 // struct update syntax }; println!("{}", adm2.group); // OK println!("{}", adm1.name); // OK //println!("{}", adm1.group); // 에러 }
한가지 주의할 점은, 이렇게 다른 구조체의 값을 가져올 때, 소유권을 가진 데이타 타입(Owned type)에 대해서는 소유권 이전(move)이 일어날 수 있다는 점이다. 예를 들어, 위의 경우 adm1의 group 필드는 그 소유권이 adm2.group 으로 이동하게 되어, 이후 adm1.group을 사용할 수 없게 된다. 참고로 adm1.name의 경우는 adm2.name으로 소유권이 이전 되지 않았기 때문에 사용할 수 있다.