You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET实现不拆分EGC/TextElement视觉字符的String.Split方案咨询

问题背景

常规String.Split方法以"👩"为分隔符拆分"👨‍👩‍👧‍👦"时,通常会返回["👨‍"; "‍👧‍👦"],直接破坏了完整的视觉字符。
我需要实现一个拆分函数,在上述场景下返回["👨‍👩‍👧‍👦"],不会拆分完整的视觉字符。这类“视觉字符”在.NET生态中叫做TextElements,在其他技术领域称为EGC(扩展字形簇,extended grapheme clusters)。
相关实现可以参考.NET内置的System.Globalization.TextElementEnumerator能力。

现有实现

我目前写了一个可用但不够完善的F#实现:

let ecgStringSplit'
  (elements : string list)
  (sep : string list)
  : string list =
  let startsWithSeparator (str : string list) =
    sep = (str |> List.truncate sep.Length)

  let mutable result = []
  let mutable inProgress = []

  let rec recursiveFn (elementsRemaining : string list) : unit =
    if elementsRemaining = [] then
      result <- result @ [ inProgress |> String.concat "" ]
    elif startsWithSeparator elementsRemaining then
      result <- result @ [ inProgress |> String.concat "" ]
      inProgress <- []
      recursiveFn (List.skip sep.Length elementsRemaining)
    else
      inProgress <- inProgress @ [ elementsRemaining.Head ]
      recursiveFn elementsRemaining.Tail

  recursiveFn elements

  result

let toEgcList (s : string) : string list =
  seq {
    let tee = System.Globalization.StringInfo.GetTextElementEnumerator(s)

    while tee.MoveNext() do
      yield tee.GetTextElement()
  } |> Seq.toList

let ecgStringSplit (str: string) (sep: string) =
  ecgStringSplit' (toEgcList str) (toEgcList sep)

// 测试用例
ecgStringSplit "Hello world 1" " " |> printfn "%A" // 输出 ["Hello"; "world"; "1"]
ecgStringSplit "👨‍👩‍👧‍👦" "👩" |> printfn "%A" // 输出 ["👨‍👩‍👧‍👦"]
优化诉求
  • 需要重构现有实现,去除可变值等不优雅的写法
  • 希望找到更简洁的实现方案,C#版本也可以,可自行移植到F#
  • 性能优化提示:如果不使用F#链表类型,实现性能会有明显提升

优化方案

现有实现的核心思路完全正确:先把原字符串和分隔符都拆成EGC序列,再在EGC维度上做子串匹配拆分,从根源上避开了直接按UTF-16码元拆分破坏emoji的问题。

无可变值的F#优化实现

通过递归传参累加器的写法可以完全去掉可变变量,同时用数组代替F#链表、用Span做切片减少性能损耗:

open System
open System.Globalization

// 拆分字符串为EGC(文本元素)序列
let toEgcEnumerable (s: string) =
    seq {
        let en = StringInfo.GetTextElementEnumerator(s)
        while en.MoveNext() do
            yield en.GetTextElement() :?> string
    }

let egcSplit (str: string) (sep: string) =
    if String.IsNullOrEmpty(str) then []
    elif String.IsNullOrEmpty(sep) then [str]
    else
        let strEgc = toEgcEnumerable str |> Array.ofSeq
        let sepEgc = toEgcEnumerable sep |> Array.ofSeq
        let sepLen = sepEgc.Length

        // 递归处理,res存最终结果,startIdx标记当前片段起始位置
        let rec loop (startIdx: int) (curIdx: int) (res: ResizeArray<string>) =
            if curIdx > strEgc.Length - sepLen then
                // 剩余长度不足分隔符长度,直接拼接剩余内容加入结果
                res.Add(String.Join("", strEgc.AsSpan(startIdx)))
                res :> seq<string> |> List.ofSeq
            else
                // 检查当前位置是否匹配分隔符
                let mutable matchSep = true
                for i in 0..sepLen-1 do
                    if strEgc[curIdx + i] <> sepEgc[i] then
                        matchSep <- false
                if matchSep then
                    // 匹配到分隔符,切割当前片段
                    res.Add(String.Join("", strEgc.AsSpan(startIdx, curIdx - startIdx)))
                    loop (curIdx + sepLen) (curIdx + sepLen) res
                else
                    loop startIdx (curIdx + 1) res
        loop 0 0 (ResizeArray())

// 测试
egcSplit "Hello world 1" " " |> printfn "%A" // 输出 ["Hello"; "world"; "1"]
egcSplit "👨‍👩‍👧‍👦" "👩" |> printfn "%A" // 输出 ["👨‍👩‍👧‍👦"]
egcSplit "a👩b👩c" "👩" |> printfn "%A" // 输出 ["a"; "b"; "c"]

C#版本实现

逻辑完全一致,写法更直白,可直接参考移植:

using System.Globalization;
using System.Text;

public static class EgcStringExtensions
{
    public static IEnumerable<string> SplitByEgc(this string str, string separator)
    {
        if (string.IsNullOrEmpty(str))
        {
            yield break;
        }
        if (string.IsNullOrEmpty(separator))
        {
            yield return str;
            yield break;
        }

        var strEgc = GetTextElements(str).ToArray();
        var sepEgc = GetTextElements(separator).ToArray();
        int sepLen = sepEgc.Length;
        var currentSegment = new StringBuilder();

        for (int i = 0; i <= strEgc.Length - sepLen; i++)
        {
            // 检查当前位置是否匹配分隔符
            bool isMatch = true;
            for (int j = 0; j < sepLen; j++)
            {
                if (strEgc[i + j] != sepEgc[j])
                {
                    isMatch = false;
                    break;
                }
            }

            if (isMatch)
            {
                yield return currentSegment.ToString();
                currentSegment.Clear();
                i += sepLen - 1; // 跳过分隔符长度
            }
            else
            {
                currentSegment.Append(strEgc[i]);
            }
        }

        // 拼接最后一段剩余内容
        for (int i = strEgc.Length - (strEgc.Length % sepLen); i < strEgc.Length; i++)
        {
            currentSegment.Append(strEgc[i]);
        }
        yield return currentSegment.ToString();
    }

    private static IEnumerable<string> GetTextElements(string s)
    {
        var en = StringInfo.GetTextElementEnumerator(s);
        while (en.MoveNext())
        {
            yield return (string)en.GetTextElement();
        }
    }
}

实现说明

  • 所有EGC拆分逻辑都基于.NET内置的TextElementEnumerator,和系统Unicode规则保持一致,不需要自行维护复杂的Unicode码位匹配规则,兼容性最好。
  • 用数组、StringBuilder、Span代替不可变链表做存储和拼接,长文本场景下性能比初始实现提升明显。
  • 如果需要扩展支持多分隔符、大小写不敏感匹配等能力,只需要调整EGC匹配层逻辑即可,核心拆分框架不需要改动。

内容的提问来源于stack exchange,提问作者Stachu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:27:21