繁体   English   中英

获取 C# 字符串中第一个非空白字符的索引

[英]Get Index of First non-Whitespace Character in C# String

有没有办法在不编写我自己的循环代码的情况下获取 C# 中字符串中第一个非空白字符的索引(或者更一般地说,第一个匹配条件的字符的索引)?

编辑

通过“编写我自己的循环代码”,我的意思是我正在寻找一个紧凑的表达式来解决问题而不会使我正在处理的逻辑混乱。

对于这一点上的任何混淆,我深表歉意。

string当然是IEnumerable<char>所以你可以使用Linq:

int offset = someString.TakeWhile(c => char.IsWhiteSpace(c)).Count();

我喜欢定义自己的扩展方法,用于返回满足序列中自定义谓词的第一个元素的索引。

/// <summary>
/// Returns the index of the first element in the sequence 
/// that satisfies a condition.
/// </summary>
/// <typeparam name="TSource">
/// The type of the elements of <paramref name="source"/>.
/// </typeparam>
/// <param name="source">
/// An <see cref="IEnumerable{T}"/> that contains
/// the elements to apply the predicate to.
/// </param>
/// <param name="predicate">
/// A function to test each element for a condition.
/// </param>
/// <returns>
/// The zero-based index position of the first element of <paramref name="source"/>
/// for which <paramref name="predicate"/> returns <see langword="true"/>;
/// or -1 if <paramref name="source"/> is empty
/// or no element satisfies the condition.
/// </returns>
public static int IndexOf<TSource>(this IEnumerable<TSource> source, 
    Func<TSource, bool> predicate)
{
    int i = 0;

    foreach (TSource element in source)
    {
        if (predicate(element))
            return i;

        i++;
    }

    return -1;
}

然后,您可以使用LINQ来解决原始问题:

string str = "   Hello World";
int i = str.IndexOf<char>(c => !char.IsWhiteSpace(c));
string s= "   \t  Test";
Array.FindIndex(s.ToCharArray(), x => !char.IsWhiteSpace(x));

返回6

要添加条件,只需...

Array.FindIndex(s.ToCharArray(), x => !char.IsWhiteSpace(x) && your condition);

您可以使用String.IndexOfAny函数,该函数返回指定Unicode字符数组中任何字符的第一个匹配项。

或者,您可以使用String.TrimStart函数从字符串的开头删除所有空白字符。 第一个非空白字符的索引是原始字符串和修剪字符串的长度之间的差异。

你甚至可以选择一组字符来修剪:)

基本上,如果你正在寻找一组有限的字符(让我们说数字)你应该采用第一种方法。

如果您试图忽略一组有限的字符(如空格),则应使用第二种方法。

最后一种方法是使用Linq方法:

string s = "        qsdmlkqmlsdkm";
Console.WriteLine(s.TrimStart());
Console.WriteLine(s.Length - s.TrimStart().Length);
Console.WriteLine(s.FirstOrDefault(c => !Char.IsWhiteSpace(c)));
Console.WriteLine(s.IndexOf(s.FirstOrDefault(c => !Char.IsWhiteSpace(c))));

输出:

qsdmlkqmlsdkm
8
q
8
var match = Regex.Match(" \t test  ", @"\S"); // \S means all characters that are not whitespace
if (match.Success)
{
    int index = match.Index;
    //do something with index
}
else
{
    //there were no non-whitespace characters, handle appropriately
}

如果您经常这样做,出于性能原因,您应该为此模式缓存已编译的Regex ,例如:

static readonly Regex nonWhitespace = new Regex(@"\S");

然后使用它像:

nonWhitespace.Match(" \t test  ");

由于这里有几个解决方案,我决定进行一些性能测试,看看每个解决方案的表现如何。 决定为那些感兴趣的人分享这些结果......

    int iterations = 1000000;
    int result = 0;
    string s= "   \t  Test";

    System.Diagnostics.Stopwatch watch = new Stopwatch();

    // Convert to char array and use FindIndex
    watch.Start();
    for (int i = 0; i < iterations; i++)
        result = Array.FindIndex(s.ToCharArray(), x => !char.IsWhiteSpace(x)); 
    watch.Stop();
    Console.WriteLine("Convert to char array and use FindIndex: " + watch.ElapsedMilliseconds);

    // Trim spaces and get index of first character
    watch.Restart();
    for (int i = 0; i < iterations; i++)
        result = s.IndexOf(s.TrimStart().Substring(0,1));
    watch.Stop();
    Console.WriteLine("Trim spaces and get index of first character: " + watch.ElapsedMilliseconds);

    // Use extension method
    watch.Restart();
    for (int i = 0; i < iterations; i++)
        result = s.IndexOf<char>(c => !char.IsWhiteSpace(c));
    watch.Stop();
    Console.WriteLine("Use extension method: " + watch.ElapsedMilliseconds);

    // Loop
    watch.Restart();
    for (int i = 0; i < iterations; i++)
    {   
        result = 0;
        foreach (char c in s)
        {
            if (!char.IsWhiteSpace(c))
                break;
            result++;
        }
    }
    watch.Stop();
    Console.WriteLine("Loop: " + watch.ElapsedMilliseconds);

