language-ext Pipes 全解析:在 C 中同时获得 Effects、Streaming 与 Composability
后端【免费下载链接】language-extC# pure functional programming framework - come and get declarative!项目地址https://gitcode.com/gh_mirrors/la/language-ext点击查看免费下载导读Pipes是 language-ext 中用于纯函数式流式编程的核心抽象它以单个统一的PipeT类型为基础派生出Producer、Consumer、Pipe、Effect四类可相互连接的流处理组件让你在常量内存中边流式处理边穿插副作用Effects同时保持组件级可组合性。读完本文你将理解 Pipes 的“三选二”困局解法、yield/awaiting两个原语、|操作符的横向融合原理、Run()的启动方式并能用仓库内真实示例文本文件分块流搭建自己的管线。这是一个相对复杂的概念初次接触感到困惑是正常的。仓库中提供了一个完整可运行的示例Samples/EffectsExamples/Examples/TextFileChunkStreamExample.cs建议配合本文一起阅读。一、传统流编程的“三选二”困局先看文档指出的核心痛点常规的流编程迫使你在以下三个特性中只能选择两个牺牲项得到的方案代价牺牲EffectsIEnumerable可用可组合的函数在常量空间内变换但无法穿插副作用除非是命令式的那种牺牲StreamingTraverse/Sequence可组合、可带效果但必须等整个列表先处理完并载入内存才能返回单个结果牺牲Composability紧耦合的 for 循环边流式处理边触发命令式副作用但不可模块化、不可分离Pipes的目标就是打破这个三角同时提供 effectful带效果、streaming流式、composable可组合的编程模型。二、两个原语与四类组件为了强制实现松散耦合Pipes 规定组件之间只能通过两条命令通信yield向下游发送输出数据awaiting从上游接收输入数据。围绕这两条命令Pipes 定义了四类组件T后缀为更通用的 monad-transformer 版本组件能力可用操作Producer/ProducerT只向下游 yieldProducer.yield、ProducerT.yieldConsumer/ConsumerT只从上游 awaitingConsumer.awaiting、ConsumerT.awaitingPipe/PipeT既能 await 又能 yieldPipe.awaiting、PipeT.awaiting、Pipe.yield、PipeT.yieldEffect/EffectT既不 yield 也不 await建模非流式组件只有绑定值AT后缀类型ProducerT、ConsumerT、PipeT、EffectT是更通用的 monad-transformer可以把任意单子M提升进来用M的行为补充 Pipes 的行为。非T后缀类型Producer、Consumer、Pipe、Effect只支持提升EffRT, A用起来更简单但灵活性稍低。从 Pipe.cs 的注释可以确认Pipe只提升Eff单子。连接与运行Pipes 使用**参数化多态泛型**重载所有操作。操作符|把Producer、Consumer、Pipe“融合fuse”在一起最终融合成一个Effect或EffectT只有融合成Effect/EffectT之后才能调用.Run()启动管线。Producer、ProducerT、Consumer、ConsumerT、Pipe、Effect、EffectT全都是同一个底层类型PipeT的特例。三、统一底层类型PipeTPipeT是所有 Pipes 组件的唯一底层形态其形状如下PipeTIN, OUT, M, A Upstream | Downstream --------- | | IN --► --► OUT -- 信息向下游流动 | | | ----|---- | A源码定义位于 PipeT.cspublic abstract record PipeTIN, OUT, M, A : KPipeTIN, OUT, M, A其中IN要消费的值的类型OUT要产生的值的类型M被提升的单子类型约束为MonadIOMA绑定值类型。类型同义词用Unit和Void封闭两端Pipes 用类型同义词隐藏未使用的输入或输出让类型签名更干净。类型同义词分两类具体类型同义词显式封闭Proxy未使用的输入/输出和多态类型同义词不显式封闭。具体同义词用Unit封闭未使用的输入用Void无人居住的类型封闭未使用的输出EffectT显式封闭两端禁止awaiting和yieldEffectTM, A PipeTUnit, Void, M, A Upstream | Downstream --------- | | Unit --► --► Void | | | ----|---- | A源码对应EffectT.cs 中EffectTM, A(PipeTUnit, Void, M, A Proxy)。ProducerT显式封闭上游端禁止awaitingProducerTOUT, M, A PipeTUnit, OUT, M, A Upstream | Downstream --------- | | Unit --► --► OUT | | | ----|---- | A源码对应ProducerT.cs 中ProducerTOUT, M, A(PipeTUnit, OUT, M, A Proxy)。ConsumerT显式封闭下游端禁止yieldConsumerTIN, M, A PipeTIN, Void, M, A Upstream | Downstream --------- | | IN --► --► Void | | | ----|---- | A源码对应ConsumerT.cs 中ConsumerTIN, M, A(PipeTIN, Void, M, A Proxy)。三种底层运行节点PipeT的Run()语义由其内部记录类型决定见 PipeT.DSL.csPipeTPureL44-L76Run()返回M.Pure(Value)即把绑定值提升进单子PipeTFailL78-L110Run()返回M.FailA(Value)让错误沿Fallible通道传播PipeTEmptyL8-L42Run()返回M.EmptyA()空流直接结束而PipeTYield与PipeTAwait的Run()会抛出InvalidOperationException(closed)L278-L280、L312-L314——这正说明未融合的 yield/await 节点不能直接运行必须先用|融合成Effect/EffectT。四、|操作符横向融合用|组合PipeT本质上是把它们并排摆放并横向融合fusing。例如把一个ProducerT、一个PipeT、一个ConsumerT组合起来信息流动如下ProducerT PipeT ConsumerT ------------- ------------ ------------- | | | | | | Unit --► readLine --► string --► parseInt --► int --► writeLine --► Void | | | | | | | | | ------|------ ------|----- ------|------ | | | A A A融合会把中间接口全部消除只留下一个EffectTEffectT ------------------------------------- | | Unit --► readLine | parseInt | writeLine --► Void | | ------------------|------------------ | A这个EffectT可以Run()返回融合后的底层M类型如果是Effect则返回融合后的底层EffRT, A。|的源码实现|由类型上的操作符重载实现PipeT.cspublic static PipeTIN, OUT, M, A operator | (PipeTIN, OUT, M, A lhs, PipeTOUT, OUT, M, A rhs) lhs.Compose(rhs); public static ProducerTOUT, M, A operator | (ProducerTIN, M, A lhs, PipeTIN, OUT, M, A rhs) lhs.Compose(rhs); public static ConsumerTIN, M, A operator | (PipeTIN, OUT, M, A lhs, ConsumerTOUT, M, A rhs) lhs.Compose(rhs.Proxy);可以看到类型约束是精确对齐的相邻组件的输出类型必须等于下游组件的输入类型这正是依赖类型系统保证管线类型正确的手段。ProducerT | ConsumerT则直接得到EffectTProducerT.cspublic static EffectTM, A operator | (ProducerTOUT, M, A lhs, ConsumerTOUT, M, A rhs) lhs.Proxy.Compose(rhs.Proxy).ToEffect();融合的底层机制是PipeT的四个内部抽象方法PipeT.csReplaceAwait用上游 producer 替换awaiting节点ReplaceYield用下游 consumer 替换yield节点PairEachAwaitWithYield/PairEachYieldWithAwait逐个配对相邻节点的请求与产出。例如 PipeT.DSL.cs 中PipeTYield.ReplaceYield会调用下游consumer(Value)并继续融合后续节点PipeTAwait.ReplaceAwaitL300-L302则用producer().Bind(x Await(x).ReplaceAwait(producer))递归替换——这就是“中间接口被融合消除”的代码级真相。五、构建组件DSL 构造器PipeT.Module.cs 提供了所有组件共享的 DSL 构造器非T版本则在各自的 Module 类中薄封装并固定M EffRT构造器语义源码位置yield(value)向下游产出单个值PipeT.Module.cs#L22-L24yieldAll(xs)依次向下游产出IEnumerable/IAsyncEnumerable/Source/SourceT中的全部值PipeT.Module.cs#L33-L105yieldRepeat(ma)反复执行单子ma并向下游产出其绑定值PipeT.Module.cs#L114-L116awaiting()从上游等待一个值PipeT.Module.cs#L136-L138map(f)用映射函数构造一个 Pipe先 await 再 yieldPipeT.Module.cs#L159-L161mapM(f)映射函数返回单子值边处理边穿插效果PipeT.Module.cs#L171-L173filter(pred)过滤不满足谓词的值PipeT.Module.cs#L147-L149pure(value)/empty()/error(e)/fail(e)只返回绑定值 / 空流 / 以Error失败 / 以任意E失败PipeT.Module.cs#L183-L222lift(f)/liftT(f)/liftM(ma)/liftIO(ma)提升纯函数、惰性 Pipe、单子、IOPipeT.Module.cs#L232-L330repeat(ma)/repeat(schedule, ma)无限重复 / 按Schedule重复PipeT.Module.cs#L340-L365fold/foldUntil/foldWhile折叠流按Schedule或谓词决定何时把聚合值 yield 出去PipeT.Module.cs#L404-L692非T版本的封装可见 Producer.Module.csProducer.yieldRT, OUT(value)内部就是PipeT.yieldEffRT, Unit, OUT(value)Consumer.Module.cs 的Consumer.awaitingRT, IN()则是PipeT.awaitingEffRT, IN, Void().ToConsumer()。这印证了“非 T 类型只是把M固定为EffRT的薄封装”。Effect与Schedule的便捷重载融合完成后Effect/EffectT还重载了与Schedule的|操作Effect.cs、EffectT.cs例如effect | schedule表示按计划重复执行整个管线Schedule.Forever时无限重复public static EffectRT, A operator |(EffectRT, A lhs, Schedule rhs) ReferenceEquals(rhs, Schedule.Forever) ? Effect.repeat(lhs) : Effect.repeat(rhs, lhs);六、运行管线Run()融合成Effect/EffectT后即可运行EffectRT, A.Run()返回EffRT, AEffect.csEffectTM, A.Run()返回KM, A即底层单子MEffectT.cs。Run()内部最终落到PipeT.Run()PipeT.cs由各内部节点的Run语义逐层求值。Effect的Run()结果为EffRT, A之后还需要交给 RT 运行时执行例如仓库测试中的用法PipesTests.csusing var rt Runtime.New(); (merge(yieldRuntime, int(1), yieldRuntime, int(1)) | awaitingRuntime, int().Map(ignore)) .Run().As() .Run(rt, EnvIO.New()) .Ignore();七、实战示例文本文件分块流文档推荐的示例 TextFileChunkStreamExample.cs 完整演示了 Producer → Pipe → Consumer 的融合管线它把文件内容按 40 字符分块流式输出static EffectRT, Unit mainEffect(string path) FileRT.openRead(path) | StreamEffRT.read(80).ToEff() | decodeUtf8 | writeLine; static PipeRT, SeqLoanbyte, string, Unit decodeUtf8 from c in Pipe.awaitingRT, SeqLoanbyte, string() from _ in Pipe.yieldRT, SeqLoanbyte, string(Encoding.UTF8.GetString(c.ToReadOnlySpan())) select unit; static ConsumerRT, string, Unit writeLine from l in Consumer.awaitingRT, string() from _ in ConsoleRT.writeLine(l) select unit;逐段解读FileRT.openRead(path)返回ProducerRT, Stream, Unit作为文件流的数据源见 File.Eff.csStreamEffRT.read(80).ToEff()是一个Pipe把Stream切成SeqLoanbyte块。底层实现Stream.cs先从ArrayPoolbyte.Shared租借 80 字节缓冲区异步读取再用ToSeqLoanUnsafe复用缓冲池避免分配读取完毕归还.ToEff()来自 PipeT.Extensions.cs把通用PipeTIN, OUT, EffRT, A降为PipeRT, IN, OUT, AdecodeUtf8用 LINQ 查询语法from/select对应Bind/Map实现Pipe组件await 一块字节yield 解码后的字符串writeLine是Consumerawait 一行并写到控制台。注意整个mainEffect通过|把四个组件融合为EffectRT, Unit全程没有显式 for 循环、没有一次性载入内存效果文件 IO、控制台 IO被声明式地穿插在流式处理中——这正是文档所说“三者兼得”的直接体现。八、更高阶的组合merge与ConduitPipes 生态还提供了跨流组合能力。ProducerT.mergeProducerT.Module.cs可以把多个 producer 并行合并成一个public static ProducerTOUT, M, Unit mergeOUT, M( SeqProducerTOUT, M, Unit producers, BufferOUT? settings null) where M : MonadUnliftIOM { if (producers.Count 0) return pureOUT, M, Unit(default); return from conduit in Pure(Conduit.make(settings ?? BufferOUT.Unbounded)) from signal in Signal.countdownM(producers.Count) from forks in forkEffects(producers, signal, conduit) from _ in conduit.ToProducerTM() from x in forks.Traverse(f f.Cancel).As() select unit; }其实现依赖Conduit——一个“带内部队列的通道”Conduit.cssink 侧是反变Comap/CoTransformsource 侧是协变Map/Transform内部队列由System.Threading.Channels.Channel支撑。Conduit.make支持多种缓冲策略Conduit.Module.cs 与 Buffer.csBufferA策略行为Channel 配置BufferA.Unbounded默认无界队列CreateUnboundedBufferA.Bounded(size)有界队列满则等待BoundedChannelOptions(size)FullMode.WaitBufferA.Single()容量 1满则等待容量 1 WaitBufferA.Latest(initial)只保留最新值以initial起步永不满不空容量 1 DropOldestBufferA.Newest(size)保留最新size个溢出丢弃最旧容量sizeDropOldestBufferA.New()只保留最新一个容量 1 DropOldest通过Conduit.ToProducerTT()/ToConsumerTT()Conduit.cs可以把通道两端接入 Pipes 组件实现跨线程的流式互通——这是 Pipes 统一机制之外的又一个扩展点。九、总结language-ext 的Pipes用“一个底层类型 两个原语 一个融合操作符”实现了流式、效果与可组合性的统一统一底层ProducerT/ConsumerT/PipeT/EffectT都是PipeTIN, OUT, M, A的类型同义词Unit/Void负责封闭未使用的端口两原语驱动yield与awaiting是组件间仅有的两种通信方式天然保证解耦|横向融合通过Compose与内部ReplaceAwait/ReplaceYield机制消除中间接口最终产出可Run()的Effect/EffectT效果原生M EffRT时可直接穿插文件、控制台等 IO 效果参见 File.Eff.cs、Console.Eff.cs 中readLines等现成的Producer组件且所有操作都在常量内存中完成。想深入了解实现细节建议按顺序阅读 PipeT.cs、PipeT.DSL.cs、PipeT.Module.cs 三个核心文件再配合 PipesTests.cs 与 EffectsExamples 示例动手实验。赞分享后端【免费下载链接】language-extC# pure functional programming framework - come and get declarative!项目地址https://gitcode.com/gh_mirrors/la/language-ext点击查看免费下载相关推荐language-ext Effects 深度指南IO、Eff 与 EffRT, A 三大副作用封装单子全解析language ext Effects 深度指南IO、Eff 与 EffRT, A 三大副作用封装单子全解析 本文基于 LanguageExt.Core/后端Nintendo Switch特斯拉菜单终极指南如何快速配置Tesla-Menu覆盖菜单Nintendo Switch特斯拉菜单终极指南如何快速配置Tesla Menu覆盖菜单 Tesla Menu是Nintendo Switch自制系统中不可或嵌入式免费开源小说阅读器终极指南5分钟快速上手Uncle小说PC版免费开源小说阅读器终极指南5分钟快速上手Uncle小说PC版 还在为找不到心仪小说而烦恼吗Uncle小说PC版是你的一站式解决方案这款免费开源的小说下载阅桌面应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

