.NET 11 Preview · with(...) 向集合构造器传参 · 一行指定容量/比较器
C# 12 的集合表达式 [1, 2, 3] 统一了四种集合创建语法——一个方括号打天下。但它有一个盲区:
| 场景 | C# 12 能做吗? | 你被迫回退到…… |
|---|---|---|
| 创建 List 并预分配容量 | ❌ [with(capacity: 100)] 不存在 | new List<int>(100) { 1, 2, 3 }——告别方括号 |
| 创建忽略大小写的 HashSet | ❌ 方括号内传不了 comparer | new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "b" } |
| 创建自定义 comparer 的字典 | ❌ 无解 | 不用集合表达式,回到构造函数 + Add |
核心张力:集合表达式让创建集合变得简洁——但一旦你需要指定容量、比较器、或任何构造函数参数,你就被踢出方括号世界,回到 new T() { } 的旧语法。C# 15 的 with(...) 元素填上了这个缺口。
// ──── C# 12:创建 List,想预分配容量?对不起,方括号做不到 ────
List<string> old = new List<string>(100) { "a", "b", "c" }; // new + 花括号——旧时代语法
// ──── C# 15:with(capacity: ...) 作为方括号的第一个元素 ────
List<string> names = [with(capacity: 100), "a", "b", "c"];
// ↑ with(...) 必须是第一个元素——语义上先构造、再填充
// ──── HashSet 指定比较器——在此之前做不到 ────
HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO", "hello"];
// set.Count == 1——OrdinalIgnoreCase 下三个字符串相等
// ──── 结合 spread——容量预分配 + 批量复制 ────
string[] source = ["one", "two", "three"];
List<string> result = [with(capacity: source.Length * 2), .. source];
// 先分配 source.Length*2 的容量,再 spread 复制——一步到位
with(...) 必须是集合表达式的第一个元素。写 [1, 2, with(capacity: 3)] 会编译错误。这对应集合的构造语义:先建壳、再装东西。位置规则也让读者一眼看到关键配置(如比较器),不用翻到表达式末尾。
// with(...) 出现前,这些全部合法:
int[] a = [1, 2, 3]; // 数组——with() 不适用,不支持
Span<int> b = [1, 2, 3]; // Span——with() 不适用,不支持
List<int> c = [1, 2, 3]; // List——现在可以加 with(),也可以不加
// 如果有一个叫 with 的方法:
object with(int x, int y) => ...;
object[] d = [with(1, 2), 3]; // C# 14: 调用 with(1,2),数组含两个元素
// C# 15: 编译错误——object[] 不支持 with()
C# 15 中,只要 token 序列以 with( 开头,编译器就把它解析为 with_element。如果目标类型不支持参数(数组、Span),即使空 with() 也会报错。这和 C# 14 是 breaking change——仅在编译时指定 C# 15 语言版本时生效。
with(...) 的参数去哪了?取决于目标类型:
| 目标类型 | 参数去往…… | 示例 |
|---|---|---|
| ① 普通 class/struct (有构造函数) |
构造函数重载决议——和 new T(args) 一样 |
List<T>(int capacity)HashSet<T>(IEqualityComparer<T>) |
| ② [CollectionBuilder] 类型 (有工厂方法) |
Create 方法的非 span 参数—— span 参数始终是最后一个,接收元素 |
ImmutableArray.Create<T>(ReadOnlySpan<T>)或自定义 MyBuilder.Create(comparer, elements) |
| ③ 接口类型 (IList、IDictionary 等) |
编译器预设的 curated 构造函数签名—— 不支持任意参数 |
IList<int> → new List<int>(capacity)IDictionary<K,V> → new Dictionary<K,V>(comparer) |
// List<T> 有三个构造函数。编译器根据 with() 参数做重载决议:
List<int> l;
l = [with(capacity: 3), 1, 2]; // → new List<int>(capacity: 3)
l = [with([1, 2]), 3]; // → new List<int>(IEnumerable<int> collection)
l = [with(default)]; // ❌ 歧义——default 匹配多个构造函数
with() 的有无,不看具体参数。有 with() → 找至少一个可访问的构造函数 → 转换成立。无 with() → 必须有无参构造函数。这意味着没有无参构造函数的集合类型,必须用 with() 来创建——[] 不行,[with(capacity: 1)] 可以。
// 自定义集合类型——通过 [CollectionBuilder] 指定工厂方法
[CollectionBuilder(typeof(MyBuilder), "Create")]
class MySet<T> : IEnumerable<T> { ... }
class MyBuilder
{
public static MySet<T> Create<T>(ReadOnlySpan<T> elements);
public static MySet<T> Create<T>(IEqualityComparer<T> comparer, ReadOnlySpan<T> elements);
// ↑ 注意:span 参数必须是最后一个——with() 参数在它之前
}
// 使用——with() 的参数传给 comparer,元素传给 span
MySet<string> set = [with(StringComparer.OrdinalIgnoreCase), "a", "b"];
// → MyBuilder.Create<string>(StringComparer.OrdinalIgnoreCase, ["a", "b"])
编译器按"去掉 span 参数后的投影方法"做重载决议。Create 方法可以有多个重载——每个重载的 span 参数之前可以有不同数量和类型的额外参数。
// IList<T> / ICollection<T> → 背后用 List<T>,with() 参数映射到 List<T> 构造函数
IList<int> list = [with(capacity: 4), 1, 2, 3];
// → new List<int>(capacity: 4) { 1, 2, 3 }
// IDictionary<K,V> → 背后用 Dictionary<K,V>
IDictionary<string, int> d = [with(StringComparer.Ordinal), "a": 1];
// → new Dictionary<string, int>(StringComparer.Ordinal) { ["a"] = 1 }
// IReadOnlyDictionary<K,V> → 只能用 comparer 参数
IReadOnlyDictionary<string, int> r = [with(StringComparer.Ordinal)];
// ✅ comparer 参数——控制 key 比较语义
r = [with(capacity: 2)];
// ❌ 编译错误——IReadOnlyDictionary 不支持 capacity 参数
| 接口 | 支持的 with() 签名 |
|---|---|
IEnumerable<E> / IReadOnlyCollection<E> / IReadOnlyList<E> | ()(等效于没有 with()) |
IList<E> / ICollection<E> | List<E>() · List<E>(int) |
IDictionary<K,V> | Dictionary<K,V>() · (int) · (IEqualityComparer<K>) · (int, IEqualityComparer<K>) |
IReadOnlyDictionary<K,V> | () · (IEqualityComparer<K>?) |
// C# 12:想把 spread 的元素放进 List,但没法预分配容量
List<string> names = [.. values];
// 等价于:new List<string>()(默认容量 4),然后逐个 Add
// values 有 100 个元素 → List 内部数组扩容 log₂(100/4) ≈ 5 次
// 每次扩容:分配新数组 + 拷贝所有已存在的元素
// C# 15:with(capacity:) 直接传给构造函数——编译器不做额外工作
List<string> names = [with(capacity: values.Length), .. values];
// 等价于:new List<string>(values.Length),内部数组一次性分配够
// 零次扩容——values.Length 个元素直接塞进预分配的空间
with(capacity:) 只是把参数转发给 List<T>(int capacity) 构造函数——和直接写 new List<T>(n) 一样。区别是:现在你能在方括号语法里做到这件事了,不用回退到 new T() { }。
// 常见 bug:忘了给 HashSet 传比较器——默认区分大小写
HashSet<string> names = ["Admin", "admin"]; // Count == 2——两个值不等
// C# 15:比较器放在 with() 里,和元素写在一起——不容易忘
HashSet<string> names = [with(StringComparer.OrdinalIgnoreCase), "Admin", "admin"];
// Count == 1——OrdinalIgnoreCase 下 Admin == admin
为什么 with() 放第一个位置能减少 bug:如果一个 100 行的字典表达式在末尾才标注比较器,你很可能看不到它——或者更糟:加了 99 个元素之后才发现比较器写错了。放在开头强迫读者(和写者)先想清楚"这个集合用什么样的相等/排序?"再往里塞东西。
| 限制 | 详情 | 原因 |
|---|---|---|
| 必须是第一个元素 | [1, with(capacity: 3), 2] → ❌ | 语义上先构造再填充——和 new T(args) { } 的直觉一致 |
| 数组 / Span 不支持 | int[] a = [with(), 1, 2] → ❌ | 数组和 Span 没有构造函数——长度由元素数量决定 |
| dynamic 参数禁用 | [with((dynamic)comparer), "a"] → ❌ | dynamic 需要运行时 binder 做重载决议——集合表达式不支持 |
| 不影响类型推断 | with() 的参数不参与泛型推断和重载决议 | 避免 [with(comparer)] 的歧义——ambiguous 错误交给 target-typed new 处理 |
| __arglist 不支持 | [with(__arglist(x, y))] → ❌ | 除非免费实现——设计团队决定不为此增加复杂度 |
| 旧语言版本保持兼容 | C# 14 中 [with(x,y)] 仍被解析为方法调用 | breaking change 只在 LangVersion ≥ 15 时生效 |
with?——设计考量 Why "with"?设计团队考虑了多种语法——最终 pick with(...) 有明确的取舍:
| 候选语法 | 为什么被否决 |
|---|---|
[args(capacity: 10), 1, 2] | 技术上 equivalent——但 with 读起来更自然:"a collection with capacity 10" |
new(...)[1, 2] 或 new([1, 2], capacity: 10) | 暗示调用构造函数——但 [CollectionBuilder] 类型不走构造函数。且语法扩散到 [...] 外部,读者可能以为是"创建后再拷贝" |
[comparer; v1, v2](分号分隔) | 容易被忽略——[1; 2] 看起来太像 [1, 2](一个逗号的差别)。写错时:对 List 把 1 当 capacity、只存一个元素 2,悄无声息 |
[comparer, v1](元素复用) | 当元素类型和比较器类型有继承关系时歧义——HashSet<object> 无法区分"第一个元素是比较器"还是"第一个元素就是放进集合的值" |
with()——语法着色下 with 关键字高亮、() 内的参数正常着色——视觉上不容忽视,语义上"先构造再填充"的直觉对应,扩展性上不限制未来新增参数类型。
with(...) 元素在集合表达式中的位置要求是?
ImmutableArray<int> arr = [with(), 1, 2, 3]; —— with() 的参数传给谁?
以下哪个表达式会产生编译错误?
为什么设计团队否决了 [comparer; element1, element2](分号分隔)方案?
关于集合表达式参数,以下哪个说法正确?
| 要点 | 一句话 |
|---|---|
| 语法 | [with(...), elements] —— with() 必须是第一个元素 |
| 构造函数通路 | 参数传给目标类型的构造函数——编译器做重载决议 |
| CollectionBuilder 通路 | 参数传给 Create 方法的非 span 参数——span 始终是最后一个 |
| 接口通路 | IList → List 构造函数;IDictionary → Dictionary 构造函数;不支持任意参数 |
| 数组 / Span | 不支持 with()——无构造函数,也无此必要 |
| 性能场景 | [with(capacity: n), .. values] —— 一行消除扩容开销 |
| 正确性场景 | [with(comparer), .. values] —— 比较器写在集合旁边,不易忘 |
| 运行时开销 | 零——纯编译期转发,等价于手动调用构造函数或工厂方法 |
| 状态 | .NET 11 Preview 5 已合并——GA 预计 2026.11 |
with(...) 让方括号语法不再有"盲区"。需要传容量?[with(capacity: 100), .. data]。需要比较器?[with(StringComparer.OrdinalIgnoreCase), "a", "b"]。你不需要在"简洁方括号"和"完整构造函数"之间做选择了——两者合一。
C# 15 集合表达式参数填上了 C# 12 方括号语法的最后一块拼图。下节课转向 C# 15 的内存安全演进:
声明指针、取地址 &、fixed、sizeof 不再需要 unsafe 块——但解引用仍需 unsafe。这是多版本演进的第一步。
["key": value] 语法——字典字面量。仍在开发中,runtime 团队已介入,预计后续 Preview 发布。
Union Types → Closed Hierarchies → Collection Expression Args → Memory Safety。四大特性形成 C# 15 核心能力矩阵。
← L26: Closed Hierarchies · L28: Memory Safety → · 🎯 回顾 Mission