API 디자인의 내부 변경 가능성 남용?

Aug 19 2020

C ++에 대한 나의 배경은 내부 가변성 에 대해 불편하게 만듭니다 . 아래 코드는이 주제에 대한 내 조사입니다.

차용 검사기의 관점에서 내부 상태가 조만간 변경 될 수있는 모든 단일 구조체에 대한 많은 참조를 다루는 것이 불가능하다는 데 동의합니다. 그것은 분명히 내부 가변성이 도움이 될 수있는 곳입니다.

또한, 장에 15.5 "RefCell 및 인테리어 가변성 패턴" 의 언어 프로그래밍 녹 의에 대한 예를 Messenger특성과에 구현 MockMessenger구조체 나 체계적으로 선호하는 것이 일반적인 API 디자인이라고 생각합니다 &self이상 &mut self심지어는 아주 명백한 경우 조만간 어떤 종류의 변경이 필수입니다. Messenger메시지를 보낼 때 구현이 내부 상태를 변경하지 않는 방법은 무엇입니까? 예외 그냥과 일치하는 메시지를 인쇄하고 &self있지만, 일반적인 경우는 아마, 버퍼링을 의미 할 수 내부 스트림의 일종, 쓰기 에러 플래그를 업데이트에서 구성 것이다 ...이 모든 것은 확실히 필요 &mut self예로서,impl Write for File.

이 문제를 해결하기 위해 내부 가변성에 의존하는 것은 C ++에서, 단지 응용 프로그램의 다른 곳에서 우리가 ness 에 대해 일관성이 없었기 때문에 멤버를 const_cast욕하거나 남용하는 것처럼 들립니다 (C ++ 학습자들에게 일반적인 실수).mutableconst

따라서 아래 예제 코드로 돌아가서 다음을 수행해야합니다.

  • 사용 &mut self에서 (가 필수 아닌 경우에도, 컴파일러는 불평하지 않습니다) change_e()change_i()내가 저장된 정수의 값을 변경할 수 있다는 사실과 일치 유지하기 위해?
  • &self내가 실제로 저장된 정수의 값을 변경하더라도 내부 가변성이 허용하기 때문에 계속 사용 합니까?

이 결정은 구조체 자체에 국한 될뿐만 아니라이 구조체를 사용하여 응용 프로그램에서 표현할 수있는 것에 큰 영향을 미칩니다. 두 번째 솔루션은 공유 참조 만 포함되어 있기 때문에 확실히 많은 도움이 될 것입니다.하지만 Rust에서 기대하는 것과 일치합니다.

Rust API Guidelines 에서이 질문에 대한 답을 찾을 수 없습니다 . C ++ CoreGuidelines 와 유사한 다른 Rust 문서가 있습니까?

/*
    $ rustc int_mut.rs && ./int_mut
     initial:   1   2   3   4   5   6   7   8   9
    change_a:  11   2   3   4   5   6   7   8   9
    change_b:  11  22   3   4   5   6   7   8   9
    change_c:  11  22  33   4   5   6   7   8   9
    change_d:  11  22  33  44   5   6   7   8   9
    change_e:  11  22  33  44  55   6   7   8   9
    change_f:  11  22  33  44  55  66   7   8   9
    change_g:  11  22  33  44  55  66  77   8   9
    change_h:  11  22  33  44  55  66  77  88   9
    change_i:  11  22  33  44  55  66  77  88  99
*/

struct Thing {
    a: i32,
    b: std::boxed::Box<i32>,
    c: std::rc::Rc<i32>,
    d: std::sync::Arc<i32>,
    e: std::sync::Mutex<i32>,
    f: std::sync::RwLock<i32>,
    g: std::cell::UnsafeCell<i32>,
    h: std::cell::Cell<i32>,
    i: std::cell::RefCell<i32>,
}

