C# dynamic 是个陷阱:在泄漏扩散之前阻止它们(Dapper 用户必读)
Source: Dev.to

在 C# 中,dynamic 在边界处非常方便。问题在于它可能悄悄泄漏到你期望使用静态类型的地方,而你的方法签名仍然看起来完全安全。
我在真实项目中遇到了这些泄漏,并编写了一个小型 Roslyn 分析器来提前捕获它们。我是 DynamicLeakAnalyzer(NuGet:DimonSmart.DynamicLeakAnalyzer)的作者。本文阐述了它针对的问题以及如何在边界处修复泄漏。
What dynamic really means
在 C# 中,dynamic 是一种 静态类型,但它 绕过编译时类型检查 对该表达式。成员访问、重载解析、运算符和转换都在运行时绑定。
在运行时,值仍然以 object 的形式流动。编译器会生成运行时绑定(DLR 调用点)并进行缓存,因此在第一次绑定之后,重复调用可以更快。
“动态传染”是如何发生的
两种模式会让泄漏在审查时难以发现:
- 不可见的运行时工作: 成员访问和转换在运行时发生。
- 欺骗性的签名: 方法可以返回
int,但内部仍然执行动态转换。 var可能悄然变为dynamic: 当右侧是动态的时,推断出的类型会变为dynamic。
示例
class Program
{
static int GetX(int i) => i;
static void Main()
{
dynamic prm = 123;
int a = GetX(prm); // DSM001: 隐式的运行时动态转换
var b = GetX(prm); // DSM002: 因为调用是动态的,b 变为 dynamic
}
}
在实际代码中,prm 往往不是局部变量。它可能是传入方法的对象,且动态类型可能隐藏在字段或属性中,例如 prm.Payload.Id。
Dapper 陷阱
Dapper 让人在不知不觉中引入 dynamic 变得很容易。调用 QueryFirst 而不指定返回类型会返回一个动态行对象(通常是 DapperRow),因此属性访问会变为动态的。
using Dapper;
using System.Data;
public static class Repo
{
public static int GetActiveUserId(IDbConnection cn)
{
// QueryFirst() without returns a dynamic row (DapperRow).
var row = cn.QueryFirst("select Id from Users where IsActive = 1");
// row.Id is dynamic, the conversion to int happens at runtime.
return row.Id; // DSM001
}
}
这种代码经常能够通过审查,因为签名显示返回 int。动态绑定隐藏在中间。
Source: …
Dapper 中更安全的替代方案
强类型 API
int id = cn.QuerySingle("select Id from Users where IsActive = 1");
映射到小型 DTO
public sealed record UserId(int Id);
int id = cn.QuerySingle("select Id from Users where IsActive = 1").Id;
如必须使用动态行时立即强制转换
var row = cn.QueryFirst("select Id from Users where IsActive = 1");
int id = (int)row.Id;
目标并不是“永远不要使用 dynamic”。目标是“在边界处阻止泄漏”。
解决方案:DynamicLeakAnalyzer
DynamicLeakAnalyzer 是一个 Roslyn 分析器,在泄漏扩散之前将其放大。它报告两个规则:
- DSM001(隐式 dynamic 转换): 在需要静态类型的地方(返回、赋值、参数等)使用了
dynamic表达式。代码能够编译,但转换在运行时发生。 - DSM002(
var推断为dynamic):var捕获了 dynamic 结果,从而成为 dynamic。
安装并强制执行
添加分析器:
dotnet add package DimonSmart.DynamicLeakAnalyzer
使用 .editorconfig 让警告变为错误:
root = true
[*.cs]
dotnet_diagnostic.DSM001.severity = error
dotnet_diagnostic.DSM002.severity = error
在何处 dynamic 可以使用,何处不宜
良好的边界示例
- COM 互操作
- JSON 适配器和胶水代码
- 数据库适配器(包括 dynamic Dapper 行)
应避免使用 dynamic 的场景
- 核心领域逻辑
- 热循环
- 面向其他开发者的库
下一步
- 在真实代码库上运行分析器,查看动态泄漏已经出现的位置。
- 如果使用 Dapper,搜索返回动态行的
QueryFirst(和非泛型Query(调用。 - 如果出现误报或漏报,请提交一个包含最小复现案例的 issue。