Funtion, Method and Associated Functions

Function은 특정 구조체나 인스턴스와 관계없이 독립적으로 정의되는데, Method는 구조체(또는 enum) 인스턴스의 상태를 읽거나 변경하는 동작을 구현하며, Assosiated Functions는 특정 타입과 연결되어 있지만 해당 타입의 인스턴스와는 독립적인 함수를 의미합니다.

1. 함수와 메소드의 차이점

가. 함수 (Function)

  • 특정 구조체나 인스턴스와 관계없이 독립적으로 정의됩니다.
  • fn 키워드를 사용해 선언하며, 어디서든 호출할 수 있습니다.
  • 호출 시 add(3, 5)처럼 함수 이름만 사용합니다.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

나. 메소드 (Method)

  • 구조체(struct), 열거형(enum) 등 특정 타입의 impl 블록 안에서 정의됩니다.
  • 첫 번째 파라미터로 반드시 self, &self, &mut self 중 하나를 받습니다.
  • 인스턴스를 통해서만 호출할 수 있으며, rect.area()처럼 점(.) 연산자를 사용합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

이처럼 Rust에서는 함수는 독립성, 메소드는 인스턴스와의 종속성이 가장 큰 차이입니다.

2. 함수와 메소드의 차이점

가. 함수(Function)

(1) 장점

  • 독립적이어서 다양한 곳에서 재사용하기 쉽고, 유닛 테스트가 간편합니다.
  • 간단한 연산이나 유틸리티 작업에 적합합니다.
  • 이름만으로 호출하므로 사용이 직관적입니다.

(2)

  • 구조체 등 데이터 타입과 직접적으로 연관된 행동을 묶어 표현하기 어렵습니다.
  • 많은 함수가 생기면 네임스페이스 충돌이나 코드의 구조적 복잡도가 증가할 수 있습니다.
  • 데이터 캡슐화와 추상화에서 약점이 있습니다.

나. 메소드(Method)

(1)장점

  • 데이터와 행위의 결합(캡슐화): 구조체의 로직과 동작을 impl 블록에 모아, 객체지향적 코드를 짤 수 있습니다.
  • 가독성: 인스턴스.메소드(…) 형태로 표현해 자연스럽고 읽기 쉬운 코드를 만듭니다.
  • 확장성: 타입별로만 동작을 추가하거나 오버라이드하는 데 용이합니다.

(2) 단점

  • 인스턴스가 있어야만 호출 가능합니다. 즉, 독립적으로 동작할 수 없는 경우가 많습니다.
  • 객체의 상태 변경이 필요하다면 &mut self 등으로 가변 참조를 써야 하므로, 소유권·빌림 규칙에 주의를 기울여야 합니다.
  • 단순 기능(예: 수학 연산 등)에는 불필요하게 복잡합니다.
구분장점단점
함수독립성, 재사용성, 간결함, 테스트 용이성데이터 캡슐화 불가, 네임스페이스 충돌 위험
메소드데이터와 로직 결합, 캡슐화, 가독성, 타입별 동작 확장 용이인스턴스 필요, 소유권·참조 규칙 신경써야, 불필요한 복잡성 가능

Rust에서는 함수는 독립적이고 범용적인 동작에, 메소드는 인스턴스와 연관된 행동에 사용하는 것이 일반적입니다.

2. 메소드와 연관 함수의 차이점

가. 메소드

  • 메서드는 반드시 첫 번째 인자로 self, &self 또는 &mut self를 받습니다.
  • 구조체 인스턴스를 통해 점(.) 연산자로 호출합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };  

    // 메소드 호출
    println!("사각형의 면적: {}", rect.area());
}

나. 연관 함수

  • 연관 함수는 self를 인자로 받지 않습니다.
  • 타입 이름을 통해 이중 콜론(::) 연산자로 호출합니다.
  • 주로 생성자 역할(예: new)로 사용되고, 구조체 등의 유틸리티 함수로도 사용됩니다.

