이번에는 Rust의 struct에 대해서 알아보자.
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
struct Student {
id: String,
score: i8,
}
impl Student {
fn new(id: &str, score: i8) -> Self {
Self {
id: String::from(id),
score,
}
}
fn print(&self) {
println!("{}: {}", self.id, self.score);
}
fn inc_score(&mut self, amt: i8) {
self.score += amt;
}
}
impl Display for Student {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}: {}", self.id, self.score)
}
}
pub fn run() {
let mut s = Student::new("a11111111", 75);
s.print();
s.inc_score(10);
println!("{s}");
let mut v = [s, Student::new("a22222222", 99)]; // s moved into v
for x in &mut v {
x.inc_score(5);
}
// need &v in order not to destroy the students
for x in &v {
println!("{x}")
}
println!("{v:?}");
}
전체 코드는 위와 같으며, 개요는 아래와 같다.
이 코드는 학생 정보를 관리하기 위한 간단한 Rust 프로그램이다. Student
라는 구조체를 정의하고, 그 구조체를 다루기 위한 여러 메서드와 트레잇을 구현하고 있다.
-
Student
구조체id
: 학생의 아이디를 저장.String
타입으로 소유권을 가짐.score
: 학생의 점수를 저장.i8
타입.
-
Student
구조체에 대한 메서드new
: 새Student
인스턴스를 생성.print
: 학생의 정보를 출력.inc_score
: 학생의 점수를 증가시킴.
-
Display
트레잇 구현- 학생 정보를 좀 더 편리하게 출력할 수 있게
fmt
메서드를 정의.
- 학생 정보를 좀 더 편리하게 출력할 수 있게
-
run
함수- 학생 정보를 만들고, 조작하는 예제 코드.
s
라는 이름의Student
인스턴스를 생성하고 정보를 출력한 후, 점수를 증가시킨다.v
라는 배열에 두 명의 학생을 저장한다. 여기서s
가v
로 이동(move)한다.for
루프를 통해 모든 학생의 점수를 증가시킨다.- 마지막으로
for
루프를 통해 학생 정보를 출력한다.
참고: 코드에는 몇 가지 오류가 있을 수 있다. 예를 들어, println!("{s}");
, println!("{x}")
등은 변수를 제대로 출력하지 않을 것이다. 이 부분은 println!("{}", s);
, println!("{}", x);
등으로 수정되어야 한다.
이제 하나하나 살펴보자
use std::fmt::{Display, Formatter, Result};
use std::fmt::{Display, Formatter, Result};
이 코드는 Rust의 표준 라이브러리에서 fmt
모듈 안에 있는 Display
, Formatter
, Result
라는 이름의 타입이나 트레잇을 현재의 스코프로 가져오는 것이다.
-
Display
: 이 트레잇은 타입을 사용자 친화적인 형태로 출력할 수 있게 해준다. 여기서는Student
구조체에 대한Display
트레잇을 구현하고 있다. -
Formatter
: 이것은Display
나Debug
트레잇의fmt
메서드에서 사용되는 타입이다. 이를 통해 실제로 어떻게 출력할지를 지정한다. -
Result
: 이는fmt
메서드에서 사용되는Result
타입을 나타내고, 출력이 성공적이었는지 혹은 실패했는지를 나타낸다. 여기서는Result
가 성공적일 경우 아무것도 반환하지 않고(Ok(())
), 실패할 경우 에러를 반환한다.
이렇게 선언함으로써, 이 세 가지 타입이나 트레잇을 std::fmt::Display
이라고 전체 경로를 쓰지 않고, 그냥 Display
라고 짧게 사용할 수 있게 된다.
#[derive(Debug)]
#[derive(Debug)]
는 Rust에서 특별한 어노테이션이다. 이 어노테이션을 통해 Debug
트레잇을 자동으로 구현할 수 있다. Debug
트레잇은 타입을 디버그하기 쉬운 형태로 출력할 수 있게 해준다. 이를 구현하면, {:?}
형식 지정자를 사용해서 해당 타입의 인스턴스를 디버그 형식으로 출력할 수 있다.
예를 들어,
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
println!("{:?}", point);
}
이 코드는 Point { x: 1, y: 2 }
라는 디버그 출력을 생성할 것이다.
이 어노테이션을 사용하면, 디버그 출력을 수동으로 구현할 필요가 없어져서 코드를 더 간결하게 유지할 수 있다.
{:?}
{:?}
는 Rust의 형식 지정자 중 하나다. 이 형식 지정자를 사용하면 Debug
트레잇을 구현한 타입의 값을 디버그 형식으로 출력할 수 있다. 이것은 주로 디버깅을 위한 목적으로 사용되며, 이 형식 지정자를 사용하려면 해당 타입이 Debug
트레잇을 구현해야 한다.
예를 들어:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
println!("{:?}", point);
}
여기서 println!("{:?}", point);
코드는 Point
구조체의 인스턴스 point
를 디버그 형식으로 출력하게 해준다. 실제 출력 결과는 Point { x: 1, y: 2 }
가 될 것이다.
이렇게 {:?}
를 사용하면 변수나 오브젝트의 내부 상태를 쉽게 확인할 수 있어 디버깅이 편리해진다.
그렇다면 {:?}를사용하지 않으면 어떻게 되는가?
{:?}
를 사용하지 않으면 Debug
트레잇을 따르지 않은 일반적인 형식으로 출력해야 한다. 이때 해당 타입이 Display
트레잇을 구현하고 있다면, 그냥 {}
형식 지정자를 사용해서 출력할 수 있다.
예를 들어,
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
// println!("{}", point); // 이 코드는 에러를 발생시킨다. Point가 Display 트레잇을 구현하지 않았기 때문이다.
}
만약 Display
트레잇도 구현하지 않은 상태에서 {}
를 사용하려고 하면, 컴파일 에러가 발생한다. 그래서 {:?}
는 디버깅을 위한 간단한 방법으로 자주 사용되는데, 이 형식 지정자를 사용하면 타입이 Debug
트레잇을 구현하고 있다는 전제 하에 쉽게 내부 상태를 볼 수 있다.
struct
struct
는 Rust에서 사용하는 복합 데이터 타입 중 하나다. 여러 개의 데이터를 묶어서 하나의 타입으로 다룰 수 있게 해준다. 기본적인 문법은 다음과 같다.
struct 이름 {
필드1: 타입1,
필드2: 타입2,
//...
}
예를 들어, 2차원 공간의 점을 나타내는 Point
라는 구조체를 만들어보자.
struct Point {
x: i32,
y: i32,
}
이제 Point
타입의 변수를 만들어보자.
let p1 = Point { x: 1, y: 2 };
필드에 접근하려면 .
연산자를 사용한다.
println!("x: {}, y: {}", p1.x, p1.y);
mut
키워드를 사용해서 가변 변수로 만들 수도 있다.
let mut p2 = Point { x: 0, y: 0 };
p2.x = 5;
p2.y = 10;
구조체는 메서드도 가질 수 있다. impl
블록 안에서 구현하면 된다.
impl Point {
// 구조체의 필드를 읽는 메서드
fn get_x(&self) -> i32 {
self.x
}
// 구조체의 필드를 변경하는 메서드
fn set_x(&mut self, x: i32) {
self.x = x;
}
}
let mut p3 = Point { x: 0, y: 0 };
p3.set_x(5);
println!("x: {}", p3.get_x());
여기서 &self
는 메서드가 자신의 인스턴스에 접근할 수 있도록 하는 것이고, &mut self
는 가변 참조로 자신을 변경할 수 있게 한다.
이처럼 struct
를 사용하면 관련 있는 여러 개의 데이터를 묶어서 관리할 수 있다. 코드의 가독성과 재사용성도 높아진다.
struct Student
는 학생 정보를 저장하는 곳이고, impl Student
는 그 정보를 어떻게 다룰지를 정의한다고 생각하면 이해하기 쉽다. 둘 다 이름이 Student
인 이유는, 이 메서드들이 Student
타입과 밀접하게 연관되어 있기 때문이다. 이런 식으로 Rust에서는 타입과 그 타입에 대한 메서드들을 명확하게 연결할 수 있다.
imple Student
impl Student
는 Rust에서 Student
라는 구조체(struct)에 대한 구현(implementation)을 시작하겠다는 의미다. 이 블록 내에서는 Student
타입의 인스턴스가 사용할 수 있는 메서드들을 정의한다.
예를 들어, 아래 코드에서는 Student
구조체에 대한 두 개의 메서드, new
와 print_name
,을 정의한다.
"구조체에 대한 구현을 시작한다"는 거의 "구조체에 기능을 추가한다"라고 이해하면 된다. Rust에서는 구조체(struct)를 만들면, 그것은 단순히 데이터를 담는 역할만 한다. 하지만 프로그래밍을 할 때 단순히 데이터를 저장하는 것 이상의 기능이 필요하다. 예를 들어, 학생(Student) 구조체가 있다고 치면, 학생의 이름을 출력하거나, 성적을 업데이트하는 등의 기능을 구조체에 추가하고 싶을 것이다.
이런 추가적인 기능은 Rust에서 impl
블록을 통해 정의된다. impl
블록 안에서는 해당 구조체에 적용되는 함수(메서드)를 정의할 수 있다.
예를 들어, 아래와 같이 Student
라는 구조체에 print_name
이라는 기능을 추가할 수 있다:
struct Student {
name: String,
}
impl Student {
fn print_name(&self) {
println!("학생의 이름은 {}이다.", self.name);
}
}
이렇게 impl
블록을 사용하면, Student
타입의 변수(오브젝트)가 print_name
이라는 메서드를 사용할 수 있게 된다.
struct Student {
name: String,
}
impl Student {
fn new(name: &str) -> Self {
Self {
name: String::from(name),
}
}
fn print_name(&self) {
println!("{}", self.name);
}
}
여기서 impl Student
블록 내부에 있는 new
함수는 Student
타입의 새로운 인스턴스를 생성하고 반환한다. print_name
함수는 인스턴스의 name
필드를 출력한다.
이렇게 impl
블록을 사용해서 특정 타입에 메서드를 추가할 수 있다. 이 방식으로 OOP(Object-Oriented Programming)의 일부 기능을 Rust에서도 구현할 수 있다.
fn new(id: &str, score: i8) -> Self {
Self {
id: String::from(id),
score,
}
}
fn new(id: &str, score: i8) -> Self
함수는 Student
구조체의 인스턴스를 생성하고 초기화하는 생성자 함수다. Rust에서는 new
라는 이름의 함수를 생성자로 사용하는 것이 관례다, 하지만 이건 필수적인 것은 아니다. 생성자의 목적은 객체를 적절하게 초기화하는 것이다.
파라미터
id: &str
: 학생 ID를 나타내는 문자열 슬라이스score: i8
: 학생의 점수를 나타내는 8비트 정수
반환값
-> Self
:Self
는 현재 구조체 타입, 즉Student
를 나타낸다. 함수는Student
타입의 새로운 인스턴스를 반환한다.
함수 본문
Self { id: String::from(id), score, }
: 여기서Self
는Student
를 의미하며,Student
구조체의 필드를 초기화한다.id: String::from(id)
:&str
타입의id
를String
타입으로 변환해서id
필드에 저장한다.score
: 입력받은score
를 그대로score
필드에 저장한다.
이 함수를 사용하면 Student
구조체의 인스턴스를 쉽게 생성하고 초기화할 수 있다. 예를 들어:
let student = Student::new("a11111111", 75);
이렇게 하면 ID가 "a11111111"이고 점수가 75인 Student
인스턴스가 생성된다.
Self
Self
는 Rust에서 자주 사용되는 키워드로, 현재 구현 블록(impl
)이나 트레이트(trait
)에 연관된 타입을 참조한다. 다시 말해, Self
는 구현 블록이나 트레이트가 적용되는 타입을 대체한다.
어떻게 사용하는가?
- 메서드 내에서 현재 인스턴스를 반환할 때: 이렇게 하면 동일한 타입의 새로운 인스턴스를 쉽게 생성하거나 반환할 수 있다.
impl MyClass { fn new() -> Self { // 생성자 구현 } }
- 메서드 체인에서: 메서드가 현재 인스턴스의 뮤터블 참조나 소유권을 반환할 때, 메서드 체인을 만들기 위해
Self
를 사용할 수 있다.impl MyClass { fn set_x(self, x: i32) -> Self { // ... self } }
- 다형성을 활용할 때: 트레이트를 구현할 때
Self
를 사용하면, 그 트레이트를 구현하는 모든 타입에서 동일한 코드를 재사용할 수 있다.trait MyTrait { fn clone(&self) -> Self; }
예시
struct Circle {
radius: f64,
}
impl Circle {
// Self를 반환 타입으로 사용
fn new(radius: f64) -> Self {
Self { radius }
}
// Self를 매개변수 타입으로 사용
fn is_larger(&self, other: &Self) -> bool {
self.radius > other.radius
}
}
여기에서 Self
는 Circle
타입을 의미한다. new
함수는 Self
를 반환하므로 Circle
타입의 인스턴스를 반환하고, is_larger
메서드는 &Self
즉 &Circle
타입의 레퍼런스를 매개변수로 받는다.
이렇게 Self
를 사용하면 코드를 더 유연하고 재사용 가능하게 만들 수 있다.
Self
를 사용하지 않으면 그 대신에 구조체의 이름을 명시적으로 적어줘야 한다. 예를 들어, Student
라는 구조체가 있고 그 안에 new
라는 함수가 있다고 해보자. Self
를 사용하면 코드는 이렇게 보일 것이다.
impl Student {
fn new(id: &str, score: i8) -> Self {
Self {
id: String::from(id),
score,
}
}
}
Self
를 사용하지 않으면 Student
라고 명시적으로 적어줘야 한다.
impl Student {
fn new(id: &str, score: i8) -> Student {
Student {
id: String::from(id),
score,
}
}
}
두 코드는 같은 일을 하지만, Self
를 사용하면 좀 더 유연하다. 예를 들어, 나중에 Student
라는 이름을 Pupil
로 바꾸려고 한다면, Self
를 사용한 코드는 그대로 두고 구조체의 이름만 바꾸면 되지만, Self
를 사용하지 않은 코드에서는 new
함수 내에서도 Student
를 Pupil
로 바꿔줘야 한다.
쉽게 말해, Self
를 쓰면 나중에 코드를 수정할 때 편리하다. 코드를 한 번만 바꾸면 되니까.
Self와 self의 차이
Self
와 self
둘 다 Rust에서 특별한 의미를 갖는데, 다음과 같이 다르다.
-
Self
: 대문자로 시작하는 이 키워드는 현재 구현(implementation)하고 있는 타입 자체를 가리킨다. 예를 들어,impl Student
블록 내에서Self
는Student
타입을 의미한다.Self
는 주로 반환 타입이나 연관 타입 등에서 사용된다. -
self
: 소문자로 시작하는 이 키워드는 인스턴스 메서드에서 현재 객체의 인스턴스를 가리킨다. 즉,self
를 통해 객체 내부의 필드나 메서드에 접근할 수 있다.
예시를 들면,
struct Student {
name: String,
}
impl Student {
// 여기서 Self는 Student를 가리킨다.
fn new(name: &str) -> Self {
Self {
name: String::from(name),
}
}
// 여기서 self는 Student의 인스턴스를 가리킨다.
fn print_name(&self) {
println!("{}", self.name);
}
}
여기서 new
함수의 반환 타입으로 Self
를 사용했고, print_name
메서드에서는 self
를 통해 인스턴스의 name
필드에 접근했다.
간단하게 말해, Self
는 '이 타입 자체'라는 의미이고, self
는 '이 인스턴스'라는 의미다.
fn print(&self) {
println!("{}: {}", self.id, self.score);
}
이 print
함수는 Student
구조체의 인스턴스(오브젝트)에 대한 정보를 출력하는 역할을 한다. 함수 안에서는 self.id
와 self.score
를 사용해 구조체의 id
와 score
필드 값을 출력한다.
&self
는 함수의 파라메터로서, 이 함수가 어떤 Student
오브젝트에 대해 작동해야 하는지 알려주는 역할을 한다. 여기서 &self
가 의미하는 것은 "이 함수를 호출하는 Student
오브젝트의 참조(references)를 가져와라"라는 뜻이다.
왜 참조(&
)를 사용하냐면, 오브젝트의 실제 데이터를 복사하지 않고 그냥 참조만 하기 위해서다. 이렇게 하면 메모리를 효율적으로 사용할 수 있고, 성능도 좋아진다.
즉, &self를 파라메터로 사용하면 Student 구조체에서 정의된 id와 score의 참조를 가져와서 함수 내에서 사용할 수 있다. 이렇게 하면 메모리를 새로 할당해서 복사하는 것이 아니라 기존에 있는 데이터를 참조해서 작업을 하기 때문에 더 효율적이다.
예를 들어:
let s = Student { id: String::from("1"), score: 90 };
s.print();
이런 식으로 print()
함수를 호출할 때, s
오브젝트는 자동으로 &self
에 전달된다. 즉, 함수 내부에서는 self.id
는 "1"이 되고, self.score
는 90이 된다. 그리고 이 정보를 화면에 출력한다.
fn inc_score(&mut self, amt: i8) {
self.score += amt;
}
이 코드는 Student
구조체의 score
필드를 변경하기 위한 함수다. 함수는 amt
라는 i8 타입의 파라미터를 받아, 그 값을 score
에 더한다.
&mut self
는 무엇을 의미하는지에 대해 알아보자. 일반적으로 &self
는 불변 참조를 가져오지만, &mut self
는 가변 참조를 가져온다. 즉, 이 함수 내에서 Student
구조체의 필드값을 변경할 수 있다는 것을 의미한다. 이렇게 해야 self.score += amt;
와 같이 score
필드를 변경할 수 있다.
간단히 말하면, &mut self
를 사용하면 함수 안에서 Student
구조체의 데이터를 변경할 수 있다. 그래서 inc_score
함수를 호출할 때는 Student
구조체의 가변 참조를 전달해야 한다.
impl Display for Student {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}: {}", self.id, self.score)
}
}
impl Display for Student
라는 문장은 Student
구조체에 대해서 Display
트레이트를 구현한다는 뜻이다. 트레이트는 러스트에서 인터페이스와 비슷한 개념으로, 어떤 특정 기능이나 동작을 정의한다. Display
트레이트는 객체를 문자열로 표현하는 방법을 정의하고, 이를 위해서는 fmt
라는 메서드를 반드시 구현해야 한다.
문법적으로 보면, impl
키워드는 '구현한다(Implement)'의 줄임말이다. 이 키워드 뒤에 구현할 트레이트의 이름(Display
)과 대상이 되는 타입(Student
)를 for
키워드와 함께 적어준다.
impl Display for Student {
// 트레이트에 정의된 메서드 구현
}
impl Display for Student
이렇게 하면 Student
객체에 대해서 Display
트레이트의 메서드를 사용할 수 있게 된다. 즉, Student
객체를 문자열로 쉽게 변환할 수 있게 되는 것이다.
이 구조를 사용하면 Student
객체를 println!
이나 format!
같은 문자열과 관련된 러스트의 표준 라이브러리 함수에서 사용할 수 있게 된다. 예를 들면, println!("{}", student_instance);
와 같이 사용할 수 있다.
이 코드는 Student
구조체에 대해 Display
트레이트를 구현한다. Display
트레이트는 표준 라이브러리에 정의된 인터페이스로, 객체를 사람이 읽을 수 있는 형태의 문자열로 변환하는 역할을 한다. fmt
라는 함수를 구현해야 이 트레이트를 사용할 수 있다.
fn fmt(&self, f: &mut Formatter) -> Result
이 함수는 다음과 같은 의미를 가진다:
&self
: 이 함수가 작동할Student
구조체의 불변 참조f: &mut Formatter
: 문자열로 변환할 내용을 저장할Formatter
타입의 가변 참조. 가변이므로 함수 내에서 변경이 가능하다.&mut Formatter
는std::fmt::Formatter
타입의 가변 참조를 가져오는 것을 의미한다.Formatter
는 출력 형식을 지정하거나 조작하는 데 사용되는 구조체이다.&mut
키워드는 이Formatter
객체를 가변 참조로 가져온다는 것을 의미하는데, 가변 참조는 해당 객체를 변경할 수 있다는 것을 나타낸다.- 실제로
Formatter
객체가 가지고 있는 정보와 메서드들은 여러 가지이다. 예를 들어, 출력될 문자열의 정렬, 너비, 채우기 문자 등을 지정하는 설정 정보를 가질 수 있다. 이런 정보는 문자열을 출력하는write!
매크로나 다른 출력 함수에서 사용될 수 있다. fmt
메서드 내에서는 이Formatter
객체를 사용해 실제 출력을 수행한다. 예를 들어,write!(f, "{}: {}", self.id, self.score)
라인은Formatter
객체f
를 사용하여 실제로 문자열을 만들고 그 결과를 반환한다. 여기서f
는 가변 참조이므로 내부 상태가 변경될 수 있다.- 요약하면,
&mut Formatter
는 출력을 조작하거나 설정하는 데 사용되는Formatter
객체의 가변 참조를 가져온다. 이를 통해 해당 함수나 메서드에서 출력 형식을 조절할 수 있다.
-> Result
: 함수의 반환 타입. 여기서는std::fmt::Result
타입을 의미하며, 문자열 변환 작업이 성공하면Ok(())
를, 실패하면Err
를 반환한다.
write!(f, "{}: {}", self.id, self.score)
코드는 Formatter
의 가변 참조 f
에 Student
구조체의 id
와 score
를 포함한 문자열을 작성한다. 작성 작업이 성공하면 Ok(())
를 반환하고, 실패하면 Err
를 반환한다. 이 반환값은 fmt
함수의 반환값으로도 사용된다.
pub fn run() {
let mut s = Student::new("a11111111", 75);
s.print();
s.inc_score(10);
println!("{s}");
let mut v = [s, Student::new("a22222222", 99)]; // s moved into v
for x in &mut v {
x.inc_score(5);
}
// need &v in order not to destroy the students
for x in &v {
println!("{x}")
}
println!("{v:?}");
}
이 run()
함수는 Student
구조체를 다루는 여러 동작을 보여주고 있다.
let mut s = Student::new("a11111111", 75);
: 새로운Student
객체를 만든다. 객체는 가변(mut)이다.s.print();
:Student
객체의print()
메서드를 호출해 정보를 출력한다.- Rust에서 메서드를 호출할 때 첫 번째 인자가 자동으로
self
,&self
, 또는&mut self
로 전달된다. 여기서self
는 메서드가 호출되는 오브젝트 자체를 의미한다.s.print()
를 호출하면 내부적으로는Student::print(&s)
처럼 작동한다. - 이런 방식은 다른 프로그래밍 언어에서도 흔히 볼 수 있는데, 예를 들어 Java에서
object.method()
형식으로 메서드를 호출하면, 메서드 내부에서는this
키워드를 사용해 오브젝트에 접근할 수 있다. Rust에서의self
는 이this
와 유사한 역할을 한다. - 따라서
s.print()
는 내부적으로&self
를s
로 자동으로 채워주므로 파라메터를 따로 전달하지 않아도 된다.
- Rust에서 메서드를 호출할 때 첫 번째 인자가 자동으로
s.inc_score(10);
:Student
객체의 점수를 10 증가시킨다.- 파라메터로 10을 받으면, amt 값을 10으로 업데이트하고, 내부적으로 self.score 값을 10 증가시키게 된다.
println!("{s}");
:Student
객체를 출력한다. 하지만 이 코드 보다는println!("{}", s);
로 작성하는 것이 좋다.let mut v = [s, Student::new("a22222222", 99)];
:Student
객체들을 가진 배열을 생성한다.s
객체는 이 배열로 이동(moved)한다.- 여기서는 소유권이 넘어가는 것이다.
- 여기서
let mut v = [s, Student::new("a22222222", 99)];
코드는 배열v
를 생성하고s
와 새로 생성된Student
오브젝트를 그 안에 넣는다. 이 때s
의 소유권이 배열v
로 이동(move)된다. - Rust에서는 데이터를 다룰 때 소유권(move)와 참조(borrow)라는 개념이 중요하다. 여기서는
s
의 소유권을v
에게 주어,v
가 이 오브젝트를 완전히 소유하게 된다. 그래서s
는 더 이상 사용할 수 없게 된다. - 특별한 이유가 물어진다면, 이것은 Rust의 메모리 안전성을 보장하는 방식 중 하나다. 소유권을 명확히 해서 어느 시점에서 메모리를 해제할지, 누가 데이터를 수정할 수 있는지 등을 명확하게 하는 것이다.
- 만약 이
s
오브젝트가 나중에 다시 필요하다면,clone
메서드를 사용해 데이터를 복사한 뒤, 복사된 데이터의 소유권을 넘길 수 있다. 예를 들면,let mut v = [s.clone(), Student::new("a22222222", 99)];
처럼 사용할 수 있다. 이렇게 하면s
의 데이터는 그대로 유지되면서, 그 복사본이v
로 이동한다.
for x in &mut v
: 배열에 있는 각Student
객체의 점수를 5씩 증가시킨다.for x in &v
: 배열에 있는 모든Student
객체를 출력한다.println!("{v:?}");
: 배열 전체를 디버깅 형식으로 출력한다.
for x in &mut v
에서 &mut v
를 사용한 이유는 배열 내의 각 Student
객체의 점수를 증가시키려고 하기 때문이다. x.inc_score(5);
라는 코드에서 inc_score
메서드는 &mut self
를 파라미터로 받으므로, 배열의 각 항목을 가변 참조로 가져와야 한다.
여기서
for x in &mut v {
x.inc_score(5);
}
// need &v in order not to destroy the students
for x in &v {
println!("{x}")
}
이 두 for문은 비슷해 보이지만 목적과 동작이 다르다.
-
for x in &mut v { ... }
: 이건 배열v
의 각 항목을 가변 참조로 가져와서 수정할 수 있다. 즉,x
는&mut Student
타입이 되고,inc_score(5)
를 호출하여 각 학생의 점수를 5점씩 높인다. -
for x in &v { ... }
: 이건 배열v
의 각 항목을 불변 참조로 가져온다. 즉,x
는&Student
타입이 되고, 오브젝트를 변경할 수 없다. 단지 읽기만 가능하다. 그래서println!("{x}")
를 사용하여 각 학생의 정보를 출력한다.
요약하자면, 첫 번째 for문은 배열의 오브젝트를 수정하기 위한 것이고, 두 번째 for문은 배열의 오브젝트를 읽기 위한 것이다. 첫 번째에서는 &mut v
를 사용하여 배열의 각 오브젝트를 수정할 수 있게 하고, 두 번째에서는 &v
를 사용하여 오브젝트를 안전하게 읽을 수 있게 한다.
println!("{v:?}")
에서 {:?}
는 디버깅 형식으로 출력하라는 의미다. Student
구조체에 #[derive(Debug)]
어노테이션이 있으므로, Rust는 Student
객체를 디버깅 형식으로 자동으로 출력할 수 있다. 이렇게 하면 배열 v
내의 모든 Student
객체들이 디버깅 형식으로 출력된다.
댓글 없음:
댓글 쓰기