【发布时间】:2009-02-17 08:07:23
【问题描述】:
我听说过一个字符串是不能改变的(不可变的?)。我猜这应该是正确的。但我也听说两个具有相同内容的字符串共享相同的内存空间(或者你所说的)。这是正确的吗?
如果是这样,这是否意味着如果我创建一个包含数千个字符串的 List,如果这些字符串中的大多数彼此相等,它就不会真正占用太多空间?
【问题讨论】:
我听说过一个字符串是不能改变的(不可变的?)。我猜这应该是正确的。但我也听说两个具有相同内容的字符串共享相同的内存空间(或者你所说的)。这是正确的吗?
如果是这样,这是否意味着如果我创建一个包含数千个字符串的 List,如果这些字符串中的大多数彼此相等,它就不会真正占用太多空间?
【问题讨论】:
编辑:在下面的答案中,我将实习生池称为特定于 AppDomain 的;我很确定这是我之前观察到的,但 String.Intern 的 MSDN 文档建议整个过程只有一个实习生池,这使得这一点更加重要。
原答案
(我打算将此作为评论添加,但我认为这是一个足够重要的点,需要额外的答案......)
正如其他人所解释的,字符串驻留发生在所有字符串文字上,但不适用于“动态创建”的字符串(例如,从数据库或文件中读取的字符串,或使用 StringBuilder 或 String.Format 构建的字符串。)
但是,我不会建议调用String.Intern 来绕过后一点:它将在您的AppDomain 的整个生命周期内填充实习生池。。相反,请使用仅对您的使用而言本地的池。以下是此类池的示例:
public class StringPool
{
private readonly Dictionary<string,string> contents =
new Dictionary<string,string>();
public string Add(string item)
{
string ret;
if (!contents.TryGetValue(item, out ret))
{
contents[item] = item;
ret = item;
}
return ret;
}
}
然后你只需要使用类似的东西:
string data = pool.Add(ReadItemFromDatabase());
(请注意,池不是线程安全的;正常使用不需要它。)
通过这种方式,您可以在不再需要池时立即将其丢弃,而不是在内存中永久保存大量字符串。如果你真的想的话,你也可以让它更聪明,实现一个 LRU 缓存或其他东西。
编辑:只是为了澄清为什么这比使用String.Intern 更好...假设您从数据库或日志文件中读取了一堆字符串,处理它们,然后转到另一个任务。如果您在这些字符串上调用 String.Intern,只要您的 AppDomain 还活着,它们就永远会被垃圾回收 - 甚至可能不会。如果您加载多个不同的日志文件,您将逐渐在实习生池中积累字符串,直到您完成或内存不足。相反,我建议使用这样的模式:
void ProcessLogFile(string file)
{
StringPool pool = new StringPool();
// Process the log file using strings in the pool
} // The pool can now be garbage collected
在这里,您可以获得同一文件中的多个字符串在内存中仅存在一次(或至少仅通过 gen0 一次)的好处,但您不会污染“全局”资源(实习池)。
【讨论】:
ContainsKey 查找两次,然后是索引器,而TryGetValue 将在一次操作中同时执行这两项操作?
这或多或少是真的。它被称为“字符串实习”。字符串文字只会在内存中出现一次,并且每个设置为相同值的变量都指向这个单一的表示。但是,在代码中创建的字符串不会自动被实习。
【讨论】:
如果我没记错的话,在代码中硬编码的字符串是分开汇集的。这称为“Interned”,有一种方法可以查询字符串是否为:String.IsInterned Method
在该页面的“备注”下,您可以阅读:
公共语言运行时自动维护一个称为“实习池”的表,其中包含程序中声明的每个唯一文字字符串常量的单个实例,以及您以编程方式添加的任何唯一 String 实例。
希望对你有所帮助,如果我错了,请纠正我。
马蒂亚斯
【讨论】:
为了使字符串“共享”它们的内存位置,是将它们实习在实习池中,其中包含对在您的程序中以编程方式声明或创建的每个唯一文字字符串的单个引用。
请注意,代码中的所有字符串文字都会被自动插入。
【讨论】: