【问题标题】:C# Winforms - Proper Memory Handling of Frequent String TemplatingC# Winforms - 频繁字符串模板的正确内存处理
【发布时间】:2018-01-29 05:11:20
【问题描述】:

我有一个 C# 桌面应用程序,它将获取 RTF 文本块(大小不一,但在较大的块中可能超过 ~20k),然后用数据库记录中的值替换占位符文本(例如“亲爱的 [姓名]”将变为“亲爱的约翰·史密斯”)。对于这个问题的所有意图和目的,假设这是一个邮件合并,并且其他人正在管理模板,但 C# 将读取模板并合并值。

现在,我已经阅读了关于 SO 的几个类似问题。我了解字符串是不可变的,并且您可以强制运行 GC 以清理未引用的字符串。

我不想使用 3rd 方字符串模板库来解决这个问题,因为我想更好地理解这里应该采用的正确机制以及其他意见。

我目前的想法是将模板预处理成一个可能看起来像这样的列表(例如,我使用纯文本字符串而不是 RTF 文本):

List<string> lstTemplate = new List<string>()
{
  "The quick brown ",
  "[Animal1]",
  " jumped over the lazy ",
  "[Animal2]"
};

然后我会使用 StringBuilder 来组装这些片段,以避免在内存中有大量模板的部分副本:

Dictionary<string,string> exampleDataRecord = new Dictionary<string,string>()
{
  { "[Animal1]", "fox" },
  { "[Animal2]", "dog" }
};

StringBuilder sbMerged = new StringBuilder();
foreach(string piece in lstTemplate)
{
  if((piece[0] == '[') && (piece[piece.Length-1] == ']'))
  {
    // Append the variable from the data record
    sbMerged.Append(exampleDataRecord[piece]);
  }
  else
  {
    // Append the template content
    sbMerged.Append(piece);
  }
}

// Display the final, merged result
rtfMyRichTextBox.Text = sbMerged.Text;

这个过程可能平均每天运行数千次,所以我想我会在应用程序期间将预处理的字符串列表保存在内存中。

我的问题是 - 这是解决这个问题的好方法,不会导致大量内存浪费或强制 GC?我认为当 RTF 文本框处理时,它将摆脱合并的文本内容,并且 StringBuilder 应该避免内存中大量部分构建的字符串。

但是,我无法摆脱我错过了什么的感觉,或者我只是盯着它太久了。

【问题讨论】:

    标签: c# string templates memory-management


    【解决方案1】:

    由于这是对您所寻求的机制的理解,我将提供一个实现。通过这个实现,您可以安全地忽略内存管理。让 GC 做它的事。

    我觉得展开代码的本质应该是这样的:

    public string Expand(params string[] dynamics ) {
      string[][] tables = new string[][] { statics, dynamics };
      StringBuilder sb = new StringBuilder();
      foreach (var index in indices)
      {
        sb.Append(tables[index.Item1][index.Item2]);
      }
      return sb.ToString();
    }
    

    这个想法是有一个 静态字符串 数组,其中包含占位符之间的文本范围。还有一个 动态字符串 数组,其中包含要替换给定占位符的文本。然后,在预处理您的模板时,您构造静态字符串数组和 索引对 数组,它们引用静态或动态数组中的索引,使用二维索引,其中 0 是静态的,1 是动态的。

    例如,考虑模板"Hello, {0}. You are {1}! Thanks, {0}." 我们有三个静态字符串:

    { "Hello, ", ". You are ", "! Thanks, ", "." }
    

    动态可以是以下任何一个例子:

    { "Adam", "a man" }
    { "Eve", "a woman" }
    { "jhilgeman", "cool" }
    

    预处理后的索引数组看起来像这样(下图)。注意:我将使用简化的 &lt;a, b&gt; 表示法来表示元组值,而不是 C# 的 Tuple.Create(a,b) 语法

    {
       <0, 0>, // statics[0]   = "Hello, "
       <1, 0>, // dynamics[0]  = "Adam"
       <0, 1>, // statics[1]   = ". You are "
       <1, 1>, // dynamics[1]  = "a man"
       <0, 2>, // statics[2]   = "! Thanks, "
       <1, 0>, // dynamics[0]  = "Adam"  (reuses the same string)
       <0, 3>  // statics[3]   = "."
    }
    

    此技术使用 StringBuilder 快速组装最终结果,并将所有内容放入数组中,以便统一访问文本的静态部分和应该非常有效地执行的动态替换字符串。

    我让您自己想出一个合适的预处理策略,您可以在其中选择分隔符和转义字符,以及如何处理 RTF 格式,以及如何将 [Name] 之类的内容解析为替换索引,以及如何构建替换数组(如果它们恰好来自数据库)。

    但为了完整起见,这里是一个简单的纯文本示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Text.RegularExpressions;
    
    class Template
    {
        public string Expand(params string[] dynamics)
        {
            string[][] tables = new string[][] { statics, dynamics };
            StringBuilder sb = new StringBuilder();
            foreach (var index in indices)
            {
                sb.Append(tables[index.Item1][index.Item2]);
            }
            return sb.ToString();
        }
    
        private Tuple<int, int>[] indices;
        private string[] statics;
    
        public Template(string source)
        {
            int last = 0;
            List<string> statics = new List<string>();
            List<Tuple<int, int>> indices = new List<Tuple<int, int>>();
            foreach (Match match in Regex.Matches(source, @"\{(\d+)\}"))
            {
                if (match.Index > last)
                {
                    // emit a static portion
                    indices.Add(Tuple.Create(0, statics.Count));
                    statics.Add(source.Substring(last, match.Index - last));
                }
                // emit the dynamic portion
                indices.Add(Tuple.Create(1, int.Parse(match.Groups[1].Value)));
                last = match.Index + match.Length;
            }
            // emit any leftovers
            if (last < source.Length)
            {
                indices.Add(Tuple.Create(0, statics.Count));
                statics.Add(source.Substring(last));
            }
            this.statics = statics.ToArray();
            this.indices = indices.ToArray();
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Template t = new Template("Hello, {0}. You are {1}! Thanks, {0}.");
    
            Console.WriteLine(t.Expand("Adam", "a man"));
            Console.WriteLine(t.Expand("Eve", "a woman"));
            Console.WriteLine(t.Expand("jhilgeman", "cool"));
        }
    }
    

    【讨论】:

    • 除了使用列表/元组而不是字典之外,这与我所拥有的有什么根本不同?
    • @jhilgeman,你的问题很大程度上是恐慌——你似乎在担心一个你没有的问题。因此,我向您保证,高性能实现几乎与您所拥有的完全一样 - 除了您将字符串替换标准化以进行整数查找而不是字符串搜索/比较。您可以让扩展器以类似方式处理静态和动态片段,从而进一步改进它。您应该不会仅因模板扩展而出现性能问题。
    • 好的。我不太关心性能,而更关心适当的内存管理,所以如果我的方法是根据您的评论正确处理该方面,那么这就是我的答案。
    • @jhilgeman,是的,我会再次确认,在您展示或谈论的部分中,您对内存的使用是正常的。我想明确地说,您尚未披露如何构建模板的详细信息,但我认为这超出了范围。
    • 是的,我试图将这个问题限制在一个问题上,而没有太多的切线。谢谢!
    猜你喜欢
    • 2014-05-04
    • 1970-01-01
    • 1970-01-01
    • 2012-07-11
    • 2018-05-02
    • 1970-01-01
    • 2011-06-22
    • 2010-10-11
    • 1970-01-01
    相关资源
    最近更新 更多