高性能版本的零内存分配LikeString函数(ZeroMemAllocLikeOperator)

继上一篇文章在.NET Core,除了VB的LikeString,还有其它方法吗?(四种LikeString实现分享)分享了四种实现方式,笔者对这四种实现方式,不管是执行性能还是内存分配性能上,都不太满意。

那么是否有好的实现方法呢?答案是有的。

今天我们就搬出ReadOnlySpan<T>这个非常好用的结构类型,它是在 .NET Core 2.1 中新引入的类型,与它一同被引入的类型还有:

  • System.Span: 这以类型安全和内存安全的方式表示任意内存的连续部分;
  • System.ReadOnlySpan: 这表示任意连续内存区域的类型安全和内存安全只读表示形式;
  • System.Memory: 这表示一个连续的内存区域;
  • System.ReadOnlyMemory : 类似ReadOnlySpan, 此类型表示内存的连续部分ReadOnlySpan, 它不是 ByRef 类型; 注:ByRef 类型指的是 ref readonly struct

下面,我们就来看看如何实现高性能和零内存分配的 LikeString 函数吧!

csharp 复制代码
#nullable enable

using System;

namespace AllenCai
{
    /// <summary>
    /// 这是一个模仿Microsoft.VisualBasic.CompilerServices.LikeOperator.LikeString方法,<br />
    /// 实现支持*和?通配符和支持忽略大小写规则以及区域无关性的匹配。<br />
    /// 该实现的目的是为了减少内存分配,提高性能。
    /// </summary>
    public class ZeroMemAllocLikeOperator
    {
        /// <summary>
        /// 对给定的两个字符串执行比较,支持使用*和?通配符。
        /// </summary>
        public static bool LikeString(string? content, string? pattern, bool ignoreCase = true, bool useInvariantCulture = true)
        {
            if (content == null && pattern == null)
                return true;
            if (content == null || pattern == null)
                return false;

            ReadOnlySpan<char> patternSpan = pattern.AsSpan();
            ReadOnlySpan<char> contentSpan = content.AsSpan();

            return LikeString(contentSpan, patternSpan, ignoreCase, useInvariantCulture);
        }

        /// <summary>
        /// 对给定的两个字符Span执行比较,支持使用*和?通配符。
        /// </summary>
        public static bool LikeString(ReadOnlySpan<char> contentSpan, ReadOnlySpan<char> patternSpan, bool ignoreCase = true, bool useInvariantCulture = true)
        {
            char zeroOrMoreChars = '*';
            char oneChar = '?';

            // 如果pattern是由1个星号*组成,那么没必要匹配,直接返回true。
            if (patternSpan.Length == 1)
            {
                ref readonly char patternItem = ref patternSpan[0];
                if (patternItem == zeroOrMoreChars)
                {
                    return true;
                }
            }

            // 如果被匹配内容的长度只有1位,而pattern刚好也是一个问号?,那么没必要匹配,直接返回true。
            if (contentSpan.Length == 1)
            {
                ref readonly char patternItem = ref patternSpan[0];
                if (patternItem == oneChar)
                {
                    return true;
                }
            }

            // 如果pattern是由多个星号*和问号?组成,那么没必要匹配,直接返回true。
            int zeroOrMorePatternCount = 0;
            int onePatternCount = 0;
            for (int i = 0; i < patternSpan.Length; i++)
            {
                ref readonly char patternItem = ref patternSpan[i];
                if (patternItem == zeroOrMoreChars)
                {
                    zeroOrMorePatternCount++;
                }
                else if (patternItem == oneChar)
                {
                    onePatternCount++;
                }
            }
            if (zeroOrMorePatternCount + onePatternCount == patternSpan.Length)
            {
                //只要出现1个或多个星号*,那么就没必要在乎被匹配内容的长度了。
                if (zeroOrMorePatternCount > 0)
                {
                    return true;
                }

                //如果没有星号*,全是问号?,那么就检查是否由问号?组成的pattern长度是否和被匹配内容的长度一致。如果一致,没必要匹配,直接返回true。
                if (patternSpan.Length == contentSpan.Length)
                {
                    return true;
                }
            }

            // 选择合适的EqualsChar方法。
            EqualsCharDelegate equalsChar;
            if (ignoreCase)
            {
                if (useInvariantCulture)
                {
                    equalsChar = EqualsCharInvariantCultureIgnoreCase;
                }
                else
                {
                    equalsChar = EqualsCharCurrentCultureIgnoreCase;
                }
            }
            else
            {
                equalsChar = EqualsChar;
            }

            return LikeStringCore(contentSpan, patternSpan, in zeroOrMoreChars, in oneChar, equalsChar);
        }