信号与系统实验报告全链路:Python实现波形生成、卷积、频谱分析与采样定理验证

信号与系统实验报告全链路:Python实现波形生成、卷积、频谱分析与采样定理验证

简介:这份《信号与系统》实验报告来自广东工业大学信息工程学院,面向电子信息类专业学生及需要完成同类课程实验的学习者,帮助系统梳理MATLAB环境下的信号分析与系统仿真流程。报告围绕连续与离散时间信号展开,涵盖基本信号生成与…

2026/10/15 10:34:29 阅读更多 →
AI Agent Harness Engineering 未来三年十大技术突破预测:TaoToken 统一 Key 通道下的工程化落地路径

AI Agent Harness Engineering 未来三年十大技术突破预测:TaoToken 统一 Key 通道下的工程化落地路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 10:34:29 阅读更多 →
grep context-mode实战:用-A -B -C参数快速定位日志与代码问题

grep context-mode实战:用-A -B -C参数快速定位日志与代码问题

排查线上问题时,我经常在服务器上对着日志发呆。搜到一个报错码,只看到一行孤零零的ERROR,前后发生了什么完全不知道,只好重新翻日志、加过滤器、再跑一遍命令,往往来回折腾好几分钟。后来我把grep的context-mode用熟了…

2026/10/15 10:34:29 阅读更多 →

