API設計における内部の可変性の乱用?
C ++での私の経歴は、内部の可変性について私を不快にさせます。以下のコードは、このトピックに関する私の調査です。
借用チェッカーの観点から、内部状態が遅かれ早かれ変更される可能性のあるすべての構造体に関する多くの参照を処理することは不可能であることに同意します。それは明らかに、内部の可変性が役立つ場合があります。
また、章の15.5「RefCellとインテリア可変性パターン」の言語プログラミング錆、例えば約Messenger
トレイトとのその実装MockMessenger
構造体は、私は体系的に好むように、それは共通のAPIの設計であることを思わせる&self
以上&mut self
でもそれそのかなり明白場合遅かれ早かれ、ある種の可変性が必須になります。Messenger
メッセージを送信するときに内部状態を変更しない実装はどのようにできますか?例外は、と一致するメッセージを出力することです&self
が、一般的なケースは、おそらく、バッファリング、エラーフラグの更新を意味する可能性のある、ある種の内部ストリームへの書き込みで構成されます...これには&mut self
、たとえば、impl Write for File。
この問題を解決するために内部の可変性に依存const_cast
することは、C ++では、mutable
アプリケーションの他の場所で一貫性がないという理由だけでメンバーを悪用または悪用しているように思えますconst
(C ++の学習者にとってよくある間違い)。
したがって、以下のサンプルコードに戻ると、次のようになります。
- 格納されている整数の値を変更するという事実との一貫性を保つために
&mut self
、change_e()
tochange_i()
を使用します(必須ではない場合でも、コンパイラは文句を言いません)? &self
格納されている整数の値を実際に変更したとしても、内部の可変性によって許可されるため、を使用し続けますか?
この決定は、構造体自体にローカルであるだけでなく、この構造体を使用してアプリケーションで表現できるものに大きな影響を与えます。共有参照のみが含まれるため、2番目のソリューションは確かに大いに役立ちますが、Rustで期待されるものと一致しています。
RustAPIガイドラインでこの質問に対する答えを見つけることができません。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());
}
回答
この問題を解決するために内部の可変性に依存
const_cast
することは、C ++では、mutable
アプリケーションの他の場所で一貫性がないという理由だけでメンバーを悪用または悪用しているように思えますconst
(C ++の学習者にとってよくある間違い)。
これは、C ++のコンテキストでは完全に理解できる考えです。正確でない理由は、C ++とRustの可変性の概念が異なるためです。
ある意味で、Rustのmut
キーワードには実際には2つの意味があります。パターンでは「可変」を意味し、参照型では「排他的」を意味します。との違いは&self
、&mut self
実際には変更self
できるかどうかではなく、エイリアスを作成できるかどうかです。
このMessenger
例では、まず、あまり真剣に受け止めないようにしましょう。これは、必ずしもシステム設計ではなく、言語機能を説明するためのものです。しかし、なぜ&self
使用されるのか想像できます。共有Messenger
される構造によって実装されることを意図しているため、異なるコードが同じオブジェクトへの参照を保持し、相互に調整せずにアラートに使用できます。をとる場合、一度に存在できる参照は1つだけであるため、この目的には役に立ちません。共有にメッセージを送信することは不可能です(または何かを介して内部可変性の外部レイヤーを追加しない限り)。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
、どのポインターが他のポインターのエイリアスを許可されているかを関数に注釈を付け、ポインターがいつ有効であるかについていくつかのルールを作成する必要があります。そして、各関数でルールが守られているかどうかをチェックするコンパイラパスを持っています。ある種の...チェッカーのように。