메서드 문법
메서드 (method) 는 함수와 유사합니다.
fn 키워드와 함수명으로 선언하고, 매개변수와 반환 값을 가지며,
다른 어딘가로부터 호출될 때 실행됩니다.
하지만 메서드는 함수와 달리 구조체 컨텍스트에 정의되고 (열거형이나
트레이트 객체 안에 정의되기도 하며, 이는 각각 6장,
17장에서 알아보겠습니다),
첫 번째 매개변수가 항상 self 라는 차이점이 있습니다.
self 매개변수는 메서드를 호출하고 있는 구조체 인스턴스를 나타냅니다.
메서드 정의하기
기존의 Rectangle 매개변수를 갖던 area 함수를 수정하여
예제 5-13처럼 Rectangle 구조체에 정의된
area 메서드로 바꿔봅시다.
파일명: src/main.rs
#[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() ); }
예제 5-13: Rectangle 구조체에
area 메서드 정의하기
Rectangle의 컨텍스트에 함수를 정의하기 위해서, Rectangle에 대한 impl
(implementation, 구현) 블록을 만드는 것으로 시작합니다. 이 impl 블록 내의
모든 것은 Rectangle 타입과 연관됩니다. 그런 다음 area 함수를 impl의
중괄호 안으로 옮기고 함수 시그니처의 첫 번째 매개변수를 (이번 경우는 유일한
매개변수군요) self로 변경하고 본문 내의 나머지 모든 부분도 변경합니다.
그리고 main 함수 내에서는 rect1을 인수로 전달하여 area 함수를 호출했었는데,
그 대신 메서드 문법 (method syntax) 을 사용해 Rectangle 인스턴스의 area
메서드를 호출할 수 있습니다. 메서드 문법은 차례대로 인스턴스, 점,
메서드명, 괄호 및 인수로 구성됩니다.
area 시그니처에서는 rectangle: &Rectangle 대신 &self를 사용했습니다.
&self는 실제로는 self: &Self를 줄인 것입니다. impl 블록 내에서
Self는 impl 블록의 대상이 되는 타입의 별칭입니다. 메서드는
Self 타입의 self라는 이름의 매개변수를 첫 번째 매개변수로 가져야
하는데, 그렇게 해야 첫 번째 매개변수 자리에 적어 넣은 self 형태의 축약형을
사용할 수 있습니다. rectangle: &Rectangle에서 그랬던 것처럼 Self이 메서드가
Self의 인스턴스를 빌려온다는 것을 나타내기 위해서는 self 축약형 앞에
&를 계속 붙여둘 필요가 있음을 주목하세요. 메서드는 다른 매개변수가 그런 것처럼
self의 소유권을 가져올 수도, 지금처럼 self를 불변으로 빌려올 수도,
가변으로 빌려올 수도 있습니다.
여기서 &self를 선택한 이유는 기존의 함수 버전에서 &Rectangle을
사용했던 이유와 같습니다: 지금 원하는 것이 소유권을 가져오는 것도,
데이터를 쓰는 것도 아닌, 데이터를 읽는 것뿐이니까요. 만약 메서드에서
작업 중 호출한 인스턴스를 변경하고 싶다면, 첫 번째 매개변수로
&mut self를 사용하면 됩니다. self라고만 작성하여 인스턴스의 소유권을
가져오도록 만드는 일은 거의 없습니다; 이러한 기법은 보통 해당 메서드가
self를 다른 무언가로 변환하고 그 이후에는 원본 인스턴스의 사용을 막고자
할 때 사용됩니다.
함수 대신 메서드를 사용하는 주된 이유는 메서드 구문을 제공하고
모든 메서드 시그니처 내에서 self 타입을 반복할 필요가 없다는 것 외에도
코드를 더 조직적으로 만들기 위해서입니다. 향후 우리가 제공한 라이브러리를
사용할 사람들이 Rectangle의 기능과 관련된 코드를 라이브러리 곳곳에서
찾아내야 하는 것보다는, 하나의 impl 블록 내에 이 타입의 인스턴스로
할 수 있는 모든 것들을 모아두는 것이죠.
구조체의 필드 이름과 동일한 이름의 메서드를 만들 수도 있습니다.
예를 들면, width라는 중복된 이름의 메서드를 Rectangle 상에 정의할 수
있지요:
파일명: src/main.rs
#[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) 라고 부르는데, 러스트는 다른 언어들처럼 구조체 필드에 대한 게터를 자동으로 만들지 않습니다. 필드를 비공개 (private) 로 하고 메서드는 공개 (public) 로 만들 수 있기 때문에 게터는 어떤 타입의 공개 API로써 어떤 필드에 대해 읽기 전용 접근만 허용하고자 하는 경우 유용합니다. 공개와 비공개가 무엇이고, 필드 혹은 메서드를 공개 혹은 비공개로 만드는 방법에 대해서는 7장에서 다루겠습니다.
->연산자는 없나요?C 나 C++ 언어에서는 메서드 호출에 두 종류의 연산자가 쓰입니다. 어떤 객체의 메서드를 직접 호출할 땐
.를 사용하고, 어떤 객체의 포인터를 이용해 메서드를 호출하는 중이라서 역참조가 필요할 땐->를 사용하죠. 예를 들어서object라는 포인터가 있다면,object->something()는(*object).something()로 나타낼 수 있습니다.이
->연산자와 동일한 기능을 하는 연산자는 러스트에 없습니다. 러스트에는 자동 참조 및 역참조 (automatic referencing and dereferencing) 라는 기능이 있고, 메서드 호출에 이 기능이 포함되어 있기 때문입니다.여러분이
object.something()코드로 메서드를 호출하면, 러스트에서 자동으로 해당 메서드의 시그니처에 맞도록&,&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); }첫 번째 표현이 더 깔끔하죠? 이런 자동 참조 동작은 메서드의 수신자(
self의 타입을 말합니다)가 명확하기 때문에 가능합니다. 수신자와 메서드명을 알면 해당 메서드가 인스턴스를 읽기만 하는지(&self), 변경하는지(&mut self), 소비하는지(self) 러스트가 알아낼 수 있거든요. 또한 메서드의 수신자를 러스트에서 암묵적으로 빌린다는 점은 실제로 소유권을 인체공학적으로 만드는 중요한 부분입니다.
더 많은 매개변수를 가진 메서드
Rectangle 구조체의 두 번째 메서드를 구현하여 메서드 사용법을 연습해 봅시다.
이번에 만들 새로운 메서드는 다른 Rectangle 인스턴스를 받아서,
self 사각형 (첫 번째 Rectangle) 면적 내에 두 번째 사각형 Rectangle
인스턴스가 완전히 들어갈 수 있다면 true를 반환하고, 못 들어가면 false를
반환할 겁니다. 즉, can_hold 메서드를 정의하여 다음 예제 5-14에 나오는
프로그램이 작동하도록 만들겠습니다:
파일명: src/main.rs
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));
}
예제 5-14: 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) 에서 Rectangle 인스턴스
rect2의 불변 참조자인 &rect2를 전달했으니까요.
rect2를 읽을 수만 있으면 되기 때문에
가변으로 빌려올 필요도 없으며,
rect2의 소유권을 main에 남겨두지 않을 이유도 없으니,
논리적으로도 불변 참조자가 가장 적합합니다.
반환 값은 부울린 타입이 될 것이고, self의 너비, 높이가
다른 Rectangle의 너비, 높이보다 큰지 검사하는 형태로 구현될 겁니다.
그럼 이제 예제 5-13의 impl 블록에 can_hold 메서드를 새로 추가해 보죠!
추가하고 난 모습은 다음 예제 5-15와 같습니다:
파일명: src/main.rs
#[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)); }
예제 5-15: 다른 Rectangle 인스턴스를
매개변수로 갖는 can_hold 메서드를 Rectangle에 구현
이제 예제 5-14에서 본
main 함수를 실행하면 원하던 결과가 나올 겁니다.
이처럼 메서드는 self 매개변수 뒤에 여러 매개변수를 가질 수 있으며,
이 매개변수는 함수에서의 매개변수와 동일하게 기능합니다.
연관 함수
impl 블록 내에 구현된 모든 함수를 연관 함수 (associated function) 라고
부르는데, 이는 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-16은 예제 5-15에 나온 코드를 변경해
impl 블록을 여러 개로 만든 모습입니다:
#[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)); }
예제 5-16: 예제 5-15를 여러 impl
블록을 사용하도록 재작성하기
위 코드에서는 impl 블록을 여러 개로 나눠야 할 이유가 전혀 없지만,
impl 블록을 반드시 하나만 작성해야 할 필요는 없음을 보여드리기 위한 예시로 작성했습니다.
여러 impl 블록을 유용하게 사용하는 경우는 제네릭 타입 및 트레이트 내용을 다루는 10장에서 보실 수 있습니다.
정리
구조체를 사용하면 도메인에 의미 있는 커스텀 타입을 만들 수 있습니다.
또한, 구조체를 사용함으로써 서로 관련 있는 데이터들을 하나로 묶어 관리할 수
있으며, 각 데이터 조각에 이름을 붙여 코드를 더 명확하게 만들 수 있습니다.
impl 블록 내에서는 여러분의 타입에 대한 연관 함수들, 그리고 연관 함수의
일종인 메서드를 정의하여 여러분의 구조체 인스턴스가 가질 동작들을 명시할 수
있습니다.
하지만 구조체로만 커스텀 타입을 만들 수 있는 건 아닙니다. 다음에는 열거형을 배워서 여러분이 쓸 수 있는 도구를 하나 더 늘려보도록 합시다.