SmartFormat.NET /2 - 提升 string.Format 的新境界
SmartFormat.NET /2 是一个字符串模板库,允许您用数据填充字符串。易于使用,快速,可扩展,且功能极其强大。支持使用任何数据类型的命名 {占位符}、条件格式化、迭代 IEnumerables 等等。
引言
当是时候对 MailMergeLib - A .NET Mail Client Library 进行改进时,我正在寻找一个快速的字符串模板库,它应该能够替代在邮件文本中用于将变量值替换占位符的低效正则表达式。我仔细研究了几种解决方案,例如 这个 或 那个。但它们都无法与 Scott Rippey 过去提出的解决方案相提并论:SmartFormat.NET。这个库在 NuGet 上已有数万次下载,非常受欢迎。
背景
我对字符串模板库的要求是
- 开源,最好是 MIT 许可证
- 真正的解析,而不是
RegEx
- 兼容
string.Format
- C# 代码体积小,不依赖外部程序集
- 可扩展性
- 性能接近
string.Format
- 可以移植到 .NET Core
- 易于集成到 Mail Client Library 中
这些标准也导致拒绝了 Razor Engine。
不幸的是,Scott 和他的项目贡献者在一段时间内有不同的优先事项,所以项目一度陷入停滞。因此,我决定为该项目做出贡献,处理用户报告的问题,将其移植到 .Net Core(以及 .NET Framework 4.0 和 4.5),并在我认为存在不足的地方进行扩展。
C# 6 引入的字符串插值怎么样?
字符串插值是个有用的东西
var addrList = new[] { new { Name = "Jim", Address = new { City = "New York", State = "NY" } } };
var result = string.Format($"{addrList[0].Name} in {addrList[0].Address.City}");
然而,字符串插值仅在编译时可用。因此,我一直在寻找一个运行时可用的类似解决方案。这基本上就是 SmartFormat 的目标。
Roslyn 怎么样?
是的,您可以使用 CSharpScript
来编译像这样的插值字符串
namespace Roslyn
{
public class Program
{
public class Sample
{
public readonly int s = 12345;
}
static void Main(string[] args)
{
var sw = new Stopwatch();
sw.Start();
var state = CSharpScript.RunAsync(@"return string.Format($""The number is{s}."");", globals: new Sample()).Result;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
}
示例中已经包含了一个秒表,这将非常清楚地表明这不是一个替代方案:它在性能上与 string.Format
或 SmartFormat 相差“一个维度”。
使用代码
SmartFormat.NET /2 的语法仍然接近 Scott 在他于 Code Project 上的文章中所描述的,但方法已从“CustomFormat”更改为“Smart.Format”。SmartFormat.NET /2 还有一个非常出色的 Wiki。这里仅举几个使用最新语法的示例,希望能引起您对更多内容的兴趣
命名占位符
var addrList = new[] { new { Name = "Jim", Address = new {City = "New York", State = "NY"} } };
Smart.Format("{Name} from {Address.City}, {Address.State}", addrList);
// Outputs: "Jim from New York, NY"
复数形式
var emails = new List<string>() {"email1", "email2", "email3"};
Smart.Format("You have {0} new {0:message|messages}", emails.Count);
// Outputs: "You have 3 messages"
性别变位
var user = new[] { new { Name = "John", Gender = 0 }, new { Name = "Mary", Gender = 1 } };
Smart.Default.Parser.UseAlternativeEscapeChar('\\');
Smart.Format("{1:{Name}} commented on {1:{Gender:his|her|their}} photo", user);
列表
var Users = new[] { new { Name = "Jim" }, new { Name = "Pam" }, new { Name = "Dwight" }, new { Name = "Michael" } };
Smart.Format("{Users:{Name}|, | and } liked your comment", new object[] { new {Users = Users}});
// Outputs: "Jim, Pam, Dwight and Michael liked your comment"
选择
var emails = new List<string>() {"email1", "email2", "email3"};
Smart.Format("You have {Messages.Count:choose(0|1):no new messages|a new message|{} new messages}", new object[] {new {Messages = emails}});
// Outputs: "You have 3 new messages"
常见陷阱
SmartFormat.NET 的一些用户遇到了非常有助于入门的疑难问题和陷阱。我也将这些提示添加到了 SmartFormat.NET /2 Wiki 中。
Smart.Format vs. SmartFormatter.Format
开始使用该库时,请使用 Smart.Format(...)
。Smart.Format() 会自动初始化所有您需要的后台内容。如果您直接使用 SmartFormatter.Format(...)
,那么初始化就是您的责任。所以开始时,请暂时忽略 SmartFormatter.Format()。注意:Smart.Format(...)
只是 Smart.Default.Format(...)
的简写形式。
错误处理
默认情况下,SmartFormat 将格式化程序和解析器的 ErrorAction
设置为 ErrorAction.Ignore
。这可能导致令人困惑的结果。强烈建议在开发和调试代码时开启异常处理。
Smart.Default.ErrorAction = ErrorAction.ThrowError; Smart.Default.Parser.ErrorAction = ErrorAction.ThrowError;
错误跟踪
除了抛出和 catch
异常之外,还可以通过订阅相应的事件来跟踪任何格式化或解析错误。使用 Smart.Format(...)
时,可以这样处理这些事件
// missing or mal-formatted placeholders
var badPlaceholders = new HashSet<string>();
Smart.Default.OnFormattingFailure += (sender, args) => { badPlaceholders.Add(args.Placeholder); };
// parsing errors, like missing closing curly braces
var parsingErrorText = new HashSet<string>();
Smart.Default.Parser.OnParsingFailure += (sender, args) => { parsingErrorText.Add(args.RawText); };
无论格式化程序或解析器的 ErrorAction
设置如何,这些事件都会触发。与异常不同,**所有错误**都会被报告,而不仅仅是第一个失败。通过这种方式,您可以在代码中更精细地决定如何处理错误。
转义大括号
SmartFormat 开箱即用,可以无缝替代 string.Format
。其结果是,大括号会按照 string.Format
的方式进行转义。因此,如果期望的输出是 {literal}
,则意味着将开括号和闭括号都加倍。
string.Format("{{literal}}")
Smart.Format("{{literal}}")
然而,这种 string.Format
的兼容性在使用 SmartFormat 的扩展格式化功能时会产生问题,例如:
// This won't work with default settings!
Smart.Format("{0:{Persons:{Name}|, }}", model);
原因是格式字符串末尾的双大括号,解析器会按照 string.Format
的风格进行转义,从而导致“缺少闭括号”异常。幸运的是,这很容易解决
// This will work Smart.Default.Parser.UseAlternativeEscapeChar('\\'); Smart.Format("{0:{Persons:{Name}|, }}", model);
有了这个解析器设置,通过 Smart.Format("\{literal\}")
就可以实现输出 {literal}
。
多个数据源
与 string.Format
一样,可以使用多个数据源作为 SmartFormat 的参数。但是概念略有不同
var dict1 = new Dictionary<string, string="">() { {"Name", "John"} };
var dict2 = new Dictionary<string, string="">() { { "City", "Washington" } };
var result = Smart.Format("First dictionary: {0:{Name}}, second dictionary: {1:{City}}", dict1, dict2);
// Outputs: "First dictionary: John, second dictionary: Washington"
安全
在 .NET Framework 4.x 上运行时,SmartFormat 在其 ListFormatter
中使用 System.Runtime.Remoting.Messaging.CallContext.LogicalGet|SetData
。有了这个,我们知道 dotnetfiddle.net 会抛出安全异常。在将 SmartFormat 编译为 .Net Core 时,LogicalGet|SetData
被替换为 AsyncLocal<T>
,它没有这个问题。不幸的是,AsyncLocal<T>
仅在 .NET Framework 4.6 或更高版本上受支持。
历史
- 2016-12-04:SmartFormat.NET /2 的初始文章