(예제 1 – 생성자)

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    // 연관 함수 사용
    let rect = Rectangle::new(30, 50);
    println!("사각형의 면적: {}", rect.area());    
}

fn new의 반환 타입이 Rectange 구조체입니다.

(예제 2 – 유틸리티 함수)

struct Circle {
    radius: f64,
}

impl Circle {
    // 연관 함수 (유틸리티 함수)
    fn area(radius: f64) -> f64 {
        std::f64::consts::PI * radius * radius
    }
}

fn main() {
    // 연관 함수 호출 (유틸리티 함수)
    let circle_area = Circle::area(5.0);
    println!("Circle area: {}", circle_area);
}

메소드와 달리 Circle::area(5.0) 처럼 ::을 붙여 호출합니다.

위 코드를 실행하면
Circle area: 78.53981633974483라고 처리 결과가 제대로 표시되지만

“구조체 Circle안의 radius 필드가 사용되지 않았다”는 경고가 표시됩니다.


다시 말해 연관 함수의 인수 radius는 이름은 같지만 구조체의 radius가 아닙니다. 또한 Circle이란 인스턴스를 생성하지 않고도 원의 면적을 구했습니다.

3. 메소드와 연관 함수의 장단점

가. 메서드

(1) 사용 목적

  • 구조체(혹은 enum) 인스턴스의 **상태(필드)**를 읽거나 변경하는 동작을 구현합니다.
  • 객체의 행동을 정의하고, 객체의 데이터와 직접 상호작용합니다.

(2) 장점

  • 캡슐화: 인스턴스의 데이터를 안전하게 다루며, 객체의 상태를 직접 관리할 수 있습니다.
  • 가독성:rect.area()처럼 객체의 행동을 자연스럽게 표현할 수 있습니다.
  • 유지보수성: 객체의 동작이 구조체 내부에 모여 있어 코드 관리가 용이합니다.

나. 연관 함수

(1) 사용 목적

  • 구조체의 생성자 역할(예: new)이나, 인스턴스와 무관한 동작(예: 유틸리티 함수)을 구현합니다.
  • 인스턴스 없이 타입 자체에 대해 동작하는 기능을 제공합니다.

(2) 장점

  • 유연성: 인스턴스가 없어도 타입 이름으로 직접 호출할 수 있습니다(Rectangle::new() 등).
  • 명확성: 생성자나 특정 타입 관련 기능을 명확하게 분리할 수 있습니다.
  • 재사용성: 인스턴스와 무관한 기능을 여러 곳에서 활용할 수 있습니다.

다. 요약

  • 메서드는 객체의 상태와 밀접하게 연관된 동작을 구현하며, 객체지향적 설계와 데이터 캡슐화에 강점을 가집니다.
  • 연관 함수는 객체 생성이나 타입 자체와 관련된 기능을 제공하며, 인스턴스가 필요 없는 동작을 분리해 코드의 명확성과 재사용성을 높입니다.

Rust의 이터레이터(Iterator)

Rust에서 이터레이터(iterator)는 값을 순회(iterate)할 수 있도록 해주는 강력하고 유연한 추상화입니다. 이터레이터는 반복 가능한 값을 하나씩 꺼내면서 작업을 수행할 때 사용되며, 지연 평가(lazy evaluation)를 통해 성능도 뛰어납니다.

Rust에서 지연 평가(lazy evaluation)는 계산이 필요한 시점까지 연산을 연기하는 전략입니다. 즉, 값을 즉시 계산하는 대신, 해당 값이 필요할 때까지 계산을 미루는 방식입니다. 이를 통해 불필요한 계산을 방지하고 성능을 향상시킬 수 있습니다.

1. 기본 개념

Rust에서 이터레이터는 Iterator 트레잇을 구현한 타입입니다. 이 트레잇은 next() 메서드를 정의합니다.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • next()는 Option Enum 형식으로 반환하므로, Some(value)를 반환하다가, 더 이상 값이 없으면 None을 반환합니다.
  • for 루프는 내부적으로 이 next()를 호출하여 동작합니다.

