API 디자인의 내부 변경 가능성 남용?
C ++에 대한 나의 배경은 내부 가변성 에 대해 불편하게 만듭니다 . 아래 코드는이 주제에 대한 내 조사입니다.
차용 검사기의 관점에서 내부 상태가 조만간 변경 될 수있는 모든 단일 구조체에 대한 많은 참조를 다루는 것이 불가능하다는 데 동의합니다. 그것은 분명히 내부 가변성이 도움이 될 수있는 곳입니다.
또한, 장에 15.5 "RefCell 및 인테리어 가변성 패턴" 의 언어 프로그래밍 녹 의에 대한 예를 Messenger
특성과에 구현 MockMessenger
구조체 나 체계적으로 선호하는 것이 일반적인 API 디자인이라고 생각합니다 &self
이상 &mut self
심지어는 아주 명백한 경우 조만간 어떤 종류의 변경이 필수입니다. Messenger
메시지를 보낼 때 구현이 내부 상태를 변경하지 않는 방법은 무엇입니까? 예외 그냥과 일치하는 메시지를 인쇄하고 &self
있지만, 일반적인 경우는 아마, 버퍼링을 의미 할 수 내부 스트림의 일종, 쓰기 에러 플래그를 업데이트에서 구성 것이다 ...이 모든 것은 확실히 필요 &mut self
예로서,impl Write for File.
이 문제를 해결하기 위해 내부 가변성에 의존하는 것은 C ++에서, 단지 응용 프로그램의 다른 곳에서 우리가 ness 에 대해 일관성이 없었기 때문에 멤버를 const_cast
욕하거나 남용하는 것처럼 들립니다 (C ++ 학습자들에게 일반적인 실수).mutable
const
따라서 아래 예제 코드로 돌아가서 다음을 수행해야합니다.
- 사용
&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());
}
답변
이 문제를 해결하기 위해 내부 가변성에 의존하는 것은 C ++에서, 단지 응용 프로그램의 다른 곳에서 우리가 ness 에 대해 일관성이 없었기 때문에 멤버를
const_cast
욕하거나 남용하는 것처럼 들립니다 (C ++ 학습자들에게 일반적인 실수).mutable
const
이것은 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
또는 같은 것이 없습니다 .RefCell
UnsafeCell
그래서 아래의 예제 코드로 돌아 가면 [...]
의 의도 된 의미 에 따라 달라집니다 Thing
. Thing
채널 엔드 포인트 또는 파일처럼 공유되는 것이 본질 입니까? change_e
공유 (별칭) 참조에서 호출되는 것이 합리적 입니까? 그렇다면 내부 가변성을 사용하여 &self
. 가 Thing
주로 데이터의 컨테이너? 때로는 공유하고 때로는 독점하는 것이 합리적입니까? 그런 다음 Thing
내부 가변성을 사용하지 않아야하며 필요한 경우 라이브러리 사용자가 공유 돌연변이를 처리하는 방법을 결정하도록해야합니다.
또한보십시오
- 변수 이름 앞과 ":"뒤에 "mut"을 두는 것의 차이점은 무엇입니까?
- Rust의 타입 시그니처에 변수의 가변성이 반영되지 않는 이유는 무엇입니까?
- Rust의 셀 및 참조 횟수 유형에 대한 전체적인 설명이 필요합니다.
¹ 실제로 C ++ 에는 Rust의 참조와 유사하게 포인터가 작동하도록하는 기능이 있습니다. 거의. restrict
C ++의 비표준 확장이지만 C99의 일부입니다. Rust의 공유 ( &
) 참조는 const *restrict
포인터와 같고 배타적 ( &mut
) 참조는 비 const
*restrict
포인터 와 같습니다 . C ++에서 restrict 키워드는 무엇을 의미합니까?를 참조하십시오 .
C ++에서 의도적으로 restrict
(또는 __restrict
등) 포인터를 마지막으로 사용한 것이 언제입니까? 그것에 대해 생각하지 마십시오. 대답은 "절대"입니다. restrict
일반 포인터보다 더 적극적인 최적화를 가능하게하지만 앨리어싱에 대해 극도로주의해야하고 컴파일러가 도움을 제공하지 않기 때문에 올바르게 사용하기가 매우 어렵습니다. 그것은 기본적으로 거대한 풋건이며 거의 아무도 그것을 사용하지 않습니다. C ++에서 restrict
사용하는 방식을 널리 사용하는 것이 가치 const
가 있도록하려면 포인터가 어떤 시간에 다른 포인터를 별칭으로 지정할 수 있는지 함수에 주석을 달 수 있어야하며, 포인터가 따라 올 수있는시기에 대한 몇 가지 규칙을 만들어야합니다. 그리고 각 함수에서 규칙이 준수되고 있는지 확인하는 컴파일러 패스가 있습니다. 일종의 .. 검사기처럼.