0%

Rust 구조체란

러스트 구조체(struct)에 대해 공부한 내용을 정리합니다.


모든 프로그래밍 언어에는 기본적으로 제공되는 데이터 타입이 있다. 배열이나 튜플 또한 그 중 하나이다. 하지만 그것만으로는 모든 데이터들을 효과적으로 핸들링하기 힘들다. 그럴 때 구조체는 좋은 해결 방법이다. 구조체는 프로그래머가 묶어서 저장하고 싶은 데이터들을 직접 정의하고 변수 하나로 간편하게 관리할 수 있다.

구조체

러스트에서 구조체란 프로그래머가 직접 정의한 데이터 타입을 말한다. 구조체를 정의하는 방법은 struct 뒤에 구조체 이름, 중괄호{} 를 입력한다. 그 다음 중괄호 안에 저장할 데이터들의 이름과 타입을 작성하면 끝이다. 아래의 예시 코드를 살펴보자.

1
2
3
4
5
6
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

위 구조체는 어떤 서비스의 사용자 정보를 저장하는 데이터 타입이다. 이름, 이메일, 로그인 한 횟수, 활동 중인지 등을 한번에 저장 및 처리할 수 있다. 이처럼 구조체 내부에 저장되는 데이터들을 필드(field)라고 한다. 위 구조체에서 필드는 username, email, login_count, active 이다.

다음으로는 해당 구조체를 사용하여 USER1라는 유저의 정보를 입력해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

fn main() {
let user1 =User {
username: String::from("USER1"),
email: String::from("user1@gmail.com"),
active: true,
login_count: 1,
};

println!("{:?}", user1.username);
println!("{:?}", user1.email);
println!("{:?}", user1.active);
println!("{:?}", user1.login_count);
}

위 코드는 user1을 이전에 정의했던 구조체 User을 사용하여 데이터를 저장한 후, 저장된 값들을 순서대로 보여준다. 이처럼 특정 구조체를 바탕으로 데이터가 저장된 변수들을 인스턴스(instance)라고 부른다. 인스턴스를 생성하기 위해서는 구조체 필드 각각의 저장할 값들을 키:쌍 형태로 입력하면 된다. 만약 인스턴스에 저장된 특정 필드의 값만 지정하고 싶다면 인스턴스이름.필드이름 형태로 변수를 불러올 수 있다.

그렇다면 인스턴스에 저장된 데이터 값들을 수정할 수 있을까? 이 또한 인스턴스를 생성할 때 mut를 추가해주면 가능하다. 아래의 코드를 실행해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

fn main() {
let mut user1 =User {
username: String::from("USER1"),
email: String::from("user1@gmail.com"),
active: true,
login_count: 1,
};

user1.username = String::from("DEVAnythinG");

println!("{:?}", user1.username);
println!("{:?}", user1.email);
println!("{:?}", user1.active);
println!("{:?}", user1.login_count);
}

