- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
导读
ITruncator 是 Humanizer 中负责字符串截断的核心抽象接口,它把"如何截断、截到什么程度、从哪里截断、用什么符号标记截断"这一系列问题统一收敛为一个四参数方法签名。本篇指南以该接口为骨架,结合仓库源码、五个内置实现与 TruncatorTests.cs 中的真实测试用例,完整讲解接口契约、TruncateFrom 方向枚举、五种截断策略的底层算法差异,以及如何通过扩展方法与自定义实现把截断能力接入业务代码。读完你将能够精确区分 FixedLength 与 FixedNumberOfCharacters 的字符口径差异,掌握"保留完整单词"的两种动态策略各自的适用场景,并能写出符合 ITruncator 契约的自定义实现。
ITruncator 接口契约:一个方法统一五种截断语义
接口定义位于 ITruncator.cs,全文只有一个方法,却承担着 Humanizer 全部截断语义的抽象:
public interface ITruncator { [return: NotNullIfNotNull(nameof(value))] string? Truncate(string? value, int length, string? truncationString, TruncateFrom truncateFrom = TruncateFrom.Right); }对照 API 文档(Humanizer.ITruncator.md)中的方法说明,四个参数的语义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
value | string? | 待截断的字符串,允许为 null(此时实现应返回 null) |
length | int | 截断后的目标长度;不同实现中该长度的口径不同(见下文五种策略) |
truncationString | string? | 用于标记截断位置的字符串,常见为 "…" 或 "...",允许为 null 或空串 |
truncateFrom | TruncateFrom | 枚举,决定从字符串的哪一端截断,默认值为Right |
方法返回string?,即截断后的字符串;接口上的[return: NotNullIfNotNull(nameof(value))]特性向调用方与分析器声明:只要传入的value非 null,返回值就非 null。契约中还隐含一个细节:length通常指"最终结果的总长度上限",truncationString的字符数会占用这个预算——这一点在五种实现中有着截然不同的处理方式。
TruncateFrom 枚举:截断方向
方向参数的类型定义在 TruncateFrom.cs,仅两个成员:
Left:从字符串的起始端(左侧)截断,保留尾部内容,截断标记拼在最前面;Right:从字符串的末尾端(右侧)截断,保留开头内容,截断标记拼在最后面,也是默认值。
Left方向在 UI 中常用于"保留路径的文件名部分""保留日志的结尾部分"等需要展示尾部关键信息的场景。
内置五种实现:Truncator 静态工厂
Humanizer 没有让调用方直接 new 具体实现类(多数实现类是internal的),而是通过 Truncator.cs 这个静态工厂统一暴露五个单例:
public static class Truncator { public static ITruncator FixedLength { get; } = new FixedLengthTruncator(); public static ITruncator FixedNumberOfCharacters { get; } = new FixedNumberOfCharactersTruncator(); public static ITruncator FixedNumberOfWords { get; } = new FixedNumberOfWordsTruncator(); public static ITruncator DynamicLengthAndPreserveWords { get; } = new DynamicLengthAndPreserveWordsTruncator(); public static ITruncator DynamicNumberOfCharactersAndPreserveWords { get; } = new DynamicNumberOfCharactersAndPreserveWordsTruncator(); }五个实现覆盖了两种维度:计数口径(按字符长度 / 按字母数字个数 / 按单词数)与是否保留完整单词(固定硬切 / 动态回退到词边界)。下面逐一拆解其算法与行为差异。
1. FixedLengthTruncator:按"字符总长度"硬截断
源码见 FixedLengthTruncator.cs,逻辑最直白:
value为 null 返回 null;空串或长度不超过length时原样返回;- 若
truncationString为 null 或其长度超过length,则完全忽略标记,直接取value[..length](右侧截断)或value[^length..](左侧截断); - 否则按方向拼接:右侧截断为
value[..(length - truncationString.Length)] + truncationString,左侧截断为truncationString + value[(value.Length - length + truncationString.Length)..]。
注意它统计的是字符串的Length(UTF-16 代码单元数),不区分字母、数字、空白或标点。测试 TruncatorTests.cs 验证了标记过长时的兜底行为:"Text with delimiter length greater than truncate length truncates to fixed length without truncation string".Truncate(2, "...")输出"Te",即标记 "..." 比预算还长时直接丢弃标记。
2. FixedNumberOfCharactersTruncator:只数"字母与数字"
源码见 FixedNumberOfCharactersTruncator.cs,与 FixedLength 的关键差异在于:length只统计字母和数字(char.IsLetterOrDigit),空格、标点、符号不占预算。算法先正向扫描计数,若字母数字总数超过length才截断;截断时从两端向中间数到预算位置落刀。
这一点在测试中有鲜明对比(TruncatorTests.cs):"Text with more characters than truncate length".Truncate(10, Truncator.FixedNumberOfCharacters)输出"Text with m…"——10 个字符中只有字母数字计入,所以保留的原文比 FixedLength 的"Text long…"更长。适合对"可见字符密度"敏感、希望空格不挤占内容的排版场景。
3. FixedNumberOfWordsTruncator:按单词数量截断
源码见 FixedNumberOfWordsTruncator.cs,length的单位是单词个数,空白(char.IsWhiteSpace)是分词符。实现先无分配地扫描统计单词数,若超出预算,再从对应方向数到第length个单词的边界处拼接标记。测试证明换行、回车、制表符都能正确分词(TruncatorTests.cs):
"Text with more words than truncate length".Truncate(4, Truncator.FixedNumberOfWords) => "Text with more words…" "Words are\nsplit\rby\twhitespace".Truncate(4, Truncator.FixedNumberOfWords) => "Words are\nsplit\rby…"注意 FixedNumberOfWords 允许"中间某个单词被截断"吗?不会——它恰好在单词边界落刀,因此单词不会残缺,但单词内部不含空格的长串会被整体保留,可能出现输出超过目标字符预算的情况。这是"按词截断"与"按字符截断"天然的口径差异。
4. DynamicLengthAndPreserveWordsTruncator:字符预算 + 完整单词
源码见 DynamicLengthAndPreserveWordsTruncator.cs,它把前两种策略的优点结合:以字符长度(value.Length)为预算,但截断点落在单词中间时回退到最近的空白边界,丢弃被切到的整个单词。
核心逻辑在TruncateFromRight:先算出effectiveLength = length - truncationString.Length,若该位置不是空白(即落在词中间),用LastIndexOf(' ', effectiveLength)回退到前一个空格;找不到空格说明没有任何完整单词能放下,只返回截断标记。左侧截断TruncateFromLeft同理,从尾部反向扫描,若候选单词超长则只返回标记。
测试中的两个边界案例非常直观(TruncatorTests.cs):
"Text longer than truncate length".Truncate(10, Truncator.DynamicLengthAndPreserveWords) => "Text…" // "longer" 被切到一半,整个单词丢弃,回退到 "Text" "A Text with delimiter length less than truncate length and the last word fit".Truncate(2, ...) => "A…" // 预算只够放一个单词5. DynamicNumberOfCharactersAndPreserveWordsTruncator:字母数字预算 + 完整单词
源码见 DynamicNumberOfCharactersAndPreserveWordTruncator.cs(文件名注意为单数 PreserveWord),这是 API 文档中唯一被列为 ITruncator 派生类型的实现,也是语义最精细的一个:length只统计字母数字,同时保证单词完整。实现先统计全文字母数字总数totalAlpha,未超预算则原样返回;截断时按字母数字计数定位候选点,若落在词中间则回退/前进到最近的空白边界。
其行为在测试中体现得最复杂(TruncatorTests.cs):
"Text with more characters than truncate length".Truncate(10, Truncator.DynamicNumberOfCharactersAndPreserveWords) => "Text…" "Text with delimiter length less than truncate length and the last word fit".Truncate(7, "...", ...) => "Text…" "Text with additional spaces and null truncate string".Truncate(10, null, ...) => "Text with" // 空格不占字母数字预算,因此得以保留选择建议:需要严格控制结果字符总长度时用 FixedLength / DynamicLengthAndPreserveWords;需要控制可见字符密度、允许空白"免费"时用两个 NumberOfCharacters 变体;按语义单位(句子/短语)截断时用 FixedNumberOfWords。五种实现彼此独立,复杂度与行为都不同,适合在不同 UI 约束下替换。
扩展方法入口:一行代码完成截断
虽然 ITruncator 是核心抽象,日常使用几乎总是经由 TruncateExtensions.cs 的四个重载进入,它们在内部把默认值补齐后转发给 ITruncator:
// 1. 最简单形式:默认 "…" + FixedLength + 右侧截断 "This is a long string".Truncate(10); // "This is a…" // 2. 指定截断器,默认 "…" "Text with more words than truncate length".Truncate(4, Truncator.FixedNumberOfWords); // "Text with more words…" // 3. 指定自定义截断标记,默认 FixedLength "Text longer than truncate length".Truncate(10, "..."); // "Text lo..." "Text longer than truncate length".Truncate(10, "…", TruncateFrom.Left); // "…ng length" // 4. 全参数:标记 + 截断器 + 方向(最灵活) "This is a long string".Truncate(10, "…", Truncator.FixedNumberOfWords, TruncateFrom.Left); // "… string"四个重载的默认值链条(见 TruncateExtensions.cs)依次为:截断标记默认"…"(Unicode 省略号)、截断器默认Truncator.FixedLength、方向默认TruncateFrom.Right。底层最终都汇聚到四参数重载,其中truncator参数通过ArgumentNullException.ThrowIfNull做了空引用校验。所有重载对 null 输入都返回 null,对不超过预算的字符串都原样返回——这是 Humanizer 截断 API 一贯的"幂等友好"设计。
自定义 ITruncator:实现你自己的截断策略
内置五种策略无法覆盖需求时,实现 ITruncator 只需满足一个方法。一个典型的自定义场景是"按标点符号断句 + 限定句数",示例骨架:
public sealed class SentenceTruncator : ITruncator { [return: NotNullIfNotNull(nameof(value))] public string? Truncate(string? value, int length, string? truncationString, TruncateFrom truncateFrom = TruncateFrom.Right) { if (value is null) return null; if (value.Length <= length || length <= 0) return value; // 示例:从右向左保留最后 length 个以句号结尾的句子 var segments = value.Split('.', StringSplitOptions.RemoveEmptyEntries); if (segments.Length <= length) return value; var kept = segments[^length..]; return string.Join('.', kept) + truncationString; } }随后通过扩展方法重载接入统一入口:
"第一句。第二句。第三句。".Truncate(2, "...", new SentenceTruncator(), TruncateFrom.Right);自定义实现需要注意与内置实现保持一致的两条约定:null 输入返回 null;输出总长(含 truncationString)尽量不超过length。接口本身不强制这两条,但遵循它们才能让自定义截断器在 Humanizer 的调用链中表现可预期。参考实现可对照 ITruncator.cs 与五个内置类的空值/边界处理模式。
源码级行为验证:边界条件一览
从 TruncatorTests.cs 可以沉淀出所有内置实现共同遵守的行为矩阵,这也是阅读本文后最值得记住的部分:
- null 输入 → null 输出:五种实现与所有扩展方法重载一致;
- 空串 → 原样返回空串;
- 长度未超预算 → 原样返回,不追加任何标记;
- truncationString 为 null 或超长 → 按纯子串截断:FixedLength 直接
value[..length],FixedNumberOfCharacters 直接切满length个字母数字,Dynamic 系列则返回空串或仅返回标记(见 TruncatorTests.cs 的"Te"案例与 #L157 的""案例); - 预算不足以容纳任何完整单词 → Dynamic 系列仅返回截断标记,如
"…"; length计入 truncationString 的字符数:五种实现都从预算中扣除标记长度,只是 FixedNumberOfCharacters 与 DynamicNumberOfCharactersAndPreserveWords 的预算只统计字母数字。
这六条规则配合上面各小节的测试样例,构成了 ITruncator 契约从接口签名到具体实现、再到扩展方法入口的完整闭环。在需要为项目挑选截断策略、或排查"为什么截断结果多了/少了几个字符"时,可以按"计数口径(字符/字母数字/单词)→ 是否保留完整单词 → 方向"三步快速定位对应实现。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer ITruncator 接口深度解析:字符串截断的完整指南与五种内置实现
Humanizer ITruncator 接口深度解析:字符串截断的完整指南与五种内置实现 导读 本文围绕 Humanizer 库的 ITruncator 接口
开发工具Humanizer 字符串截断指南:TruncateExtensions 与 ITruncator 深度解析
Humanizer 字符串截断指南:TruncateExtensions 与 ITruncator 深度解析 TruncateExtensions 是 Huma
开发工具深入理解 Humanizer 的 ITruncator 接口:可插拔的字符串截断引擎
深入理解 Humanizer 的 ITruncator 接口:可插拔的字符串截断引擎 本篇文章以 Humanizer 的 ITruncator 接口为核心,系统
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考