메서드
메서드는 함수와 비슷합니다. fn 키워드와 이름으로 선언하고, 매개변수와 반환값을 가질
수 있으며, 다른 곳에서 메서드가 호출되었을 때 실행할 코드를 포함합니다. 하지만 함수와
달리 메서드는 구조체(또는 6장과 18장에서 각각 다룰 enum, trait object)의 맥락 안에서
정의되며, 첫 번째 매개변수는 언제나 self 입니다. self 는 메서드가 호출되는
구조체 인스턴스를 나타냅니다.
메서드 문법
Rectangle 인스턴스를 매개변수로 받는 area 함수를 바꿔, 목록 5-13처럼
Rectangle 구조체 위에 정의된 area 메서드로 만들어 봅시다.
#[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()
);
}
Rectangle 구조체 위에 area 메서드 정의하기함수를 Rectangle 의 맥락 안에서 정의하려면 먼저 Rectangle 에 대한 impl
(implementation) 블록을 시작합니다. 이 impl 블록 안에 있는 모든 것은 Rectangle
타입과 연관됩니다. 그런 다음 area 함수를 impl 중괄호 안으로 옮기고, 첫 번째
매개변수(이 경우 유일한 매개변수)를 시그니처와 본문 모두에서 self 로 바꿉니다.
main 에서는 원래 area 함수에 rect1 을 인수로 넘겨 호출했지만, 이제는
메서드 문법 을 사용해 Rectangle 인스턴스 위에서 직접 area 메서드를 호출할
수 있습니다. 메서드 문법은 인스턴스 뒤에 점과 메서드 이름, 괄호, 그리고 필요한 인수를
붙이는 형태입니다.
area 의 시그니처에서는 rectangle: &Rectangle 대신 &self 를 사용합니다.
사실 &self 는 self: &Self 의 축약형입니다. impl 블록 안에서 Self 타입은
이 impl 블록이 구현하는 타입의 별칭이며, 여기서는 Rectangle 입니다. 메서드는
첫 번째 매개변수로 Self 타입의 self 라는 이름을 가져야 하므로, 러스트는 첫 번째
매개변수 자리에서 이 표현을 단순히 self 로 줄여 쓰도록 허용합니다. 또한 이
메서드가 Self 인스턴스를 빌려 쓴다는 사실을 나타내려면, rectangle: &Rectangle
에서 했던 것처럼 self 앞에 여전히 & 를 붙여야 한다는 점에 주의하세요. 메서드는
다른 매개변수와 마찬가지로 self 의 소유권을 가져갈 수도 있고, 여기처럼 불변으로
빌릴 수도 있으며, 가변으로 빌릴 수도 있습니다.
여기서 &self 를 선택한 이유는 함수 버전에서 &Rectangle 을 사용했던 이유와
같습니다. 소유권을 가져가고 싶지 않고, 구조체 안의 데이터를 읽기만 하고 싶기
때문입니다. 만약 메서드가 하는 일의 일부로 호출 대상 인스턴스를 바꾸고 싶다면,
첫 번째 매개변수로 &mut self 를 사용했을 것입니다. 첫 번째 매개변수로 그냥
self 를 사용해 인스턴스 소유권을 가져가는 메서드는 드문 편이며, 보통은 메서드가
self 를 다른 무언가로 변형시키고 난 뒤, 원래 인스턴스를 더 이상 호출자가 사용하지
못하게 하고 싶을 때 사용합니다.
메서드를 함수 대신 사용하는 가장 큰 이유는 메서드 문법을 제공하고, 메서드 시그니처마다
self 의 타입을 반복해서 적지 않아도 된다는 점 외에도, 코드 조직화 측면에 있습니다.
특정 타입 인스턴스로 할 수 있는 일들을 모두 하나의 impl 블록 안에 모아 둘 수 있으므로,
우리 라이브러리를 사용하는 사람이 Rectangle 관련 기능을 여기저기서 찾아다닐
필요가 없습니다.
구조체 필드 이름과 메서드 이름을 같게 지을 수도 있다는 점도 알아 두세요. 예를 들어
Rectangle 에 width 라는 이름의 메서드를 정의할 수 있습니다.
#[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);
}
}
여기서 우리는 width 필드 값이 0보다 크면 true, 0이면 false 를 반환하는
width 메서드를 만들기로 했습니다. 이렇게 같은 이름의 필드를 메서드 안에서 다른
목적으로 얼마든지 사용할 수 있습니다. main 에서 rect1.width 뒤에 괄호를
붙이면 러스트는 우리가 width 메서드를 의미한다고 압니다. 괄호가 없으면
width 필드를 의미한다고 압니다.
언제나 그런 것은 아니지만, 메서드 이름을 필드 이름과 같게 지을 때는 그 메서드가 대개 필드 값을 그대로 반환하고 다른 일은 하지 않도록 만들고 싶어 하는 경우가 많습니다. 이런 메서드를 getter 라고 합니다. 러스트는 다른 일부 언어와 달리 구조체 필드에 대한 getter를 자동으로 만들어 주지 않습니다. getter는 필드는 비공개로 두되 메서드는 공개로 만들어, 타입의 공개 API 일부로 읽기 전용 접근을 제공하고 싶을 때 유용합니다. 공개와 비공개가 무엇인지, 필드와 메서드를 어떻게 공개/비공개로 표시하는지는 7장에서 설명합니다.
-> 연산자는 어디에 있나요?
C와 C++에서는 메서드를 호출할 때 두 종류의 연산자를 사용합니다. 객체 자체에
메서드를 호출할 때는 ., 객체를 가리키는 포인터에 대해 메서드를 호출하고 먼저
포인터를 역참조해야 할 때는 -> 를 씁니다. 즉 object 가 포인터라면,
object->something() 은 (*object).something() 과 비슷합니다.
러스트에는 -> 연산자와 같은 것이 없습니다. 대신 러스트에는 자동 참조 및
역참조(automatic referencing and dereferencing) 라는 기능이 있습니다. 메서드
호출은 러스트에서 이 동작이 일어나는 몇 안 되는 곳 중 하나입니다.
동작 방식은 이렇습니다. object.something() 처럼 메서드를 호출하면, 러스트는
object 가 메서드 시그니처와 맞도록 자동으로 &, &mut, 또는 * 를 추가합니다.
다시 말해 다음 둘은 같은 의미입니다.
#![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);
}
첫 번째 형태가 훨씬 깔끔해 보이지요. 이런 자동 참조 동작이 가능한 이유는, 메서드에는
명확한 수신자(receiver), 즉 self 의 타입이 있기 때문입니다. 러스트는 수신자와
메서드 이름을 바탕으로, 그 메서드가 읽기(&self), 변경(&mut self), 소모(self)
중 무엇을 하는지 확실히 알 수 있습니다. 러스트가 메서드 수신자에 대해서는 대여를
암묵적으로 처리해 주는 사실이, 실제 사용에서 소유권을 훨씬 다루기 쉽게 만들어 주는
중요한 이유 중 하나입니다.
추가 매개변수를 가지는 메서드
메서드를 직접 하나 더 구현해 보면서 연습해 봅시다. 이번에는 Rectangle 인스턴스가
또 다른 Rectangle 인스턴스를 받아, 두 번째 Rectangle 이 self(첫 번째
Rectangle) 안에 완전히 들어갈 수 있으면 true, 그렇지 않으면 false 를 반환하게
하고 싶습니다. 즉, can_hold 메서드를 정의한 뒤에는 목록 5-14와 같은 프로그램을
작성할 수 있어야 합니다.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold 메서드 사용하기예상 출력은 다음과 같습니다. rect2 는 두 치수 모두 rect1 보다 작지만, rect3
는 rect1 보다 더 넓기 때문입니다.
Can rect1 hold rect2? true
Can rect1 hold rect3? false
우리는 메서드를 정의하려는 것이므로, 이것은 impl Rectangle 블록 안에 들어가게
됩니다. 메서드 이름은 can_hold 이고, 또 다른 Rectangle 에 대한 불변 대여를
매개변수로 받을 것입니다. 이 매개변수의 타입은 메서드를 호출하는 코드를 보면
알 수 있습니다. rect1.can_hold(&rect2) 는 &rect2 를 넘기는데, 이것은
Rectangle 인스턴스 rect2 에 대한 불변 대여입니다. 이는 자연스럽습니다. 우리는
rect2 를 읽기만 하면 되고(쓰기 위해서는 가변 대여가 필요합니다), 메서드 호출 후에도
main 이 rect2 의 소유권을 유지해 다시 사용할 수 있게 하고 싶기 때문입니다.
can_hold 의 반환값은 불리언이고, 구현은 self 의 너비와 높이가 각각 다른
Rectangle 의 너비와 높이보다 큰지 확인하면 됩니다. 목록 5-15처럼 목록 5-13의
impl 블록 안에 새 can_hold 메서드를 추가해 봅시다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Rectangle 인스턴스를 매개변수로 받는 can_hold 메서드를 Rectangle 에 구현하기이제 목록 5-14의 main 함수와 함께 이 코드를 실행하면 원하는 출력이 나옵니다.
메서드는 self 매개변수 뒤에 원하는 만큼의 추가 매개변수를 받을 수 있고, 그
매개변수들은 함수의 매개변수와 똑같이 동작합니다.
연관 함수
impl 블록 안에 정의된 모든 함수는 연관 함수(associated functions) 라고
부릅니다. impl 뒤에 적힌 타입과 연관되어 있기 때문입니다. 이들 중에는 첫 번째
매개변수로 self 를 갖지 않는 함수도 정의할 수 있습니다(따라서 메서드는 아닙니다).
그런 함수들은 작업하는 데 타입의 인스턴스가 필요하지 않기 때문입니다. 우리는 이미
이런 함수 하나를 사용해 봤습니다. String 타입에 정의된 String::from 함수입니다.
메서드가 아닌 연관 함수는 보통 새 구조체 인스턴스를 반환하는 생성자 역할에 자주
사용합니다. 이런 함수 이름은 흔히 new 이지만, new 는 특별한 이름도 아니고
언어에 내장된 것도 아닙니다. 예를 들어 한 개의 길이만 받아 그 값을 너비와 높이
둘 다로 사용함으로써 정사각형 Rectangle 을 더 쉽게 만들 수 있도록, square
라는 연관 함수를 제공한다고 해 봅시다. 이렇게 하면 같은 값을 두 번 적지 않아도
됩니다.
파일명: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
함수의 반환 타입과 본문에 있는 Self 키워드는 impl 키워드 뒤에 오는 타입의
별칭인데, 이 경우에는 Rectangle 입니다.
이 연관 함수를 호출하려면 구조체 이름과 함께 :: 문법을 사용합니다.
예를 들어 let sq = Rectangle::square(3); 처럼 씁니다. 이 함수는 구조체에 의해
네임스페이스가 나뉘어 있습니다. :: 문법은 연관 함수뿐 아니라 모듈이 만드는
네임스페이스에도 사용됩니다. 모듈은 7장에서 다룹니다.
여러 개의 impl 블록
각 구조체는 여러 개의 impl 블록을 가질 수 있습니다. 예를 들어 목록 5-15는,
각 메서드를 별도의 impl 블록에 넣은 목록 5-16의 코드와 동등합니다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
impl 블록으로 다시 쓰기여기서는 메서드를 굳이 여러 impl 블록으로 나눌 이유가 없지만, 문법적으로는 유효합니다.
10장에서 제네릭 타입과 트레이트를 다룰 때 여러 impl 블록이 유용한 경우를 보게 됩니다.
정리
구조체는 도메인에 의미가 있는 사용자 정의 타입을 만들 수 있게 해 줍니다. 구조체를
사용하면 서로 관련된 데이터 조각을 한데 묶어 두고, 각 조각에 이름을 붙여 코드가
무엇을 의미하는지 더 분명하게 만들 수 있습니다. impl 블록 안에서는 타입과 연관된
함수를 정의할 수 있고, 메서드는 그 연관 함수의 한 종류로서 구조체 인스턴스가 어떤
동작을 하는지를 표현하게 해 줍니다.
하지만 사용자 정의 타입을 만드는 방법이 구조체뿐인 것은 아닙니다. 이제 러스트의 enum 기능으로 넘어가, 도구 상자에 또 하나의 중요한 도구를 추가해 봅시다.