管道(Pipeline)
管道运算符把左侧值作为实参传给右侧函数,让数据变换按阅读顺序从左往右排。四种管道运算符共用 |> 记号,靠前缀和后缀区分语义:
| 形态 | 展开 | 用途 |
|---|---|---|
a |> f | f(a) | 基础管道 |
a ?|> f | a 为 null 短路得 null,否则 f(a) | 判空管道 |
a |>? f | try? f(a) | 容错管道 |
a |>! f | try! 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)才可以使用,基础与判空管道没有这个要求。- 四种管道运算符都不可重载。
