API 设计中的内部可变性滥用?
Interior mutability abuse in API design?
我的 C++ 背景让我对 内部可变性 感到不安。
下面的代码是我对这个话题的调查。
我同意,从借用检查员的角度来看,处理
对内部状态可能的每个结构的许多引用
迟早要改是不可能的;那显然是哪里
内部可变性可以提供帮助。
此外,在 The Rust Programming Language 的第 15.5 "RefCell and the Interior Mutability Pattern" 章中,示例
关于 Messenger
特性及其在
MockMessenger
struct 让我觉得它很常见 API
设计系统地更喜欢 &self
而不是 &mut self
甚至
如果很明显某种可变性将是强制性的
迟早。
Messenger
的实现怎么可能不改变它的内部
发送消息时的状态?
异常只是打印消息,这是一致的
使用 &self
,但 一般情况 可能包括
写入某种内部流,这可能意味着缓冲,
更新错误标志...
所有这些当然需要&mut self
,例如
impl Write for File
.
我觉得依靠内部可变性来解决这个问题
比如,在 C++ 中,const_cast
ing 或滥用 mutable
成员只是
因为在应用程序的其他地方我们不一致
const
ness(C++ 学习者的常见错误)。
所以,回到我下面的示例代码,我应该:
- 使用
&mut self
(编译器不会报错,即使它是
非强制)从 change_e()
到 change_i()
以便
与我改变值的事实保持一致
存储的整数?
- 继续使用
&self
,因为内部可变性允许它,即使
如果我真的改变了存储整数的值?
这个决定不仅是结构本身的本地决定,而且将
对可以表达的内容有很大的影响
使用此结构的应用程序。
第二种解决方案肯定会有很大帮助,因为只有
涉及共享引用,但它与什么一致
在 Rust 中是预期的。
我找不到这个问题的答案
Rust API Guidelines。
是否有任何其他 Rust 文档类似于
C++CoreGuidelines?
/*
$ 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());
}
Relying on interior mutability to solve this problem sounds to me
like, in C++, const_cast
ing or abusing of mutable
members just
because elsewhere in the application we were not consistent about
const
ness (common mistake for learners of C++).
这是在C++语境下完全可以理解的思想。它不准确的原因是因为 C++ 和 Rust 有不同的可变性概念。
在某种程度上,Rust 的 mut
关键字实际上有两个含义。在模式中,它表示“可变”,在引用类型中,它表示“独占”。 &self
和&mut self
的区别不在于self
能不能变异,而是能不能别名.
在Messenger
的例子中,嗯,首先我们不要太当真;它旨在说明语言功能,不一定是系统设计。但是我们可以想象为什么可以使用 &self
:Messenger
意味着由 shared 的结构实现,因此不同的代码段可以保存对相同的对象并将其用于 send
警报,而无需相互协调。如果 send
取 &mut self
,它就没有用了,因为一次只能有一个 &mut self
引用。将消息发送到共享 Messenger
是不可能的(如果不通过 Mutex
或其他方式添加内部可变性的外部层)。
另一方面,每个 C++ 引用和指针都可以使用别名。¹因此在 Rust 术语中,C++ 中的 all 可变性是“内部”可变性! Rust 没有等同于 C++ 中的 mutable
,因为 Rust 没有 const
成员(这里的标语是“可变性是绑定的 属性,而不是类型”)。 Rust 确实 等同于 const_cast
,但仅适用于原始指针,因为将共享 &
引用转换为独占 &mut
引用是不合理的.相反,C++ 没有像 Cell
或 RefCell
这样的东西,因为每个值都隐含在 UnsafeCell
后面。
So, back to my example code below, should I[...]
这实际上取决于 Thing
的预期 语义 。 Thing
的本质是共享,就像通道端点或文件一样?在共享(别名)引用上调用 change_e
有意义吗?如果是,则使用内部可变性在 &self
上公开一个方法。 Thing
主要是数据容器吗?有时共享有时排他是否有意义?那么 Thing
可能不应该使用内部可变性,让库的用户决定如何处理共享突变,如果有必要的话。
另见
¹ 实际上,C++ 确实 具有使指针类似于 Rust 中的引用的功能。有点儿。 restrict
是 C++ 中的 non-standard 扩展,但它是 C99 的一部分。 Rust 的共享 (&
) 引用就像 const *restrict
指针,独占 (&mut
) 引用就像非 const
*restrict
指针。参见 What does the restrict keyword mean in C++?
您最后一次在 C++ 中故意使用 restrict
(或 __restrict
等)指针是什么时候?不要费心去想它;答案是“从不”。 restrict
启用比常规指针更积极的优化,但很难正确使用它,因为您必须非常小心别名,并且编译器不提供任何帮助。它基本上是一把巨大的脚枪,几乎没有人使用它。为了使 restrict
像在 C++ 中使用 const
一样普遍使用是值得的,您需要能够在函数上注释哪些指针允许在什么时候别名其他指针,使一些关于指针何时有效遵循的规则,并有一个编译器通过检查每个函数是否遵循这些规则。就像某种...检查器。
我的 C++ 背景让我对 内部可变性 感到不安。 下面的代码是我对这个话题的调查。
我同意,从借用检查员的角度来看,处理 对内部状态可能的每个结构的许多引用 迟早要改是不可能的;那显然是哪里 内部可变性可以提供帮助。
此外,在 The Rust Programming Language 的第 15.5 "RefCell and the Interior Mutability Pattern" 章中,示例
关于 Messenger
特性及其在
MockMessenger
struct 让我觉得它很常见 API
设计系统地更喜欢 &self
而不是 &mut self
甚至
如果很明显某种可变性将是强制性的
迟早。
Messenger
的实现怎么可能不改变它的内部
发送消息时的状态?
异常只是打印消息,这是一致的
使用 &self
,但 一般情况 可能包括
写入某种内部流,这可能意味着缓冲,
更新错误标志...
所有这些当然需要&mut self
,例如
impl Write for File
.
我觉得依靠内部可变性来解决这个问题
比如,在 C++ 中,const_cast
ing 或滥用 mutable
成员只是
因为在应用程序的其他地方我们不一致
const
ness(C++ 学习者的常见错误)。
所以,回到我下面的示例代码,我应该:
- 使用
&mut self
(编译器不会报错,即使它是 非强制)从change_e()
到change_i()
以便 与我改变值的事实保持一致 存储的整数? - 继续使用
&self
,因为内部可变性允许它,即使 如果我真的改变了存储整数的值?
这个决定不仅是结构本身的本地决定,而且将 对可以表达的内容有很大的影响 使用此结构的应用程序。 第二种解决方案肯定会有很大帮助,因为只有 涉及共享引用,但它与什么一致 在 Rust 中是预期的。
我找不到这个问题的答案 Rust API Guidelines。 是否有任何其他 Rust 文档类似于 C++CoreGuidelines?
/*
$ 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());
}
Relying on interior mutability to solve this problem sounds to me like, in C++,
const_cast
ing or abusing ofmutable
members just because elsewhere in the application we were not consistent aboutconst
ness (common mistake for learners of C++).
这是在C++语境下完全可以理解的思想。它不准确的原因是因为 C++ 和 Rust 有不同的可变性概念。
在某种程度上,Rust 的 mut
关键字实际上有两个含义。在模式中,它表示“可变”,在引用类型中,它表示“独占”。 &self
和&mut self
的区别不在于self
能不能变异,而是能不能别名.
在Messenger
的例子中,嗯,首先我们不要太当真;它旨在说明语言功能,不一定是系统设计。但是我们可以想象为什么可以使用 &self
:Messenger
意味着由 shared 的结构实现,因此不同的代码段可以保存对相同的对象并将其用于 send
警报,而无需相互协调。如果 send
取 &mut self
,它就没有用了,因为一次只能有一个 &mut self
引用。将消息发送到共享 Messenger
是不可能的(如果不通过 Mutex
或其他方式添加内部可变性的外部层)。
另一方面,每个 C++ 引用和指针都可以使用别名。¹因此在 Rust 术语中,C++ 中的 all 可变性是“内部”可变性! Rust 没有等同于 C++ 中的 mutable
,因为 Rust 没有 const
成员(这里的标语是“可变性是绑定的 属性,而不是类型”)。 Rust 确实 等同于 const_cast
,但仅适用于原始指针,因为将共享 &
引用转换为独占 &mut
引用是不合理的.相反,C++ 没有像 Cell
或 RefCell
这样的东西,因为每个值都隐含在 UnsafeCell
后面。
So, back to my example code below, should I[...]
这实际上取决于 Thing
的预期 语义 。 Thing
的本质是共享,就像通道端点或文件一样?在共享(别名)引用上调用 change_e
有意义吗?如果是,则使用内部可变性在 &self
上公开一个方法。 Thing
主要是数据容器吗?有时共享有时排他是否有意义?那么 Thing
可能不应该使用内部可变性,让库的用户决定如何处理共享突变,如果有必要的话。
另见
¹ 实际上,C++ 确实 具有使指针类似于 Rust 中的引用的功能。有点儿。 restrict
是 C++ 中的 non-standard 扩展,但它是 C99 的一部分。 Rust 的共享 (&
) 引用就像 const *restrict
指针,独占 (&mut
) 引用就像非 const
*restrict
指针。参见 What does the restrict keyword mean in C++?
您最后一次在 C++ 中故意使用 restrict
(或 __restrict
等)指针是什么时候?不要费心去想它;答案是“从不”。 restrict
启用比常规指针更积极的优化,但很难正确使用它,因为您必须非常小心别名,并且编译器不提供任何帮助。它基本上是一把巨大的脚枪,几乎没有人使用它。为了使 restrict
像在 C++ 中使用 const
一样普遍使用是值得的,您需要能够在函数上注释哪些指针允许在什么时候别名其他指针,使一些关于指针何时有效遵循的规则,并有一个编译器通过检查每个函数是否遵循这些规则。就像某种...检查器。