Skip to Content

管道(Pipeline)

管道运算符把左侧值作为实参传给右侧函数,让数据变换按阅读顺序从左往右排。四种管道运算符共用 |> 记号,靠前缀和后缀区分语义:

形态展开用途
a |> ff(a)基础管道
a ?|> fa 为 null 短路得 null,否则 f(a)判空管道
a |>? ftry? f(a)容错管道
a |>! ftry! f(a)传播管道

助记口径与空条件运算符同族:? 前缀是判空(?.、?[、?? 一家),? 后缀是容错(对应 try?),! 后缀是失败传播(对应 try!)。

基础管道

a |> f 等价 f(a)。链式管道从左往右流动,5 |> Dbl |> Dbl 就是 Dbl(Dbl(5)):

func Dbl(n: int) -> int => n * 2; print(5 |> Dbl |> Dbl);
20

右侧接 lambda 变量一样成立:

val twice = (n: int) => n * 2; print(3 |> twice |> twice);
12

带参形

a |> f(x) 等价 f(a, x):左侧值插到第一个实参位,括号里写其余实参:

func Add(a: int, b: int) -> int => a + b; print(5 |> Add(10));
15

想把左侧值放到别的槽位,不用改函数签名,用下一节的占位符。

占位符

本节用这条数据和三个辅助函数贯穿示例:

val xs = [1, 5, 9, 12]; val s = " ab "; func Between(lo: int, src: int[], hi: int) -> list<int> { val r = new list<int>(); for v in src { if v >= lo && v <= hi { r.Add(v); } } return r; } func Repeat(s: string, n: int) -> string { var r = ""; for i in 0..n { r += s; } return r; } func Run(action: () -> void) { action(); }

管道目标表达式里的 _ 是左值占位:左侧值落在 _ 标的位置。目标里写了 _,左侧值就不再默认插首参。

实参位、接收者位、索引链位都能标,多个 _ 恒定代同一个左值:

print(xs |> Between(2, _, 10)); // 实参中位:Between(2, xs, 10) print("ab" |> Repeat(_, 3)); // 实参首位显式标注:Repeat("ab", 3) print(s |> _.Trim().ToUpper()); // 接收者位 print(xs |> _[0]); // 索引位 print(xs |> _[_.Length - 1]); // 两个 _ 是同一个值
[5, 9] ababab AB 1 12

_ 能穿透到嵌套 lambda 体内和切片链里:

xs |> Run(() => print("长度 " + _.Length)); print(xs |> _[..2]);
长度 4 [1, 5]

纯方法链可以照旧直写 s.Trim().ToUpper(),不需要使用管道。自由函数串起来的长链才是管道的有效场景。

判空管道 ?|>

a ?|> f:左侧为 null 直接短路得 null,右侧根本不执行;非 null 才调 f(a)。结果恒为可空类型,非可空来源静默放行,口径与 ?. 一致:

func Wrap(s: string?) -> string? => s == null ? null : "[" + s + "]"; func Shout(s: string?) -> string? => s?.ToUpper(); val name: string? = "mush"; print(name ?|> Wrap ?|> Shout); val none: string? = null; print(none ?|> Wrap ?|> Shout); print((none ?|> Wrap) ?? "<空>");
[MUSH] null <空>

中间那行 none ?|> Wrap ?|> Shout 在第一环就短路了,Shout 没有执行。链式判空把 null 一路带到链尾,中间不用逐环判断。

每段管道的左操作数恰求值一次(短路时零次),左侧带副作用的表达式在判空管道下不会重复执行。

因为产出恒为可空,链上下一环的参数要声明成 T?(Shout 收 string?)。

要还原成非空值,用 ?? 兜底。?? 的结合比管道更紧,混写时给管道整体加括号:(none ?|> Wrap) ?? "<空>"。

带参形同样适用,s ?|> Pad(">>") 就是判空版的 s |> Pad(">>")。

容错管道 |>?

a |>? f 等价 try? f(a):要求 f 声明了 fail,失败时整条管道产出 null 而不是向上传播:

error Bad(n: int); func Risky(n: int) fail Bad -> int { if n < 0 { fail new Bad(n); } return n; } print(5 |>? Risky); print(-1 |>? Risky); print((7 |>? Risky) ?? 0);
5 null 7

|>? 只接有失败面的目标:声明了 fail 的具名函数或 failable lambda 都行,管道在编译期就是按 try? 展开的。它不是判空:左侧是 int? 它不会帮你跳过。

传播管道 |>!

a |>! f 等价 try! f(a):断言这一步一定成功,成功时透传结果,失败时进程就地带错退出:

print(5 |>! Risky); print(-1 |>! Risky); print("不会走到这里");
5 mush: MS6000: Runtime error: runtime crash: try! assertion failed: 'Risky' invoked fail (error: Main.Bad(n=-1)).

第二行失败后程序立即退出,第三行不会执行。这条管道适合「失败属于编程错误,不打算接」的调用点,与 try! 的定位相同。

案例:深嵌套的函数链

处理链有四道自由函数,函数都收数据作首参,无管道写法只能层层嵌套,从最内层往外读:

func Without(xs: list<int>, v: int) -> list<int> { val kept = new list<int>(); for x in xs { if x != v { kept.Add(x); } } return kept; } func ClampTo(xs: list<int>, hi: int) -> list<int> { val capped = new list<int>(); for x in xs { if x > hi { capped.Add(hi); } else { capped.Add(x); } } return capped; } func Energy(xs: list<int>) -> int { var total = 0; for x in xs { total = total + x * x; } return total; } func Bar(v: int) -> string { var s = ""; for i in 0..v / 10 { s = s + "▇"; } return s; } val samples: list<int> = [3, -1, 7, 4, -2, 15]; val bar = Bar(Energy(ClampTo(Without(samples, 0), 9))); // 四层括号,从最内层往外读 print(bar);
▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇

嵌套形每加一道变换就多一层括号,读的时候要先找到最内层、再带着中间实参一层层退出来。

管道把同一条链摊平成从左往右的读序:带参环节的括号里只剩剩余实参(Without(0)、ClampTo(9)),左值自动插首参。阅读顺序即执行顺序,链再长也只是多一行:

val bar = samples |> Without(0) // 剔除缺测的零值 |> ClampTo(9) // 逐点限幅,防爆表 |> Energy // 算能量:平方和 |> Bar; // 每格十点画条 print(bar);

单行也是允许的:

val bar = samples |> Without(0) |> ClampTo(9) |> Energy |> Bar; print(bar);
▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇

取舍与注意

  • 管道优先级只高于三元与赋值:两侧写算术不用加括号,1 + 2 |> Dbl 就是 Dbl(1 + 2)。
  • 函数值只许出现在调用位(直接调用、管道目标、实参、赋给函数类型的变量)。让它参与算术或 ?? 是编译错误(5 |> Dbl + 1 报 MS3104),所以与 ?? 混写兜底必须给管道整体加括号:(x ?|> F) ?? 默认。
  • |>? 与 |>! 只接有失败面的目标(声明了 fail 的具名函数或 failable lambda)才可以使用,基础与判空管道没有这个要求。
  • 四种管道运算符都不可重载。
最后更新于 2026年10月9日