不少 Dart 开发者都见过 `List a = [1, 2, 3];` 这种写法,但对它到底意味着什么,往往停留在“编译器会帮我推断”这一层。问题在于,泛型参数一旦省略,类型系统未必会按右值直觉工作。本文从规范里的 `dynamic`、`Object?` 和 raw types 讲起,再落到几个容易误判的代码示例,帮助你判断什么时候是在享受类型推断,什么时候其实已经绕开了静态检查。
开头这段代码,变量 a 到底是什么类型?
先看原问题里的代码:
void main() {
List a = [1, 2, 3];
}
很多人的第一反应是:右侧是整数列表,所以 a 应该就是 List<int>。但在 Dart 的规则里,只要泛型类型被使用了,却没有显式提供类型参数,这个类型就会被当成 raw type(原始类型)处理。
也就是说,上面的 List a 并不是“等待从右值推断完整类型”的写法,而是等价于:
List<dynamic> a = [1, 2, 3];
这也是后文一系列行为差异的起点:一旦默认补成了 dynamic,静态分析器能帮你挡住的问题就会少很多。
什么是 strict-raw-types,为什么会有这条检查
Dart 静态分析器提供了一个选项:strict-raw-types。

它针对的就是“泛型类型被写出来了,但类型参数被省略了”这种情况。按照说明,在这个模式下:
省略类型参数的类型会被定义为“原始类型”。Dart 会使用类型参数的边界来填充此类类型参数;若无边界,则使用
dynamic类型填充。
这条规则的关键点不在“语法更严格”,而在于避免开发者不知不觉退回到 dynamic。因为 raw type 看起来简洁,实际却可能把泛型本该提供的静态约束全部冲掉。
从工程角度看,strict-raw-types 的目的很直接:要求你把类型参数写清楚,或者至少明确承认“这里我就是要用 dynamic”。前者让类型信息完整,后者让风险显式暴露出来。
`dynamic` 和 `Object?` 差别到底在哪
很多文章会把两者都描述成“可以接收任何类型”,但 Dart 规范里强调的是一个更重要的区别:它们在静态分析阶段的待遇不同。

根据 Dart 语言规范第 20.7 节,dynamic 也是静态类型,并且像 Object 一样是所有类型的超类型;但它的特殊之处在于,静态分析会假定你访问的成员是存在的。
换成更直白的话说:
Object?:你只能访问它明确拥有的成员,分析器会认真检查。dynamic:你几乎可以“随便调”,分析器默认先放行,出问题再到运行时报错。
看这段代码就很清楚:
void main(List<String> args) {
final d = [];
final o = <Object?>[];
d.add(Object());
o.add(Object());
d[0].missingMethod(); // 会导致运行时的NoSuchMethodError.
// NoSuchMethodError: Class 'Object' has no instance method 'missingMethod'.
o[0].missingMethod(); // 静态分析器直接报错.
}
这里的区别不是“一个能不能存 Object”,而是“错误会在哪一层暴露出来”。
d的元素类型会落到dynamic,所以missingMethod()调用在静态分析阶段不会被拦住。o的元素类型是Object?,分析器知道这个类型上没有该方法,因此会直接报错。
这也是为什么可以把 dynamic 理解成 Dart 静态类型系统中的一个“逃逸口”:编译期省事了,但运行期风险得自己接。
省略泛型参数后,会发生哪些容易忽略的问题
1. 未写类型参数时,默认补成 `dynamic`
规范对这一点写得很明确:

If no static type annotation has been provided, the type system considers declarations to ha ve type dynamic. If a generic type is used but type arguments are not provided, the type arguments default to type dynamic.This means that given a generic declaration G<P1, . . . , Pn>. . ., where Pi is a formal type parameter declaration, i ∈ 1..n, the type G is equivalent to G<dynamic, . . . , dynamic>.
翻译过来就是:如果没有提供静态类型注解,类型系统会把声明视为 dynamic;如果使用了泛型但没有给出类型参数,那么这些类型参数默认也是 dynamic。
因此,下面这段代码里的 List a,本质上就是 List<dynamic> a:
void main() {
List a = [1, 2, 3];
}
2. 看起来像右值推断,实际可能是左值在约束右值
一个容易误解的点是:开发者常以为变量类型总是从赋值号右边推断出来。可一旦左边已经写成 raw type,类型信息就先被左侧固定住了。
下面这个例子更容易看出差异:
void main() {
List a = [1, 2, 3]..forEach((e) => print(e.length));
var b = [4, 5, 6]..forEach((e) => print(e.length));
}
第一条语句里,a 被声明为 raw type,列表会被按 List<dynamic> 处理,因此 e.length 不会触发静态分析错误,问题要等到运行时才暴露,最终抛出 no-such-method。
第二条语句里,var b 允许编译器根据元素类型推断出 List<int>。这时对 int 调用 length,分析器就会直接指出错误。
两段代码的运行意图相近,但因为左侧写法不同,静态检查强度完全不同。
3. 回调参数也可能悄悄变成动态派发
raw type 还会把动态行为扩散到你以为“已经很具体”的代码里:
void main() {
List a = [1, 2, 3]; // 原始类型,等价于 List<dynamic>
a.forEach((e) => print(e.isEven)); // 回调参数 e 的类型是 dynamic
}
这里的 e 并不是很多人直觉里的 int,而是 dynamic。也就是说,isEven 这个调用属于动态派发,方法是否存在,要到运行时才检查。
这类代码最麻烦的地方在于:表面上看不出问题,甚至业务测试阶段也未必立刻暴露,但类型系统已经基本失去保护作用。
实际项目里该怎么选:优先写明确类型,而不是依赖 raw type
如果问题是“在 Dart 泛型中应该优先使用 dynamic 还是 Object”,更准确的回答其实是:
- 不要优先写 raw type。
- 需要通用超类型约束时,优先根据场景写明确的
Object?或具体类型。 - 只有在你确实需要绕开静态成员检查时,再显式写
dynamic。
dynamic 不是不能用,而是应该“有意识地用”。如果你真的要接受任意类型,并且愿意承担运行期检查成本,那就明确写成 List<dynamic>;这样至少代码读者能立刻知道,这里存在动态行为,而不是被 List 这种简写误导。
相反,如果你只是想表达“可以存放任意对象,但仍希望分析器约束成员访问”,那么 Object? 往往比 dynamic 更合适。
如何开启 strict-raw-types
要把这类问题尽量前置到静态分析阶段,可以直接在分析器配置里开启 strict-raw-types:
analyzer:
language:
strict-raw-types: true
开启之后,分析器会更积极地提醒你补全泛型参数,减少因为省略类型参数而默认落入 dynamic 的情况。
如果团队本身就强调类型安全,这个选项值得默认启用。它不会改变 Dart 的语言本质,但能显著降低“代码看起来没问题,实际上静态检查已经失效”的概率。