가. 예제 1: 기본 사용

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.iter(); // 불변 참조로 이터레이터 생성

    while let Some(x) = iter.next() {
        println!("값: {x}");
    }
}
let v = vec![10, 20, 30]
  • v는 정수형 벡터입니다.
  • 즉, Vec 타입이고 [10, 20, 30]이라는 세 개의 요소를 가지고 있습니다.
let mut iter = v.iter()
  • v.iter()는 벡터 v의 각 요소에 대한 불변 참조 (&i32)를 반환하는 이터레이터를 생성합니다.
  • 즉, iter는 &10, &20, &30을 순서대로 반환할 준비가 된 상태입니다.
  • iter는 가변 변수로 선언되었습니다(mut) → .next()를 호출할 때 이터레이터 내부 상태를 바꾸기 때문입니다.

while let Some(x) = iter.next() { … }
  • .next()는 이터레이터에서 다음 값을 하나씩 꺼냅니다.
  • 반환값은 Option<&i32>입니다.
  • 값이 있으면 Some(&값)
  • 끝나면 None
  • while let Some(x)는 Some일 때 루프를 돌고, None이면 종료됩니다.
println!(“값: {x}”);
  • x는 &i32 타입이므로 10, 20, 30이 참조 형태로 출력됩니다.
  • println!은 참조를 자동으로 역참조해서 출력해주기 때문에 따로 *x를 쓰지 않아도 됩니다.

나. 예제 2: for 루프 사용

fn main() {
    let v = vec![1, 2, 3];

    for num in v.iter() {
        println!("num = {num}");
    }
}

while문과 아래가 다릅니다.

for num in v.iter()
  • v.iter()는 불변 참조 이터레이터를 생성합니다.
    • 즉, &1, &2, &3을 순서대로반환합니다.
  • for 루프는 이터레이터의 .next()를 자동으로 반복 호출하여 값을 하나씩 꺼냅니다.
  • 변수 num의 타입은 &i32입니다 (참조).
  • v.iter()는 벡터를 소유하지 않고 참만 하므로, v는 이루프 이후에도 여전히 사용 가능합니다.
  • println!(“num = {num}”);에 따라 1,2,3이 출력됩니다.

2. 소비(consuming) 어댑터

이터레이터를 사용해 데이터를 소모하는 메서드입니다.

  • .sum(): 합계 반환
  • .count(): 요소 개수
  • .collect(): 컬렉션으로 변환
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let sum: i32 = v.iter().sum();
    println!("합계: {}", sum);
}

여기서 특이한 점은 sum 다음에 i32라고 타입이 명시되어 있다는 점입니다.

: i32를 빼고 실행하면 “type annotations needed”라고 에러가 발생하는데,

iter().sum()의 반환 형식을 지정하지 않으면 에러 발생

sum이 &i32를 받아서 더할 수는 있지만, 반환값의 형식을 추론할 수 없기 때문에 안정성과 명확성을 추구하는 Rust가 에러를 발생시키는 것입니다.

3. 변형(transforming) 어댑터

이터레이터에서 새로운 이터레이터를 생성하지만 실제 순회는 for, collect 등으로 실행되기 전까지 지연 평가됩니다.

  • .map(): 각 요소를 변형
  • .filter(): 조건에 맞는 요소만 남김

가. map() 예제

fn main() {
    let v = vec![1, 2, 3, 4];

    let doubled: Vec<i32> = v.iter()
        .map(|x| x * 2)
        .collect();

    println!("{doubled:?}"); // [2, 4, 6, 8]
}
v.iter()
  • 벡터 v에 대해 불변 참조 이터레이터를 생성합니다.
  • 반환 타입은 impl Iterator<Item = &i32> → 각 요소는 &1, &2, &3, &4.