impl Thing {
    fn new() -> Self {
        Self {
            a: 1,
            b: std::boxed::Box::new(2),
            c: std::rc::Rc::new(3),
            d: std::sync::Arc::new(4),
            e: std::sync::Mutex::new(5),
            f: std::sync::RwLock::new(6),
            g: std::cell::UnsafeCell::new(7),
            h: std::cell::Cell::new(8),
            i: std::cell::RefCell::new(9),
        }
    }

    fn show(&self) -> String // & is enough (read-only)
    {
        format!(
            "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
            self.a,
            self.b,
            self.c,
            self.d,
            self.e.lock().unwrap(),
            self.f.read().unwrap(),
            unsafe { *self.g.get() },
            self.h.get(),
            self.i.borrow(),
        )
    }

    fn change_a(&mut self) // &mut is mandatory
    {
        let target = &mut self.a;
        *target += 10;
    }

    fn change_b(&mut self) // &mut is mandatory
    {
        let target = self.b.as_mut();
        *target += 20;
    }

    fn change_c(&mut self) // &mut is mandatory
    {
        let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
        *target += 30;
    }

    fn change_d(&mut self) // &mut is mandatory
    {
        let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
        *target += 40;
    }

    fn change_e(&self) // !!! no &mut here !!!
    {
        // With C++, a std::mutex protecting a separate integer (e)
        // would have been used as two data members of the structure.
        // As our intent is to alter the integer (e), and because
        // std::mutex::lock() is _NOT_ const (but it's an internal
        // that could have been hidden behind the mutable keyword),
        // this member function would _NOT_ be const in C++.
        // But here, &self (equivalent of a const member function)
        // is accepted although we actually change the internal
        // state of the structure (the protected integer).
        let mut target = self.e.lock().unwrap();
        *target += 50;
    }

    fn change_f(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e)
        let mut target = self.f.write().unwrap();
        *target += 60;
    }

    fn change_g(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f)
        let target = self.g.get();
        unsafe { *target += 70 };
    }

    fn change_h(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g)
        self.h.set(self.h.get() + 80);
    }

    fn change_i(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g, h)
        let mut target = self.i.borrow_mut();
        *target += 90;
    }
}

fn main() {
    let mut t = Thing::new();
    println!(" initial: {}", t.show());
    t.change_a();
    println!("change_a: {}", t.show());
    t.change_b();
    println!("change_b: {}", t.show());
    t.change_c();
    println!("change_c: {}", t.show());
    t.change_d();
    println!("change_d: {}", t.show());
    t.change_e();
    println!("change_e: {}", t.show());
    t.change_f();
    println!("change_f: {}", t.show());
    t.change_g();
    println!("change_g: {}", t.show());
    t.change_h();
    println!("change_h: {}", t.show());
    t.change_i();
    println!("change_i: {}", t.show());
}

답변

7 trentcl Aug 19 2020 at 15:59

이 문제를 해결하기 위해 내부 가변성에 의존하는 것은 C ++에서, 단지 응용 프로그램의 다른 곳에서 우리가 ness 에 대해 일관성이 없었기 때문에 멤버를 const_cast욕하거나 남용하는 것처럼 들립니다 (C ++ 학습자들에게 일반적인 실수).mutableconst

이것은 C ++의 맥락에서 완전히 이해할 수있는 생각입니다. 정확하지 않은 이유는 C ++와 Rust가 가변성의 개념이 다르기 때문입니다.

어떤면에서 Rust의 mut키워드는 실제로 두 가지 의미를 가지고 있습니다. 패턴에서는 "변경 가능"을 의미하고 참조 유형에서는 "배타적"을 의미합니다. 차이 &self&mut self있는지 정말없는 self돌연변이 여부 만이 할 수 있는지 여부를 할 수 별칭 .

