Rust의 관용적 콜백

C / C ++에서는 일반적으로 일반 함수 포인터를 사용하여 콜백을 수행하며 void* userdata매개 변수도 전달할 수 있습니다. 이 같은:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

Rust에서 이것을 수행하는 관용적 인 방법은 무엇입니까? 특히 내 setCallback()함수 는 어떤 유형을 취해야하며 어떤 유형이어야 mCallback합니까? 걸릴 Fn까요? 어쩌면 FnMut? 저장 Boxed합니까? 예는 놀랍습니다.



답변

짧은 답변 : 유연성을 극대화하기 위해 FnMut콜백 유형에 대한 콜백 setter 일반을 사용하여 콜백을 박스형 객체 로 저장할 수 있습니다 . 이에 대한 코드는 답변의 마지막 예에 나와 있습니다. 자세한 설명은 계속 읽으십시오.

“함수 포인터”: 콜백 fn

질문의 C ++ 코드와 가장 가까운 것은 콜백을 fn유형 으로 선언하는 것 입니다. C ++의 함수 포인터처럼 키워드로 fn정의 된 함수를 캡슐화합니다 fn.

type Callback = fn();

struct Processor {
    callback: Callback,
}

impl Processor {
    fn set_callback(&mut self, c: Callback) {
        self.callback = c;
    }

