张赐荣,视障者,信息无障碍专家
深耕Web/PC/移动端可访问性研究与实践工作多年,对跨平台无障碍解决方案拥有深刻的独特理论和丰富的实战经验。
精通视障用户软件交互设计,致力于用专业的能力改善、提升产品可及性体验。

張賜榮

张赐荣的技术博客

博客园 首页 新随笔 联系 订阅 管理

c# 使用正则表达式实现简易的字符串模板宏变量替换功能

在日常开发工作中,我们经常会遇到需要根据模板字符串填充内容动态生成文本的场景,比如生成邮件内容、配置文件、日志消息等等。一个强大好用的宏变量替换工具能让我们的代码变得更加优雅。

本文就带大家从零开始,用C#手把手打造一个简易但功能完备的宏变量替换功能,让我们一起走进正则表达式的世界。


一、 从实际工作中的真实场景说起:动态生成欢迎邮件

假如你正在开发一个用户注册系统。当新用户注册成功后,需要发送一封欢迎邮件。这封邮件的内容大部分是固定的,但有几个关键部分是动态变化的,比如用户名和注册日期。

我们希望的邮件模板是这个样子:

尊敬的 {@username},

欢迎您加入我们的大家庭!

您于 {@register_date} 成功注册了账户。
希望您在这里能有一段愉快的旅程。

祝好,
我们的团队

在这个模板中,{@username}{@register_date} 就是我们所说的宏变量(Macro Variable)。程序在发送邮件前,需要将它们智能地替换成真实的用户名(比如 "Arno")和注册日期(比如 "2025-08-17")。

这个需求看似简单,但如果我们想让它变得更“智能”,就会遇到一些有趣的挑战。

二、 为什么简单的 String.Replace 还不够?

看到这个需求,很多同学的第一反应可能是:“嗨,这不就是字符串替换吗?一个 String.Replace 不就搞定了?”

让我们试试看:

string template = "尊敬的 {@username},欢迎您...";
string username = "Arno";

// 看起来不错?
template = template.Replace("{@username}", username); 

这确实能工作,但一个见状的工具需要考虑得更多。假如,我们的邮件内容需要说明宏变量呢?比如下面这句:

"您可以使用 \{@username} 这样的格式来代表用户名,使用 \{@time} 来代表当前时间。"

最终的输出希望是:

"您可以使用 {@username} 这样的格式来代表用户名,使用 {@time} 代表当前时间。"

但如果我们粗暴地使用 String.Replace,它会错误地将 {@username} 也替换掉,输出如。

"您可以使用 Arno 这样的格式来代表用户名,使用 12:30 代表当前时间。"

核心问题是什么呢?

  1. 替换与显示的冲突:我们需要一种机制,能明确告诉程序:“这个 {@...} 你要给我替换掉,但另一个 {@...} 你得给我原样显示出来。”
  2. 健壮性:如果模板中出现一个我们没定义过的宏,比如 {@login_ip},程序应该怎么办?是报错崩溃,还是优雅地保持原样?
  3. 灵活与性能:如果有很多个宏变量,我们需要一个高效且易于扩展的方案,而不是一长串的 Replace 调用。

为了解决这些挑战,我们需要一套更聪明的规则和一个更强大的工具。

三、 怎样解决上面这些问题呢?

1. 制定规则:引入“转义字符”

为了解决“替换与显示”的冲突,我们引入程序设计中的经典解决方案——转义字符(Escape Character)。假如我们指定反斜杠 \ 作为转义符,它的作用就是让紧跟在它后面的特殊字符“失效”,变回普通文本。

规则如下:

  • 宏替换:当程序遇到 {@macro_name} 格式时,它会查找名为 "macro_name" 的宏并替换其值。
  • 宏转义:当程序遇到 \{@macro_name} 时,它会识别出 \ 是转义符,于是它会“吃掉”\,然后将 {@macro_name} 原样输出。

这套规则清晰地定义了我们的意图,接下来就是选择实现这个规则的方法了。

2. 实现方法:正则表达式 (Regex) + MatchEvaluator

要实现这种基于模式的、带有判断逻辑的替换,正则表达式无疑是最佳选择。C# 中的 Regex.Replace 方法有一个非常强大的重载,它允许我们传入一个叫做 MatchEvaluator 的委托(你可以理解为一个回调函数)。

它的工作流程是这样的:

  • Regex:负责根据我们定义的模式(比如 \{@...}{@...})在字符串中进行搜索。
  • MatchEvaluatorRegex 每找到一个匹配项,就把这个匹配项交给 MatchEvaluator 函数。在这个函数内部,我们可以用 C# 代码来分析这个匹配项(比如,它是带 \ 的还是不带的?),然后“告诉”Regex 应该用什么字符串来替换它。

这个“查找-决策-替换”的组合拳,正是我们解决问题的完美方案!

四、 一步步构建 MacroUtility

