구조체는 struct 또는 structure 라고 쓰이며, 여러 값을 가질 수 있는 타입이다.
튜플과 비슷하지만 구조체에서는 값이 의미하는 바가 명확하도록 각 데이터에 이름을 붙이며, 순서가 없다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
구조체 안의 data들은 필드(field)라고 불린다. 구조체를 정의한 후 사용하려면 각 필드에 대한 구체적인 값을 지정하여 해당 구조체의 인스턴스(instance)를 만든다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
구조체의 특정 값을 얻으려면 점 표기법(dot natation)을 써야한다. 인스턴스가 변경 가능하면 점 표기법을 사용하고 특정 필드에 할당하여 값을 변경할 수 있다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut 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");
}
전체 인스턴스는 변경 가능해야한다. 러스트는 특정 필드만 변경 가능한 것을 허용하지 않는다. 그리고 구조체의 새 인스턴스를 함수 본문의 마지막 표현식으로 구성하여 새 인스턴스를 암시적으로 반환할 수 있다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
email: email처럼 필드 이름과 변수 이름이 같을 경우 shorthand로 쓸 수 있다. 이를 field init shorthand라고 한다.
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
구조체 업데이트 구문을 사용하여 다른 인스턴스의 값을 포함하는 새로운 인스턴스를 만들 수 있다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
struct update구문을 사용하여 user1의 active와 username, sign_in_count의 값을 가지는 user2가 생성되었다. 또는 ... 구문을 사용하여 명시적으로 설정되지 않은 나머지 필드의 값을 설정하는 방법도 있다. 순서는 중요하지 않다!
let user2 = User {
email: String::from("another@example.com"),
..user1
};
이 예제에서 user1은 user2를 만든 후에는 사용할 수 없다. user1의 username이 String의 소유권이 user2의 username으로 옮겨졌기 때문이다. email과 username의 타입과는 달리, active와 sign_in_count의 타입은 Copy trait로 구현된 타입이므로 상관이 없다.
Rust는 tuple structs라고 하는 튜플과 비슷한 구조체도 지원한다. 튜플 구조체는 필드이름이 없고 필드 유형만 있다. 튜플 구조체는 전체 튜플에 이름을 지정하고 튜플을 다른 튜플과 다른 유형으로 만들고 싶을 때나 각 필드의 이름을 지정하는 것이 많거나 중복될 때 유용하다.
튜플 구조체를 정의하려면 struct키워드와 구조체 이름으로 시작하고 그 뒤에 튜플의 유형을 정의한다. 다음은 예시이다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
필드가 없는 구조체를 정의할 수도 있다. 이를 단위 유사 구조체(unit-like structs)라고 한다. 단위 유사 구조체는 특정 유형에 대한 특성을 구현해야 하지만 유형 자체에 저장하려는 데이터가 없을 때 유용할 수 있다. 다음은 예시이다.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
직사각형의 면적을 계산하는 프로그램을 작성해 보자.
fn main() {
let width = 30;
let height = 50;
println!("{}", area(width, height));
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
첫 번째 코드이다. 두 개의 변수를 사용한다. 튜플로 리팩토링을 해보자.
fn main() {
let rec = (30, 50);
println!("{}", area(rec));
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
두 번째 코드이다. 이제 area함수에 하나의 튜플만 전달한다. 구조체로 리팩토링을 해보자.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("{}", area(&rec));
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
세 번째 코드이다. rec의 소유권을 이전시키지 않고 borrow하기 위해서 함수 서명과 호출하는 곳에서 &를 붙인다.
Debug trait
또한 구조체는 다음 코드와 같이 출력하려는 경우 출력 값이 모호하기 때문에 오류가 발생한다.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
println!문의 중괄호 안에 :? 지정자를 넣으면 Debug라는 출력 형식을 사용하고 싶다는 뜻이다.
Debug 특성을 사용하면 개발자에게 유용한 방식으로(println!문이 표시할 수 있는 형식으로) 구조체를 인쇄할 수 있으므로 코드를 디버깅하는 동안 값을 볼 수 있다.
러스트는 디버깅 정보를 출력하는 기능을 포함하고 있지만, 구조체에서 해당 기능을 사용할 수 있도록 명시적으로 알려주어야 한다. 이를 위해 구조체 정의 바로 앞에 #[derive(Debug)]속성을 추가한다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
warning표시가 뜨긴 하지만 잘 실행된다. 더 큰 구조체가 있는 경우 읽기 쉬운 출력을 위해 {:#?}을 사용하는 경우도 있다. 아래는 {:#?}을 사용한 내용이다.
Debug형식을 사용하여 값을 인쇄하는 또 다른 방법은 dbg! 매크로를 사용하는 것이다. 매크로는 표현식의 소유권을 가져 오고 해당 표현식의 결과 값과 함께 코드에서 해당 매크로 호출이 발생한 파일 및 행 번호를 인쇄하고 소유권을 반환한다.
참고로 dbg!매크로를 호출하면 표준 출력 콘솔 스트림으로 출력되기 보다는 표준 오류 콘솔 스트림으로 출력된다.
아래는 dbg!매크로를 사용한 코드이다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
rect1의 width와 &rect1 값에 dbg!매크로를 사용하였더니 자세한 값이 출력되었다.
메소드 구문
메소드 구문(Method syntax)는 함수와 유사하다. fn키워드로 메서드를 선언하고, 매개 변수와 반환 값을 가질 수 있다. 함수와의 차이점은 메소드는 구조체나 enum, trait object의 context안에서 정의되며 첫 번째 매개변수는 항상 self구조체의 인스턴스를 나타낸다.
메소드를 정의해보자.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
area를 정의하기 위해서 Rectangle 앞에 impl(implementation)을 붙여준다. impl안의 모든 것들은 Rectangle과 연관이 있다는 뜻이다. area에서 아까는 매개 변수와 타입을 Rectangle: &Rectangle이라고 했지만 여기서는 &self라고 한다. &self는 사실 self: &self의 약어이다. 메소드는 소유권을 가지거나 변경 여부와 관계없이 borrow할 수 있다.
또한, 구조체의 필드 중 하나의 동일한 이름을 메서드에 지정하도록 선택할 수 있다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
종종 필드와 이름이 같은 메서드를 제공할 때 필드의 값만 반환하고 다른 작업은 하지 않기를 원하는 메서드를 getters라고 한다.
->(화살표) 오퍼레이터는 어디에 있나?
C와 C++에서 메소드를 호출하는 방법은 두 가지가 있다.
객체에서 직접 메소드를 호출하려는 경우에는 .을 쓰고, 객체에 대한 포인터에서 메소드를 사용하는 경우나 포인터를 먼저 역참조(dereference)해야 하는 경우에는 ->를 사용한다. 다르게 말하면 만약 object가 포인터라면 object->something()은 (*object).something()과 유사하다.
러스트는 -> 와 같은 연산자를 제공하지는 않지만, 메서드를 호출하는 경우에 실행되는 automatic referencing과 dereferencing이라는 특징을 가지고 있다.
object.something() 메소드를 호출하면 러스트는 자동으로 &,&mut, *을 추가시켜 object를 메소드의 서명과 일치시킨다. 이는 다음과 같다.
#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
이 automatic referencing behavior은 메서드에 self라는 명확한 수신자가 있기 때문에 동작한다.
Rectangle의 두번째 인스턴스가 첫번째 안에 완전히 들어갈 수 있으면 true를 반환하고, 아니면 false를 반환하는 프로그램을 작성해 보자.
두 Rectangle의 width와 height를 비교하는 can_hold라는 메소드를 만들어보자. 이 메소드는 매개변수를 변경할 수 없도록 Rectangle을 borrow할 것이다. 그렇기 때문에
fn can_hold(&self, other: &Rectangle) -> bool {
아래와 같이 &Rectangle이라는 참조구문을 이용해서 매개변수를 정의한다.
나는 사용자의 입력을 받아 두 개의 Rectangle 구조체를 만들어 비교하려고 한다. 코드는 다음과 같다.
use std::io;
fn main() {
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
let mut width1 = String::new();
let mut height1 = String::new();
let mut width2 = String::new();
let mut height2 = String::new();
println!("Enter rect1's width: ");
io::stdin()
.read_line(&mut width1)
.expect("Failed");
let width1: u32 = width1.trim().parse().expect("Please type a number!");
println!("Enter rect1's height: ");
io::stdin()
.read_line(&mut height1)
.expect("Failed");
let height1: u32 = height1.trim().parse().expect("Please type a number!");
println!("Enter rect2's width: ");
io::stdin()
.read_line(&mut width2)
.expect("Failed");
let width2: u32 = width2.trim().parse().expect("Please type a number!");
println!("Enter rect2's height: ");
io::stdin()
.read_line(&mut height2)
.expect("Failed");
let height2: u32 = height2.trim().parse().expect("Please type a number!");
let rect1 = Rectangle {
width: width1,
height: height1,
};
let rect2 = Rectangle {
width: width2,
height: height2,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect2 hold rect1? {}", rect2.can_hold(&rect1));
}
한눈에 봐도 더러운 코드다. 실행 결과는 다음과 같다.
impl블록 안에 정의되어 있는 모든 함수들은 associated function이라고 불린다. 그리고 associated function을 self 매개변수 없이 정의할 수 있다. associated function의 예로는 String 타입에 String::from 기능이 있다.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
예를 들어 다음과 같은 associated function이 있다고 가정해보자. 이 함수를 사용하려면 :: 구문을 구조체의 이름과 함께 사용해야 한다.
let sq = Rectangle::square(3);
또한, 각 구조체는 여러 impl블록을 가질 수 있다.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
'러스트' 카테고리의 다른 글
패키지, 크레이트, 모듈(1) (0) | 2022.03.11 |
---|---|
열거형과 Match (0) | 2022.03.11 |
소유권(2) (0) | 2022.03.08 |
소유권(1) (0) | 2022.03.07 |
제어문 (0) | 2022.03.04 |