    fn process_events(&self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello world!");
}

fn main() {
    let p = Processor {
        callback: simple_callback,
    };
    p.process_events(); // hello world!
}

이 코드 Option<Box<Any>>는 함수와 관련된 “사용자 데이터”를 보유하기 위해 를 포함하도록 확장 될 수 있습니다 . 그래도 관용적 인 Rust는 아닙니다. 데이터를 함수와 연관시키는 Rust 방식 은 최신 C ++에서와 같이 익명의 클로저로 데이터를 캡처하는 것입니다 . 폐쇄가 아니기 때문에 fn, set_callback함수 객체의 다른 종류에 동의해야합니다.

일반 함수 객체로서의 콜백

Rust와 C ++ 클로저에서 동일한 호출 서명을 가진 클로저는 캡처 할 수있는 다른 값을 수용하기 위해 다른 크기로 제공됩니다. 또한 각 클로저 정의는 클로저 값에 대해 고유 한 익명 유형을 생성합니다. 이러한 제약으로 인해 구조체는 callback필드 유형의 이름을 지정할 수 없으며 별칭을 사용할 수도 없습니다.

구체적인 유형을 참조하지 않고 struct 필드에 클로저를 포함하는 한 가지 방법은 struct generic 을 만드는 것 입니다. 구조체는 전달하는 구체적인 함수 또는 클로저에 대한 콜백 유형과 크기를 자동으로 조정합니다.

struct Processor<CB>
where
    CB: FnMut(),
{
    callback: CB,
}

impl<CB> Processor<CB>
where
    CB: FnMut(),
{
    fn set_callback(&mut self, c: CB) {
        self.callback = c;
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn main() {
    let s = "world!".to_string();
    let callback = || println!("hello {}", s);
    let mut p = Processor { callback: callback };
    p.process_events();
}

이전과 마찬가지로 콜백의 새로운 정의는으로 정의 된 최상위 함수를 허용 할 수 fn있지만이 정의 || println!("hello world!")|| println!("{}", somevar). 이 때문에 프로세서는 userdata콜백을 동반 할 필요가 없습니다 . 의 호출자가 제공하는 클로저 set_callback는 환경에서 필요한 데이터를 자동으로 캡처하고 호출시 사용할 수있게합니다.

그러나 함께 거래는 무엇을 FnMut, 왜 그냥 Fn? 클로저는 캡처 된 값을 보유하고 있기 때문에, 클로저를 호출 할 때 Rust의 일반적인 변형 규칙을 적용해야합니다. 클로저가 보유한 가치로 무엇을하는지에 따라 세 가지 패밀리로 그룹화되며 각각 특성으로 표시됩니다.

  • Fn데이터를 읽기만하는 클로저이며 여러 스레드에서 여러 번 안전하게 호출 될 수 있습니다. 위의 두 가지 마감은 모두 Fn입니다.
  • FnMut데이터를 수정하는 클로저 (예 : 캡처 된 mut변수 에 쓰기) 입니다. 여러 번 호출 할 수도 있지만 병렬로 호출 할 수는 없습니다. ( FnMut다중 스레드에서 클로저를 호출하면 데이터 경합이 발생하므로 뮤텍스의 보호를 통해서만 수행 할 수 있습니다.) 클로저 객체는 호출자에 의해 변경 가능하게 선언되어야합니다.
  • FnOnce캡처 한 데이터 중 일부 를 소비하는 클로저입니다 . 예를 들어 캡처 된 값을 소유권을 갖는 함수로 이동합니다. 이름에서 알 수 있듯이 이들은 한 번만 호출 할 수 있으며 호출자가 소유해야합니다.

클로저를 받아들이는 객체의 유형에 대한 트레이 트 바운드를 지정할 때 다소 반 직관적으로 FnOnce는 실제로 가장 관대 한 것입니다. 제네릭 콜백 유형이 FnOnce특성을 충족해야한다고 선언하는 것은 말 그대로 모든 클로저를 허용한다는 것을 의미합니다. 그러나 그것은 가격과 함께 제공됩니다 : 그것은 소유자가 그것을 한 번만 호출 할 수 있다는 것을 의미합니다. 이후는 process_events()콜백을 여러 번 호출하도록 선택할 수 있으며, 방법으로 자체는 다음에 가장 관대 한 결합이며, 두 번 이상 호출 할 수 있습니다 FnMut. process_eventsmutating 으로 표시해야했습니다 self.

비 제네릭 콜백 : 함수 특성 객체

콜백의 일반적인 구현은 매우 효율적이지만 심각한 인터페이스 제한이 있습니다. 각 Processor인스턴스는 구체적인 콜백 유형으로 매개 변수화되어야합니다. 즉 Processor, 단일 콜백 유형 만 처리 할 수 ​​있습니다. 각 클로저에는 고유 한 유형이 있으므로 제네릭 Processorproc.set_callback(|| println!("hello"))다음에 오는을 처리 할 수 ​​없습니다 proc.set_callback(|| println!("world")). 두 개의 콜백 필드를 지원하도록 구조체를 확장하려면 전체 구조체를 두 가지 유형으로 매개 변수화해야하는데, 이는 콜백 수가 증가함에 따라 빠르게 다루기 어려워 질 것입니다. add_callback다른 콜백의 벡터를 유지 하는 함수 를 구현하기 위해 콜백 수가 동적이어야하는 경우 더 많은 유형 매개 변수를 추가하는 것은 작동하지 않습니다 .

유형 매개 변수를 제거하기 위해 우리는 특성에 기반한 동적 인터페이스를 자동으로 생성 할 수있는 Rust의 기능인 특성 객체를 이용할 수 있습니다 . 이것은 때때로 유형 삭제 라고도 하며 C ++ [1] [2] 에서 널리 사용되는 기술 입니다. Java 및 FP 언어의 다소 다른 용어 사용과 혼동하지 마십시오. C ++에 익숙한 독자는 구현하는 클로저 FnFn특성 객체 사이의 차이를 일반 함수 객체와 std::functionC ++의 값 사이의 차이와 동일 하게 인식 할 것 입니다.

특성 개체는 &연산자와 함께 개체를 빌려 특정 특성에 대한 참조로 캐스팅하거나 강제 함으로써 생성됩니다 . 이 경우 Processor콜백 객체를 소유해야하므로 차용을 사용할 수 없지만 기능적으로 트레이 트 객체와 동일한 힙 할당 Box<dyn Trait>(의 Rust에 해당하는 std::unique_ptr)에 콜백을 저장해야 합니다.

경우 Processor저장 Box<dyn FnMut()>, 더 이상 일반적인 할 필요가 없지만, set_callback 방법은 이제 일반적인를 받아 c를 통해 impl Trait인수 . 따라서 상태가있는 클로저를 포함하여 모든 종류의 콜 러블을 수락 할 수 있으며 Processor. set_callback허용 된 콜백의 유형이 Processor구조체에 저장된 유형에서 분리되기 때문에 to 일반 인수 는 프로세서가 허용하는 콜백의 종류를 제한하지 않습니다 .

struct Processor {
    callback: Box<dyn FnMut()>,
}

impl Processor {
    fn set_callback(&mut self, c: impl FnMut() + 'static) {
        self.callback = Box::new(c);
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello");
}

fn main() {
    let mut p = Processor {
        callback: Box::new(simple_callback),
    };
    p.process_events();
    let s = "world!".to_string();
    let callback2 = move || println!("hello {}", s);
    p.set_callback(callback2);
    p.process_events();
}

박스형 클로저 내부의 참조 수명

에서 허용되는 인수 'static유형에 바인딩 된 수명 은 컴파일러가에 포함 된 참조 를 확신 할 수있는 간단한 방법 입니다 . 이는 해당 환경을 참조하는 클로저 일 수 있으며 전역 값만 참조하므로 사용하는 동안 계속 유효합니다. 콜백. 그러나 정적 경계는 또한 매우 무겁습니다. 객체를 소유하는 클로저 (위에서 클로저를 만들어서 확인 했음 )는 허용하지만 로컬 환경을 참조하는 클로저는 거부합니다. 프로세서보다 수명이 길고 실제로 안전합니다.cset_callbackcmove

프로세서가 살아있는 한 살아있는 콜백 만 필요하므로 수명을 프로세서의 수명과 연결해야합니다. 이는 'static. 그러나 'static에서 제한되는 수명을 제거하면 set_callback더 이상 컴파일되지 않습니다. 이는 set_callback새 상자를 만들고로 callback정의 된 필드에 할당하기 때문 Box<dyn FnMut()>입니다. 정의가 boxed trait 객체에 대한 수명을 지정하지 않기 때문에은 'static암시되고 할당 'static은 허용되지 않는 수명 (명명되지 않은 콜백의 임의 수명에서까지)을 효과적으로 확장합니다 . 수정 사항은 프로세서에 대한 명시 적 수명을 제공하고 해당 수명을 상자의 참조와에서받은 콜백의 참조 모두에 연결하는 것입니다 set_callback.

struct Processor<'a> {
    callback: Box<dyn FnMut() + 'a>,
}

impl<'a> Processor<'a> {
    fn set_callback(&mut self, c: impl FnMut() + 'a) {
        self.callback = Box::new(c);
    }
    // ...
}

이러한 수명이 명시 적으로 지정되었으므로 더 이상을 사용할 필요가 없습니다 'static. 클로저는 이제 로컬 s객체를 참조 할 수 있습니다 . 즉 , 문자열이 프로세서 보다 오래 지속 되도록하기 위해 의 정의 앞에의 정의가 있는 move경우 더 이상이 될 필요가 없습니다 .sp