        private static bool LikeStringCore(ReadOnlySpan<char> contentSpan, ReadOnlySpan<char> patternSpan, in char zeroOrMoreChars, in char oneChar, EqualsCharDelegate equalsChar)
        {
            // 遍历pattern,逐个字符匹配。
            int contentIndex = 0;
            int patternIndex = 0;
            while (contentIndex < contentSpan.Length && patternIndex < patternSpan.Length)
            {
                ref readonly char patternItem = ref patternSpan[patternIndex];
                if (patternItem == zeroOrMoreChars)
                {
                    // 如果pattern中的下一个字符是星号*,那么就一直往后移动patternIndex,直到找到不是星号*的字符。
                    while (true)
                    {
                        if (patternIndex < patternSpan.Length)
                        {
                            ref readonly char nextPatternItem = ref patternSpan[patternIndex];
                            if (nextPatternItem == zeroOrMoreChars)
                            {
                                patternIndex++;
                                continue;
                            }
                        }
                        break;
                    }

                    // 如果patternIndex已经到了pattern的末尾,那么就没必要再匹配了,直接返回true。
                    if (patternIndex == patternSpan.Length)
                    {
                        return true;
                    }

                    // 如果patternIndex还没到pattern的末尾,那么就从contentIndex开始匹配。
                    while (contentIndex < contentSpan.Length)
                    {
                        if (LikeStringCore(contentSpan.Slice(contentIndex), patternSpan.Slice(patternIndex), in zeroOrMoreChars, in oneChar, equalsChar))
                        {
                            return true;
                        }
                        contentIndex++;
                    }

                    return false;
                }

                if (patternItem == oneChar)
                {
                    // 如果pattern中的下一个字符是问号?,那么就匹配一个字符。
                    contentIndex++;
                    patternIndex++;
                }
                else
                {
                    // 如果pattern中的下一个字符不是星号*,也不是问号?,那么就匹配一个字符。
                    if (contentIndex >= contentSpan.Length)
                    {
                        return false;
                    }

                    ref readonly char contentItem = ref contentSpan[contentIndex];
                    if (!equalsChar(in contentItem, in patternItem))
                    {
                        return false;
                    }

                    //if (ignoreCase)
                    //{
                    //    if (char.ToUpperInvariant(contentItem) != char.ToUpperInvariant(patternItem))
                    //    {
                    //        return false;
                    //    }
                    //}
                    //else
                    //{
                    //    if (contentItem != patternItem)
                    //    {
                    //        return false;
                    //    }
                    //}

                    contentIndex++;
                    patternIndex++;
                }
            }

            // 如果content都匹配完了,而pattern还没遍历完,则检查剩余的patternItem是否都是星号*,如果是就返回true,否则返回false。
            if (contentIndex == contentSpan.Length)
            {
                // 如果pattern中的下一个字符是星号*,那么就一直往后移动patternIndex,直到找到不是星号*的字符。
                while (true)
                {
                    if (patternIndex < patternSpan.Length)
                    {
                        ref readonly char nextPatternItem = ref patternSpan[patternIndex];
                        if (nextPatternItem == zeroOrMoreChars)
                        {
                            patternIndex++;
                            continue;
                        }
                    }
                    break;
                }

                return patternIndex == patternSpan.Length;
            }

            return false;
        }

        private static bool EqualsChar(in char contentItem, in char patternItem)
        {
            return contentItem == patternItem;
        }

        private static bool EqualsCharCurrentCultureIgnoreCase(in char contentItem, in char patternItem)
        {
            return char.ToUpper(contentItem) == char.ToUpper(patternItem);
        }

        private static bool EqualsCharInvariantCultureIgnoreCase(in char contentItem, in char patternItem)
        {
            return char.ToUpperInvariant(contentItem) == char.ToUpperInvariant(patternItem);
        }

        private delegate bool EqualsCharDelegate(in char contentItem, in char patternItem);
    }
}

PS: 以上代码在 .NET Standard 2.1 项目使用,可直接编译通过。

在 .NET Standard 2.0 项目中,需要额外引入 System.Memory 这个 NuGet 包,且需要将 LangVersion(C#语言版本)更改为 8.0 或更高(通常使用defaultlatest也可以)。

相关推荐
Caramel_biscuit6 小时前
面试经典150题
面试·职场和发展·c#
WineMonk9 小时前
ArcGIS Pro SDK (八)地理数据库 6 版本控制
arcgis·c#·gis·arcgis pro sdk·地理数据库
WineMonk9 小时前
ArcGIS Pro SDK (八)地理数据库 7 DDL
arcgis·c#·gis·arcgis pro sdk·地理数据库
那个那个鱼10 小时前
C#面:依赖注入有哪些著名的框架?
开发语言·c#·.net
zls36536510 小时前
一键成神:C#自动化打包的魔法之旅
运维·开发语言·c#·自动化
是萝卜干呀11 小时前
Backend - C# 基础知识
开发语言·c#
玩c#的小杜同学11 小时前
c#获取本机的MAC地址(附源码)
开发语言·笔记·c#·linq
泰勒今天想展开14 小时前
C#-反射
java·oracle·c#
大浪淘沙胡18 小时前
C#使用异步方式调用同步方法的实现方法
数据库·c#
就是有点傻19 小时前
c#的几种通信
c#