.map(|x| x * 2)
  • map은 이터레이터의 각 항목에 closure를 적용해 새로운 이터레이터를 만듭니다.
  • 여기서 x는 &i32이므로, x * 2는 실제로는 *x * 2와 같은 의미입니다.
  • 즉, 값은 다음과 같이 변합니다:
  • &1 → 1 * 2 → 2
  • &2 → 2 * 2 → 4
  • x는 &i32이기 때문에 직접 곱하려면 *x * 2라고 해야 하지만, Rust는 x * 2를 보면 자동으로 역참조(*x) 해주기 때문에 생략 가능합니다.

.collect()
  • 이터레이터 결과를 컨테이너 타입(여기서는 Vec)으로 수집합니다.
  • 이 부분에서 타입 추론이 불가능할 수 있기 때문에, doubled: Vec<i32>로 타입을 명시했습니다.

println!(“{doubled:?}”);
  • {:?}는 벡터를 디버그 형식으로 출력해줍니다.
  • 출력 결과는 [2, 4, 6, 8]입니다.

나. filter() 예제

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let even: Vec<_> = v.into_iter()
        .filter(|x| x % 2 == 0)
        .collect();

    println!("{:?}", even); // [2, 4]
}

다른 것은 같고 filter 부분만 다른데, x를 2로 나눴을 때 나머지가 0인 것을 % 연산자(나머지 연산자)로 구해서 해당되는 것만 collect(수집)하는 것입니다.

4. 소유권과 이터레이터

이터레이터는 다음 세 가지 방식으로 만들 수 있습니다.

메서드설명
.iter()불변 참조 이터레이터
.iter.mut()가변 참조 이터레이터
.into_iter()소유권을 이동하는 이터레이터
fn main() {
    let mut v = vec![1, 2, 3];

    for x in v.iter_mut() {
        *x *= 10;
    }

    println!("{:?}", v); // [10, 20, 30]
}

vector 변수 v를 가변 참조로 선언한 다음,
값을 하나씩 꺼내서 10을 곱한 다음 x에 저장하므로 v 변수가 변경됩니다.
이후 벡터 변수 v를 디버그 포맷으로 출력합니다.

5. 사용자 정의 이터레이터

직접 구조체에 Iterator 트레잇을 구현하여 사용자 정의 이터레이터를 만들 수도 있습니다.

struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}
가. 구조체 정의
struct Counter {
    count: usize,
}

usize 타입의 count 필드를 가진 Counter 구조체를 정의합니다.

나. Counter의 new 메소드 구현
impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

Counter 구조체의 fn new() 메소드를 정의하는데, count 필드의 값을 0으로 초기화합니다.

다. Counter를 위한 Iterator 트레잇 구현
impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

Counter 구조체를 위해 Iterator 트레잇을 구현하는데
fn new() 메소드에서 반환값은 Item인데 usize 형식이고,
매번 next()가 호출될 때 count를 1씩 증가시키고, 5보다 작거나 같으면 Some(count)을 반환하고, 그렇지 않으면 None을 반환하여 반복을 종료합니다.

라. 메인 함수
fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}

Count 구조체의 count 필드를 0으로 초기화한 후 c에 넣고
c를 1씩 증가시키면서 실행하는데 5까지만 출력하고 종료합니다.
Counter::new()로 만든 c는 Iterator를 구현하고 있기 때문에 for 루프에서 사용 가능합니다.

6. 정리

  • Iterator는 next()를 통해 요소를 하나씩 반환합니다.
  • .map, .filter 등은 지연 평가(lazy evaluation) 방식으로 동작합니다.
  • .collect()나 for 루프 등을 통해 실제로 실행됩니다.
  • 반복 가능한 자료형은 대부분 이터레이터를 제공합니다 (Vec, HashMap, Range 등)
  • Rust의 함수형 프로그래밍 스타일을 구성하는 핵심 개념입니다.