有没有一种方法可以在不编写自己的循环代码的情况下,在C#中获取字符串中第一个非空白字符的索引(或更一般地匹配条件的第一个字符的索引)?
编辑
通过“编写自己的循环代码”,我实际上是指我正在寻找一种简洁的表达式,它可以解决问题而不会使我正在处理的逻辑混乱。
对此造成的任何困惑,我深表歉意。
有没有一种方法可以在不编写自己的循环代码的情况下,在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;
}
string str = " Hello World";
int i = str.IndexOf<char>(c => !char.IsWhiteSpace(c));
str.IndexOf(...)
将使用在框架中定义的非泛型版本(我已编辑答案以调用泛型版本 :-) - Eric J.<char>
并不是必需的。但是像 IndexOfWhere()
这样的名称可能更清晰明了。 - H Hstring s= " \t Test";
Array.FindIndex(s.ToCharArray(), x => !char.IsWhiteSpace(x));
返回6
要添加一个条件,只需执行...
Array.FindIndex(s.ToCharArray(), x => !char.IsWhiteSpace(x) && your condition);
ReadOnlySpan
更加高效:
string s = " xyz";
int index = s.Length - s.AsSpan().TrimStart().Length;
// index is 3
.AsSpan()
和.TrimStart()
都不会创建字符串的副本,它们只是存储一个对字符串字符和长度的引用。
.AsSpan()
是String
的扩展方法,它创建了一个指向字符串第一个字符的范围。它的长度是整个字符串的长度。.TrimStart()
是ReadOnlySpan<char>
的扩展方法,它创建了一个指向第一个非空格字符的范围。它的长度是整个字符串的长度减去第一个非空格字符的位置。这种模式可以通用地用于跳过任何给定字符的列表:
string s = "foobar";
int index = s.Length - s.AsSpan().TrimStart("fo").Length;
// index is 3
我对这种方法以及此Q&A中的其他几种方法进行了基准测试,使用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微秒 |
FirstNonMatch | 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
一样快(因为静态 Regex
方法缓存编译模式),但是由于 BenchmarkDotNet 每次测试运行都创建一个新进程,所以该缓存没有任何效果。
String_Trim
基准测试取决于第一个非空白字符之后有多少个字符,因为它会复制子字符串。对于短文本,性能可能接近于 ReadOnlySpan_Trim
,但对于长文本,性能将会更差。该基准测试的输入文本包含 50k 个非空白字符,因此已经存在显着差异。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 ");
var index = match.Success ? match.Index : -1;
(如果不匹配,则使用(int?)null
或者default(int?)
,如果你喜欢null
而不是-1
)。@lcfseth 很好的观点。提问者要求不要用循环,“更一般地说,是第一个满足条件的字符的索引”,这看起来更像是正则表达式。对于像“空格”这样简单的情况,我同意你的看法,但在其他情况下,这可能很有用。 - Tim S.new Regex(@"\S")
不会创建编译的正则表达式,但是 new Regex(@"\S", RegexOptions.Compiled)
会。 - zett42由于这里有几个解决方案,所以我决定进行一些性能测试,以查看每个解决方案的表现。决定将这些结果分享给那些感兴趣的人...
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 Test" 时
将其转换为字符数组并使用 FindIndex: 154
修剪空格并获取第一个字符的索引: 189
使用扩展方法: 234
循环: 146
当 s = "Test" 时
将其转换为字符数组并使用 FindIndex: 39
修剪空格并获取第一个字符的索引: 155
使用扩展方法: 57
循环: 15
当 s =(没有空格的1000个字符的字符串)
将其转换为字符数组并使用 FindIndex: 506
修剪空格并获取第一个字符的索引: 534
使用扩展方法: 51
循环: 15
当 s =(以“ \t Test”开头的1000个字符的字符串)
将其转换为字符数组并使用 FindIndex: 609
修剪空格并获取第一个字符的索引: 1103
使用扩展方法: 226
循环: 146
自行得出结论,但我的结论是使用您最喜欢的方法,因为在实际场景中性能差异微不足道。
这里有很多将字符串转换为数组的解决方案。然而,这并不是必要的。一个字符串中的单个字符可以像数组中的元素一样访问。
以下是我认为非常高效的解决方案:
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);
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;
}
);
return (x == 's');
。 - Eric J.