最新新闻

SpringBoot集成Activiti 7.x工作流引擎实战指南

SpringBoot集成Activiti 7.x工作流引擎实战指南

1. 项目背景与核心价值 工作流引擎是现代企业级应用开发中不可或缺的基础设施。Activiti作为目前Java生态中最成熟的开源工作流引擎之一,其7.x版本在云原生适配和API简化方面做出了重大改进。而SpringBoot的自动化配置特性,恰好能解决传统工作流系统集成…

2026/10/15 11:27:08 阅读更多 →
DeepSeek-R1推理模型与通用模型选型及提示语策略

DeepSeek-R1推理模型与通用模型选型及提示语策略

简介:《DeepSeek从入门到精通》是清华大学新闻与传播学院元宇宙文化实验室编写的PDF学习资料,面向希望系统掌握DeepSeek的AI初学者、深度用户与开发者,帮助读者理解DeepSeek-R1开源推理模型的定位,并学会在实际工作中高效调用其能…

2026/10/15 11:27:08 阅读更多 →
SQLite FTS5 context-mode:上下文感知全文检索实战指南

SQLite FTS5 context-mode:上下文感知全文检索实战指南

1. 什么是 context-mode:一个被误读却极具实操价值的 SQLite 高级查询模式 “context-mode”这个词最近在开发者社区里频繁出现,但翻遍 SQLite 官方文档、FTS5 手册甚至 Stack Overflow 的高赞回答,你都找不到一个叫 “context-mode” 的正式…

2026/10/15 11:27:08 阅读更多 →
HoRain云--OpenClaw云助手:AI配置全攻略与TaoToken统一Key接入实践

HoRain云--OpenClaw云助手:AI配置全攻略与TaoToken统一Key接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 11:27:08 阅读更多 →
【A11】0 号用户叙述笔记

【A11】0 号用户叙述笔记

0 号用户(admin / admin11)是平台的"创世种子"——它存在,但不统治。它的使命是让系统能够不需要它。一、基本定义项目值用户 ID0用户名admin密码admin11(代码硬编码初始值,数据库可改)本质双层身…

2026/10/15 11:27:08 阅读更多 →
轻量级OpenPose:CPU实时2D多人姿态估计的算法解析与工程落地

轻量级OpenPose:CPU实时2D多人姿态估计的算法解析与工程落地

如果你手头正好有一个“在CPU上把2D多人姿态估计跑起来”的需求,那么看到“Real-time 2D Multi-Person Pose Estimation on CPU:Lightweight OpenPose”这个标题时,大概率会有一种“终于等到”的感觉。姿态估计这两年很火,但绝大多…

2026/10/15 11:26:07 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →