메서드(method) 는 함수와 유사합니다.
fn 키워드와 함수명으로 선언하고, 매개변수와 반환 값을 가지며,
다른 어딘가로부터 호출될 때 실행됩니다.
하지만 메서드는 함수와 달리 구조체 컨텍스트에 정의되고(열거형이나
트레이트 객체 안에 정의되기도 하며, 이는 각각 5장,
16장에서 알아보겠습니다),
첫 번째 매개변수가 항상 self 라는 차이점이 있습니다.
self 매개변수는 메서드를 호출하고 있는 구조체 인스턴스를 나타냅니다.
기존의 Rectangle 매개변수를 갖던 area 함수를 수정하여
예제 5-13처럼 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() );}
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
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로써 어떤
필드에 대해 읽기 전용 접근만 허용하고자 하는 경우 유용합니다. 공개와 비공개가
무엇이고, 필드 혹은 메서드를 공개 혹은 비공개로 만드는 방법에 대해서는
6장에서 다루겠습니다.
C 나 C++ 언어에서는 메서드 호출에 두 종류의 연산자가 쓰입니다.
어떤 객체의 메서드를 직접 호출할 땐 .를 사용하고,
어떤 객체의 포인터를 이용해 메서드를 호출하는 중이라서 역참조가 필요할 땐 ->를 사용하죠.
예를 들어서 object라는 포인터가 있다면,
object->something()는 (*object).something()로 나타낼 수 있습니다.
이 -> 연산자와 동일한 기능을 하는 연산자는 러스트에 없습니다.
러스트에는 자동 참조 및 역참조(automatic referencing and dereferencing) 라는 기능이 있고,
메서드 호출에 이 기능이 포함되어 있기 때문입니다.
여러분이 object.something() 코드로 메서드를 호출하면,
러스트에서 자동으로 해당 메서드의 시그니처에 맞도록 &, &mut, *를 추가합니다.
즉, 다음 두 표현은 서로 같은 표현입니다.
# #[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에 나오는
프로그램이 작동하도록 만들겠습니다.
예제 5-14: can_hold 메서드를 작성하고 나면
작동할 코드
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));}
rect2는 너비와 높이 둘 다 rect1보다 작지만,
rect3는 rect1 보다 너비가 넓으므로
출력은 다음과 같을 겁니다.
Can rect1 hold rect2? trueCan 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와 같습니다.
예제 5-15: 다른 Rectangle 인스턴스를
매개변수로 갖는 can_hold 메서드를 Rectangle에 구현
impl 블록 내에 구현된 모든 함수를 연관 함수(associated function) 라고
부르는데, 이는 impl 뒤에 나오는 타입과 모두 연관된 함수이기 때문입니다.
동작하는 데 해당 타입의 인스턴스가 필요하지 않다면 self를 첫 매개변수로
갖지 않는 (따라서 메서드가 아닌) 연관 함수를 정의할 수도 있습니다.
우리는 이미 String 타입에 정의되어 있는 String::from 함수처럼 이런
종류의 함수를 사용해 봤습니다.
메서드가 아닌 연관 함수는 구조체의 새 인스턴스를 반환하는 생성자로
자주 활용됩니다. 이 함수들은 보통 new라고 명명되는데, new는
이 언어에서 특별한 이름 혹은 키워드가 아닙니다. 생성자의 예시로,
Rectangle로 정사각형을 만들 때 너비, 높이에 같은 값을 두 번 지정하지 않고
치수 하나를 매개변수로 받아서 해당 치수로 너비와 높이를 설정하는
연관 함수 square를 만들어서, 더 간단하게 정사각형을 만드는 기능을
제공해보겠습니다.
위 코드에서는 impl 블록을 여러 개로 나눠야 할 이유가 전혀 없지만,
impl 블록을 반드시 하나만 작성해야 할 필요는 없음을 보여드리기 위한 예시로 작성했습니다.
여러 impl 블록을 유용하게 사용하는 경우는 제네릭 타입 및 트레이트 내용을 다루는 10장에서 보실 수 있습니다.
구조체를 사용하면 도메인에 의미 있는 커스텀 타입을 만들 수 있습니다.
또한, 구조체를 사용함으로써 서로 관련 있는 데이터들을 하나로 묶어 관리할 수
있으며, 각 데이터 조각에 이름을 붙여 코드를 더 명확하게 만들 수 있습니다.
impl 블록 내에서는 여러분의 타입에 대한 연관 함수들, 그리고 연관 함수의
일종인 메서드를 정의하여 여러분의 구조체 인스턴스가 가질 동작들을 명시할 수
있습니다.
하지만 구조체로만 커스텀 타입을 만들 수 있는 건 아닙니다.
다음에는 열거형을 배워서 여러분이 쓸 수 있는 도구를 하나 더 늘려보도록 합시다.