「大多数时候不用改,偶尔需要改」是 Cow 的典型场景:不改就借用、改了才复制。这篇用真实的指针比较和分配计数验证它省了多少,以及 to_mut 在什么时候真正克隆。
源码:
1 | use std::borrow::Cow;
2 | fn strip_tabs(s: &str) -> Cow<'_, str> {
3 | if s.contains('\t') { Cow::Owned(s.replace('\t', " ")) } else { Cow::Borrowed(s) }
4 | }
5 | fn kind<T: ?Sized + ToOwned>(c: &Cow<'_, T>) -> &'static str { match c { Cow::Borrowed(_) => "Borrowed(没有分配)", Cow::Owned(_) => "Owned(分配了新 String)" } }
6 | fn main() {
7 | let a = "no tabs here"; let b = "has\ttab";
8 | let ra = strip_tabs(a); let rb = strip_tabs(b);
9 | println!("{:<14} → {:?} {} 指向原字符串:{}", a, ra, kind(&ra), std::ptr::eq(ra.as_ptr(), a.as_ptr()));
10 | println!("{:<14} → {:?} {}", b, rb, kind(&rb));
11 | }
编译:成功
运行(退出码 0):
no tabs here → "no tabs here" Borrowed(没有分配) 指向原字符串:true
has tab → "has tab" Owned(分配了新 String) 源码:
1 | use std::alloc::{GlobalAlloc, Layout, System};
2 | use std::borrow::Cow;
3 | use std::sync::atomic::{AtomicUsize, Ordering};
4 | struct C; static AL: AtomicUsize = AtomicUsize::new(0);
5 | unsafe impl GlobalAlloc for C {
6 | unsafe fn alloc(&self, l: Layout) -> *mut u8 { AL.fetch_add(1, Ordering::Relaxed); unsafe { System.alloc(l) } }
7 | unsafe fn dealloc(&self, p: *mut u8, l: Layout) { unsafe { System.dealloc(p, l) } }
8 | }
9 | #[global_allocator] static G: C = C;
10 | fn always_string(s: &str) -> String { s.replace('\t', " ") }
11 | fn cow(s: &str) -> Cow<'_, str> { if s.contains('\t') { Cow::Owned(s.replace('\t', " ")) } else { Cow::Borrowed(s) } }
12 | fn main() {
13 | let inputs: Vec<String> = (0..10_000).map(|i| if i % 10 == 0 { format!("row{i}\tx") } else { format!("row{i} x") }).collect();
14 | let a0 = AL.load(Ordering::Relaxed);
15 | let mut n1 = 0usize; for s in &inputs { n1 += always_string(s).len(); }
16 | let a1 = AL.load(Ordering::Relaxed) - a0;
17 | let a0 = AL.load(Ordering::Relaxed);
18 | let mut n2 = 0usize; let mut owned = 0usize;
19 | for s in &inputs { let c = cow(s); if matches!(c, Cow::Owned(_)) { owned += 1; } n2 += c.len(); }
20 | let a2 = AL.load(Ordering::Relaxed) - a0;
21 | println!("每次都返回 String:堆分配 {} 次(总长度 {})", a1, n1);
22 | println!("返回 Cow<str> :堆分配 {} 次(其中 Owned {} 个,总长度 {})", a2, owned, n2);
23 | }
编译:成功
运行(退出码 0):
每次都返回 String:堆分配 10000 次(总长度 88890)
返回 Cow<str> :堆分配 1000 次(其中 Owned 1000 个,总长度 88890)分配次数用自定义 #[global_allocator] 计数,release 构建。
源码:
1 | use std::borrow::Cow;
2 | fn main() {
3 | let original = String::from("hello");
4 | let mut c: Cow<str> = Cow::Borrowed(&original);
5 | println!("开始:{:?},是借用:{}", c, matches!(c, Cow::Borrowed(_)));
6 | c.to_mut().push_str(", world"); // 第一次修改:克隆,变成 Owned
7 | println!("to_mut().push_str 之后:{:?},是借用:{}", c, matches!(c, Cow::Borrowed(_)));
8 | c.to_mut().push('!'); // 之后再修改不会再克隆(缓冲区可能因容量不足而移动,但不是 Cow 造成的)
9 | println!("再 push 一次:{:?};原字符串没有被改变:{:?}", c, original);
10 | let s: String = c.into_owned();
11 | println!("into_owned 得到 String:{s}");
12 | }
编译:成功
运行(退出码 0):
开始:"hello",是借用:true
to_mut().push_str 之后:"hello, world",是借用:false
再 push 一次:"hello, world!";原字符串没有被改变:"hello"
into_owned 得到 String:hello, world! 编译:成功
运行(退出码 0):
String = 24,&str = 16,Cow<str> = 24
Vec<u8> = 24,&[u8] = 16,Cow<[u8]> = 24niche 的原理见Option<&T> 的空指针优化。