引言:为什么你的代码应该用业务语言说话
\n\n想象一下:你正在为一个复杂的交易机器人编写配置。无需数百行if-else和魔法数字,你只需编写:BuyWhen(Price.CrossesAbove(MA(50)))。你的经理不懂C#,但看到这段代码就能理解逻辑。这不是魔法——这是C#中的DSL。
领域特定语言(Domain-Specific Languages, DSL)不仅仅是一种流行趋势。它是一种工具,能将混乱的技术细节转化为优雅、可读的代码。在本文中,我们不仅会讨论理论——我们将从零开始创建一个真实的C# DSL,分析常见错误,并展示如何将这一实践立即应用到你的项目中。
\n\n什么是DSL,为什么开发者需要它?
\n\nDSL是一种为特定任务创建的编程语言。与通用语言(C#、Java、Python)不同,DSL使用领域语言说话。在C#中有两种主要方法:
\n\n- \n
- 内部DSL——通过Fluent API和表达式树构建在C#之上。你仍处于原生环境中。 \n
- 外部DSL——需要解析(通常通过Roslyn)并生成C#代码。 \n
DSL的实际好处:降低认知负荷,减少错误数量(业务逻辑与技术实现分离),并加速新开发者的入职。
\n\n在C#中创建内部DSL:逐步指南
\n\n步骤1:定义领域和语法
\n\n假设我们正在创建一个用于数据验证的DSL。不再使用:
\n\nif (user.Age < 18 || user.Name == null || user.Email == null)\n throw new ValidationException();\n\n我们希望这样编写:
\n\nValidate.User(user)\n .MustBeAdult()\n .MustHaveName()\n .MustHaveEmail();\n\n步骤2:Fluent接口——C# DSL的基础
\n\n关键元素是从每个方法返回this。创建一个验证器类:
public class UserValidator\n{\n private readonly User _user;\n private readonly List<string> _errors = new();\n\n public UserValidator(User user) => _user = user;\n\n public UserValidator MustBeAdult()\n {\n if (_user.Age < 18)\n _errors.Add("用户未成年");\n return this;\n }\n\n public UserValidator MustHaveName()\n {\n if (string.IsNullOrWhiteSpace(_user.Name))\n _errors.Add("姓名必填");\n return this;\n }\n\n public void Validate()\n {\n if (_errors.Any())\n throw new ValidationException(string.Join("; ", _errors));\n }\n}\n\n以及一个优雅入口的工厂方法:
\n\npublic static class Validate\n{\n public static UserValidator User(User user) => new UserValidator(user);\n}\n\n步骤3:添加条件结构和上下文
\n\n真正的C# DSL应该支持分支。实现When:
public UserValidator When(bool condition, Func<UserValidator, UserValidator> validatorFunc)\n{\n if (condition)\n return validatorFunc(this);\n return this;\n}\n\n使用方式:
\n\nValidate.User(user)\n .MustHaveName()\n .When(user.Age > 18, v => v.MustBeAdult());\n\n高级水平:使用表达式树的C# DSL
\n\n为了构建动态查询(ORM、规则),我们使用Expression<Func<T, bool>>。创建一个规则构建器:
public class RuleBuilder<T>\n{\n private Expression<Func<T, bool>> _expression;\n\n public RuleBuilder(Expression<Func<T, bool>> expression) => _expression = expression;\n\n public RuleBuilder<T> And(Expression<Func<T, bool>> other)\n {\n var andExpression = Ex