将字符串拆分为行的最佳方法

如何将多行字符串拆分为多行?

我知道这条路

var result = input.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

看起来有点丑,而且没有空行。有更好的解决方案吗?

206843 次浏览
  • 如果它看起来很丑陋,只需删除不必要的 ToCharArray调用。

  • 如果你想被 \n或者 \r拆分,你有两个选择:

    • 使用一个数组字面值-但是这会给你一个 Windows 风格的行结尾的空行 \r\n:

      var result = text.Split(new [] { '\r', '\n' });
      
    • Use a regular expression, as indicated by Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
      
  • If you want to preserve empty lines, why do you explicitly tell C# to throw them away? (StringSplitOptions parameter) – use StringSplitOptions.None instead.

你可以使用正则表达式。分割:

string[] tokens = Regex.Split(input, @"\r?\n|\r");

编辑: 添加了 |\r来解决(旧的) Mac 行终止符。

如果要保留空行,只需删除 StringSplitOptions。

var result = input.Split(System.Environment.NewLine.ToCharArray());

稍微有点扭曲,但是有一个迭代器块可以做到这一点:

public static IEnumerable<string> Lines(this string Text)
{
int cIndex = 0;
int nIndex;
while ((nIndex = Text.IndexOf(Environment.NewLine, cIndex + 1)) != -1)
{
int sIndex = (cIndex == 0 ? 0 : cIndex + 1);
yield return Text.Substring(sIndex, nIndex - sIndex);
cIndex = nIndex;
}
yield return Text.Substring(cIndex + 1);
}

你可致电:

var result = input.Lines().ToArray();
using (StringReader sr = new StringReader(text)) {
string line;
while ((line = sr.ReadLine()) != null) {
// do something
}
}
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

更新: 有关替代/异步解决方案,请参见 给你


这种方法效果很好,而且比正则表达式更快:

input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)

在数组中首先使用 "\r\n"非常重要,这样就可以将它作为一个换行符。上面给出的结果与这两种正则表达式解决方案中的任何一种都相同:

Regex.Split(input, "\r\n|\r|\n")


Regex.Split(input, "\r?\n|\r")

除了正则表达式比正则表达式慢10倍以外,下面是我的测试:

Action<Action> measure = (Action func) => {
var start = DateTime.Now;
for (int i = 0; i < 100000; i++) {
func();
}
var duration = DateTime.Now - start;
Console.WriteLine(duration);
};


var input = "";
for (int i = 0; i < 100; i++)
{
input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}


measure(() =>
input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)
);


measure(() =>
Regex.Split(input, "\r\n|\r|\n")
);


measure(() =>
Regex.Split(input, "\r?\n|\r")
);

产出:

00:00:03.8527616

00:00:31.8017726

00:00:32.5557128

这是 扩展方法:

public static class StringExtensionMethods
{
public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
{
return str.Split(new[] { "\r\n", "\r", "\n" },
removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
}
}

用法:

input.GetLines()      // keeps empty lines


input.GetLines(true)  // removes empty lines
    private string[] GetLines(string text)
{


List<string> lines = new List<string>();
using (MemoryStream ms = new MemoryStream())
{
StreamWriter sw = new StreamWriter(ms);
sw.Write(text);
sw.Flush();


ms.Position = 0;


string line;


using (StreamReader sr = new StreamReader(ms))
{
while ((line = sr.ReadLine()) != null)
{
lines.Add(line);
}
}
sw.Close();
}






return lines.ToArray();
}

我有这个 另一个答案,但这一个,基于杰克的 回答要快得多可能是首选,因为它的工作异步,虽然稍慢。

public static class StringExtensionMethods
{
public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
{
using (var sr = new StringReader(str))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (removeEmptyLines && String.IsNullOrWhiteSpace(line))
{
continue;
}
yield return line;
}
}
}
}

用法:

input.GetLines()      // keeps empty lines


input.GetLines(true)  // removes empty lines

测试:

Action<Action> measure = (Action func) =>
{
var start = DateTime.Now;
for (int i = 0; i < 100000; i++)
{
func();
}
var duration = DateTime.Now - start;
Console.WriteLine(duration);
};


var input = "";
for (int i = 0; i < 100; i++)
{
input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}