위 코드는 user1.username의 값이 “DEVAnythinG” 으로 수정되어 프린트 된다. 만약 user1을 정의할 때 mut을 추가하지 않는다면 변경된 값을 할당할 수 없다는 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
error[E0594]: cannot assign to `user1.username`, as `user1` is not declared as mutable
--> test.rs:16:5
|
9 | let user1 =User {
| ----- help: consider changing this to be mutable: `mut user1`
...
16 | user1.username = String::from("DEVAnythinG");
| ^^^^^^^^^^^^^^ cannot assign

error: aborting due to previous error

For more information about this error, try `rustc --explain E0594`.

인스턴스 정의에 mut을 추가하는 방법은 인스턴스 내의 모든 필드 값들을 가변으로 저장된다. 그렇다면 특정 필드 값만 가변으로 변경할 수 있을까? 그것은 불가능하다. 결론은 다음과 같다. “구조체의 인스턴스는 모든 필드값이 가변이거나 불변이다”

필드 초기화 단축 문법

필드 초기화 단축 문법이란 함수의 매개 변수와 구조체의 필드 이름이 같을 경우 사용할 수 있는 방법이다. 아래 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

fn build_user(username: String, email: String) -> User {
User {
username,
email,
active: true,
login_count: 1
}
}

fn main() {

let user1 = build_user(String::from("user"),String::from("user@gmail.com"));
println!("{:?}", user1.username);
println!("{:?}", user1.email);
println!("{:?}", user1.active);
println!("{:?}", user1.login_count);
}

위 코드에서 build_user()는 매개변수로 받는 username, email을 구조체의 필드 이름과 동일하게 작성하여 키:쌍 형태로 입력하지 않았다. 하지만 위 코드를 컴파일 후 실행해보면 결과는 아래와 같이 정상적으로 프린트 된다.

1
2
3
4
"user"
"user@gmail.com"
true
1

필드 갱신 단축 문법

이미 존재하는 인스턴스의 값을 참조하여 새로운 인스턴스를 만드려면 어떻게 할 수 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

fn main() {

let user1 = User {
username: String::from("user1"),
email: String::from("user1@gmail.com"),
login_count: 1,
active: true
};

let user2 = User {
username: String::from("user2"),
email: String::from("user2@gmail.com"),
login_count: user1.login_count,
active: user1.active
};

println!("{:?}", user2.username);
println!("{:?}", user2.email);
println!("{:?}", user2.active);
println!("{:?}", user2.login_count);
}

위 코드는 user2login_count, active 필드 값을 user1으로부터 복사한다. 하지만 만약 어떤 인스턴스로부터 복사할 필드 갯수가 100개라고 생각해보자.그러면 위 예시처럼 작성하면 코드가 매우 길어질 것이다. 이럴 때 바로 필드 갱신 단축 문법(..)을 사용하면 된다. 아래 코드는 이전 코드와 동일한 작동을 하지만 단축 문법을 사용하여 더 짧은 코드로 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct User {
username: String,
email: String,
login_count: u64,
active: bool
}

fn main() {

let user1 = User {
username: String::from("user1"),
email: String::from("user1@gmail.com"),
login_count: 1,
active: true
};

let user2 = User {
username: String::from("user2"),
email: String::from("user2@gmail.com"),
..user1
};

println!("{:?}", user2.username);
println!("{:?}", user2.email);
println!("{:?}", user2.active);
println!("{:?}", user2.login_count);
}

튜플 구조체

튜플과 유사하게 생긴 구조체를 정의할 수도 있다. 아래 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
struct RGB (i32, i32, i32);
struct LOC (i32, i32, i32);

fn main() {
let black = RGB(0,0,0);
let origin = LOC(0,0,0);
println!("{}",black.1);
println!("{}",origin.2);
}

위 코드는 RGB, LOC 두 개의 구조체를 사용한다. 정의된 black, origin은 똑같은 (0,0,0)의 값을 가지고 있지만 정의된 구조체가 다르기에 두 개가 동일한 데이터는 아니다. 따라서 RGB 인스턴스를 매개 변수로 받는 함수에 LOC 인스턴스를 사용할 수는 없다. 그 점을 제외하고는 튜플과 동일한 원리로 작동한다.

유사 유닛 구조체

유사 유닛 구조체는 필드가 하나도 없이 생성된 구조체이다. 사실 지금은 해당 구조체가 왜 필요한지 의문일 수 있지만 이후 트레이트를 공부할 때 더 자세히 다룰 것이다.

구조체 데이터의 소유권

이번 글 모든 예시에서 우리는 String 타입을 사용하였다. 구조체에서도 물론 &str과 같은 참조를 사용할 수 있지만 이를 위해서는 러스트 기능인 lifetime을 사용해야 한다. lifetime는 특정 범위 내에서 참조된 데이터의 유효성을 보장해준다. 이에 대한 부분도 뒤에서 더 자세히 다룰 것이다.