说了这么多,是时候上代码了!我们创建一个 MacroUtility 的静态工具类,因为它不需要存储任何状态,非常适合作为公共函数库。

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace WindowsApplication
{
    /// <summary>
    /// 一个提供静态方法来处理字符串宏替换的工具类。
    /// </summary>
    public static class MacroUtility
    {
        // 定义正则表达式
        //    - \\?         : 匹配一个可选的、经过转义的反斜杠 (\)
        //    - \{@         : 匹配字面的 "{@"
        //    - (.+?)       : 这是一个非贪婪的捕获组,匹配宏的名称。
        //    - \}          : 匹配字面的 "}"
        private static readonly Regex MacroRegex = new Regex(@"\\?\{@(.+?)\}", RegexOptions.Compiled);

        /// <summary>
        /// 展开字符串中的宏,并处理转义字符。
        /// </summary>
        public static string ExpandMacros(string template, Dictionary<string, string> macros)
        {
            // 处理一些边界情况,让代码更健壮
            if (string.IsNullOrEmpty(template))
            {
                return string.Empty;
            }
            if (macros == null || macros.Count == 0)
            {
                // 如果没有宏可以替换,直接返回原模板
                return template; 
            }
            
            // 核心!调用 Regex.Replace 并传入我们的“决策者”Lambda函数
            string result = MacroRegex.Replace(template, match =>
            {
                // 在“决策者”内部,实现我们的规则
                // 检查匹配到的内容是否以 '\' 开头,例如 "\{@username}"
                if (match.Value.StartsWith(@"\"))
                {
                    // 规则2实现:这是一个被转义的宏,我们去掉'\',返回剩下的部分
                    return match.Value.Substring(1); // 返回 "{@username}"
                }

                // 如果不是转义宏,就提取宏的名称
                // match.Groups[1].Value 就是我们用括号捕获的内容
                string macroName = match.Groups[1].Value;

                // 从字典中查找宏的值,并处理未定义和null的情况
                if (macros.TryGetValue(macroName, out var value))
                {
                    // 规则1实现:找到了!返回对应的值。
                    // 使用 ?? string.Empty 确保即使字典里的值是null,也不会导致程序崩溃。
                    return value ?? string.Empty;
                }
                else
                {
                    // 宏未定义,优雅地保持原样,而不是抛出异常
                    return match.Value;
                }
            });

            return result;
        }
    }
}```

**代码解释:**

*   **正则表达式 `@"\\?\{@(.+?)\}"`**:这是我们整个工具的灵魂。
    *   `\\?` 匹配一个可选的 `\`,用来识别是否被转义。
    *   `\{@` 和 `\}` 精准地定位宏的边界。
    *   `(.+?)` 是捕获宏名称的关键。这里的 `?` 使其成为**非贪婪(Non-Greedy)**匹配,它会尽可能少地匹配字符。这能正确处理像 `{@time},}多一个}` 这样的情况,只匹配到第一个 `}` 就停止。
    *   `RegexOptions.Compiled` 是一个性能优化项,对于会被反复使用的正则表达式,它能带来不小的速度提升。
*   **`match` 对象**:在 Lambda 函数中,`match.Value` 代表整个匹配到的字符串(如 `\{@username}`),而 `match.Groups[1].Value` 则代表第一个括号 `()` 里捕获的内容(即宏名称 `username`)。

#### **五、 实战演练**

现在,让我们用这个新鲜出炉的工具来解决我们最初的邮件问题吧!

```csharp
internal class Program
{
    static void Main(string[] args)
    {
        // 定义宏字典
        var mailMacros = new Dictionary<string, string>
        {
            { "username", "Arno" },
            { "register_date", DateTime.Now.ToString("yyyy-MM-dd") }
        };

        // 2. 准备邮件模板,包含需要替换的宏和需要转义的宏
        string mailTemplate = @"
尊敬的 {@username},

欢迎您加入我们的大家庭!
您于 {@register_date} 成功注册了账户。

P.S. 在系统中,您可以使用 \{@username} 格式来引用您的用户名。
未定义的宏如 {@undefined_macro} 会保持原样。

祝好,
我们的团队
";
        
        // 调用工具!
        string finalMailContent = MacroUtility.ExpandMacros(mailTemplate, mailMacros);

        // 见证奇迹!
        Console.WriteLine(finalMailContent);
    }
}

运行结果:

尊敬的 Arno,

欢迎您加入我们的大家庭!
您于 2025-08-17 成功注册了账户。

P.S. 在系统中,您可以使用 {@username} 格式来引用您的用户名。
未定义的宏如 {@undefined_macro} 会保持原样。

祝好,
我们的团队

看!所有的宏都按照我们的预期被正确处理了!

posted on 2025-08-18 09:29  张赐荣  阅读(30)  评论(0)    收藏  举报

感谢您访问张赐荣的技术分享博客!
博客地址:https://cnblogs.com/netlog/
知乎主页:https://www.zhihu.com/people/tzujung-chang
个人网站:https://prc.cx/