에서 Messenger예를 들어, 음, 처음의 너무 심각하게 고려하지하자; 시스템 디자인이 아닌 언어 기능을 설명하기위한 것입니다. 그러나 우리는 왜 &self사용 되는지 상상할 수 있습니다 : Messenger공유 되는 구조에 의해 구현된다는 것을 의미 하므로 서로 다른 코드 조각이 동일한 객체에 대한 참조를 보유 send하고 서로 조정하지 않고 이를 경보에 사용할 수 있습니다. 경우 send취할 것 인 &mut self하나가 될 수 있기 때문에,이 목적을 위해 쓸모가있을 것 &mut self한 번에 존재 참조. Messenger( Mutex또는 무언가 를 통해 내부 가변성의 외부 레이어를 추가하지 않고) 공유에 메시지를 보내는 것은 불가능합니다 .

반면에 모든 C ++ 참조와 포인터는 별칭을 지정할 수 있습니다 .¹ 따라서 Rust 용어 로 C ++의 모든 변경 가능성은 "내부"변경 가능성입니다! Rust에는 멤버 mutable가 없기 때문에 Rust는 C ++에서 와 동등 하지 않습니다 const(여기서 캐치 프레이즈는 "변이성은 유형이 아니라 바인딩의 속성"입니다). Rust const_cast 공유 &참조를 배타적 &mut참조 로 바꾸는 것은 타당하지 않기 때문에 원시 포인터에 대해서만와 동등합니다 . 반대로 C ++에는 모든 값이 이미 암시 적으로 뒤에 있기 때문에 Cell또는 같은 것이 없습니다 .RefCellUnsafeCell

그래서 아래의 예제 코드로 돌아 가면 [...]

의 의도 된 의미 에 따라 달라집니다 Thing. Thing채널 엔드 포인트 또는 파일처럼 공유되는 것이 본질 입니까? change_e공유 (별칭) 참조에서 호출되는 것이 합리적 입니까? 그렇다면 내부 가변성을 사용하여 &self. 가 Thing주로 데이터의 컨테이너? 때로는 공유하고 때로는 독점하는 것이 합리적입니까? 그런 다음 Thing내부 가변성을 사용하지 않아야하며 필요한 경우 라이브러리 사용자가 공유 돌연변이를 처리하는 방법을 결정하도록해야합니다.

또한보십시오

  • 변수 이름 앞과 ":"뒤에 "mut"을 두는 것의 차이점은 무엇입니까?
  • Rust의 타입 시그니처에 변수의 가변성이 반영되지 않는 이유는 무엇입니까?
  • Rust의 셀 및 참조 횟수 유형에 대한 전체적인 설명이 필요합니다.

¹ 실제로 C ++ 에는 Rust의 참조와 유사하게 포인터가 작동하도록하는 기능이 있습니다. 거의. restrictC ++의 비표준 확장이지만 C99의 일부입니다. Rust의 공유 ( &) 참조는 const *restrict포인터와 같고 배타적 ( &mut) 참조는 비 const *restrict포인터 와 같습니다 . C ++에서 restrict 키워드는 무엇을 의미합니까?를 참조하십시오 .

C ++에서 의도적으로 restrict(또는 __restrict등) 포인터를 마지막으로 사용한 것이 언제입니까? 그것에 대해 생각하지 마십시오. 대답은 "절대"입니다. restrict일반 포인터보다 더 적극적인 최적화를 가능하게하지만 앨리어싱에 대해 극도로주의해야하고 컴파일러가 도움을 제공하지 않기 때문에 올바르게 사용하기가 매우 어렵습니다. 그것은 기본적으로 거대한 풋건이며 거의 아무도 그것을 사용하지 않습니다. C ++에서 restrict사용하는 방식을 널리 사용하는 것이 가치 const가 있도록하려면 포인터가 어떤 시간에 다른 포인터를 별칭으로 지정할 수 있는지 함수에 주석을 달 수 있어야하며, 포인터가 따라 올 수있는시기에 대한 몇 가지 규칙을 만들어야합니다. 그리고 각 함수에서 규칙이 준수되고 있는지 확인하는 컴파일러 패스가 있습니다. 일종의 .. 검사기처럼.