measure(() =>
input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
);


measure(() =>
input.GetLines()
);


measure(() =>
input.GetLines().ToList()
);

产出:

00:00:03.9603894

00.00.00.0029996

00:00:04.8221971

这是棘手的处理 混合线结束正确。正如我们所知道的,行终止字符可以是“ Line Feed”(ASCII 10,\n\x0A\u000A) ,“ Carrigage Return”(ASCII 13,\r\x0D\u000D) ,或者它们的一些组合。回到 DOS,Windows 使用两个字符的序列 CR-LF \u000D\u000A,所以这个组合应该只发出一行。Unix 使用单个 \u000A,而非常老的 Mac 使用单个 \u000D字符。在单个文本文件中处理这些字符的任意混合的标准方法如下:

  • 每个 CR 或 LF 字符都应该跳到下一行 除了..
  • ... 如果一个 CR 紧接着是 LF (\u000D\u000A) ,那么这两个 一起只跳过一行。
  • String.Empty是唯一不返回行的输入(任何字符都至少包含一行)
  • 最后一行必须返回,即使它既没有 CR 也没有 LF。

前面的规则描述了 StringReader ReadLine和相关函数的行为,下面所示的函数产生相同的结果。它是一个有效的 C # 断线功能,忠实地执行这些准则,以正确处理任何任意序列或 CR/LF 的组合。枚举行不包含任何 CR/LF 字符。保留空行并将其返回为 String.Empty

/// <summary>
/// Enumerates the text lines from the string.
///   ⁃ Mixed CR-LF scenarios are handled correctly
///   ⁃ String.Empty is returned for each empty line
///   ⁃ No returned string ever contains CR or LF
/// </summary>
public static IEnumerable<String> Lines(this String s)
{
int j = 0, c, i;
char ch;
if ((c = s.Length) > 0)
do
{
for (i = j; (ch = s[j]) != '\r' && ch != '\n' && ++j < c;)
;


yield return s.Substring(i, j - i);
}
while (++j < c && (ch != '\r' || s[j] != '\n' || ++j < c));
}

注意: 如果不介意在每个调用上创建一个 StringReader实例的开销,可以使用以下 C # 7代码。如前所述,虽然上面的示例可能稍微有效一些,但是这两个函数产生的结果完全相同。

public static IEnumerable<String> Lines(this String s)
{
using (var tr = new StringReader(s))
while (tr.ReadLine() is String L)
yield return L;
}

不进行任何分配就将字符串拆分成行。

public static LineEnumerator GetLines(this string text) {
return new LineEnumerator( text.AsSpan() );
}


internal ref struct LineEnumerator {


private ReadOnlySpan<char> Text { get; set; }
public ReadOnlySpan<char> Current { get; private set; }


public LineEnumerator(ReadOnlySpan<char> text) {
Text = text;
Current = default;
}


public LineEnumerator GetEnumerator() {
return this;
}


public bool MoveNext() {
if (Text.IsEmpty) return false;


var index = Text.IndexOf( '\n' ); // \r\n or \n
if (index != -1) {
Current = Text.Slice( 0, index + 1 );
Text = Text.Slice( index + 1 );
return true;
} else {
Current = Text;
Text = ReadOnlySpan<char>.Empty;
return true;
}
}




}

但是我一直在使用一个简单的扩展方法集合来解决这个问题,它利用了 TextReader.ReadLine():

public static class StringReadLinesExtension
{
public static IEnumerable<string> GetLines(this string text) => GetLines(new StringReader(text));
public static IEnumerable<string> GetLines(this Stream stm) => GetLines(new StreamReader(stm));
public static IEnumerable<string> GetLines(this TextReader reader) {
string line;
while ((line = reader.ReadLine()) != null)
yield return line;
reader.Dispose();
yield break;
}
}

使用这些代码非常简单:

// If you have the text as a string...
var text = "Line 1\r\nLine 2\r\nLine 3";
foreach (var line in text.GetLines())
Console.WriteLine(line);
// You can also use streams like
var fileStm = File.OpenRead("c:\tests\file.txt");
foreach(var line in fileStm.GetLines())
Console.WriteLine(line);

希望这能帮到别人。