结果以毫秒为单位....

其中s =“\\ t测试”
转换为char数组并使用FindIndex: 154
修剪空格并获取第一个字符的索引: 189
使用扩展方法: 234
循环: 146

其中s =“测试”
转换为char数组并使用FindIndex: 39
修剪空格并获取第一个字符的索引: 155
使用扩展方法: 57
循环: 15

其中s =(1000字符串没有空格)
转换为char数组并使用FindIndex: 506
修剪空格并获取第一个字符的索引: 534
使用扩展方法: 51
循环: 15

其中s =(1000字符串以“\\ t Test”开头)
转换为char数组并使用FindIndex: 609
修剪空格并获取第一个字符的索引: 1103
使用扩展方法: 226
循环: 146

得出你自己的结论,但我的结论是使用你最喜欢的那个,因为在现实世界的场景中,性能差异是微不足道的。

受此修剪字符串解决方案的启发,但使用ReadOnlySpan效率更高

string s = "   xyz";
int index = s.Length - s.AsSpan().TrimStart().Length;
// index is 3

.AsSpan().TrimStart() ) 都不会创建字符串的副本,它们只是存储对字符串字符和长度的引用

  • .AsSpan()String的扩展方法,它创建一个指向字符串第一个字符的跨度。 它的长度是字符串的总长度。
  • .TrimStart()ReadOnlySpan<char>的扩展方法,它创建一个指向第一个非空白字符的跨度。 它的长度是总字符串长度减去第一个非空白字符的 position。

此模式通常可用于跳过给定字符的任何列表:

string s = "foobar";
int index = s.Length - s.AsSpan().TrimStart("fo").Length;
// index is 3

我使用BenchmarkDotNet我的基准代码)对这个方法和这个问答中的其他几个方法进行了基准测试:

方法

意思

错误

标准偏差

Regex_Compiled 45.05 我们 0.043 我们 0.034 我们
ReadOnlySpan_Trim(这个答案) 50.24 我们 0.073 我们 0.061 我们
String_Trim 94.64 我们 0.458 我们 0.428 我们
Regex_Interpreted 114.41 我们 0.224 我们 0.210 我们
Regex_StaticMethod (阅读下文!) 114.19 我们 0.056 我们 0.046 我们
第一次不匹配 150.58 我们 0.214 我们 0.190 我们
Array_FindIndex 200.40 我们 1.951 我们 1.730 我们
StringExt_IndexOfPredicate 336.31 我们 0.896 我们 0.838 我们
Linq_TakeWhile 490.97 我们 0.994 我们 0.930 我们

没想到RegEx_Compiled最快。 实际上RegEx_StaticMethod应该与RegEx_Compiled一样执行(因为 static Regex方法缓存编译模式),但是由于 BenchmarkDotNet 在每次测试运行时创建一个新进程,该缓存没有任何影响。

String_Trim基准取决于第一个非空白字符后跟随的字符数,因为它复制了 substring。对于短文本,性能可能接近ReadOnlySpan_Trim ,但对于较长的文本,性能会差得多。 该基准测试的输入文本包含 50k 个非空白字符,因此已经存在显着差异。

您可以修剪,获取第一个字符并使用IndexOf。

有一个非常简单的解决方案

string test = "    hello world";
int pos = test.ToList<char>().FindIndex(x => char.IsWhiteSpace(x) == false);

pos将是4

你可以有更复杂的条件,如:

pos = test.ToList<char>().FindIndex((x) =>
                {
                    if (x == 's') //Your complex conditions go here
                        return true;
                    else 
                        return false;
                }
            );

是的你可以试试这个:

string stg = "   xyz";
int indx = (stg.Length - stg.Trim().Length);  

有些东西会在某个地方循环。 要完全控制什么是空白,你可以使用linq来对象进行循环:

int index = Array.FindIndex(
               s.ToCharArray(), 
               x => !(new [] { '\t', '\r', '\n', ' '}.Any(c => c == x)));

这里有很多解决方案将字符串转换为数组。 这不是必需的,字符串中的单个字符可以像数组中的项一样被访问。

这是我应该非常有效的解决方案:

private static int FirstNonMatch(string s, Func<char, bool> predicate, int startPosition = 0)
{
    for (var i = startPosition; i < s.Length; i++)
        if (!predicate(s[i])) return i;

    return -1;
}

private static int LastNonMatch(string s, Func<char, bool> predicate, int startPosition)
{
    for (var i = startPosition; i >= 0; i--)
        if (!predicate(s[i])) return i;

    return -1;
}

要使用这些,请执行以下操作:

var x = FirstNonMatch(" asdf ", char.IsWhiteSpace);
var y = LastNonMatch(" asdf ", char.IsWhiteSpace, " asdf ".Length);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM