Bending .NET - ReadLine Patterns

or how to make it rain so puny humans get soaked.

In this post, part of the Bending .NET series, I take a quick look at code patterns for reading lines from TextReader using ReadLine, which has existed from the early beginning of .NET (Framework 1.1 as far as I can tell) based on a pattern of returning null when there are no more lines to read. Let’s make it rain 🌧

TLDR: is pattern matching syntax can be used to simplify ReadLine code via implicit null check to:

1
2
3
4
5
using var reader = new StringReader(text);
while (reader.ReadLine() is string line)
{
    Console.WriteLine(line);
}

Digital rain animation ala The Matrix Source: wikimedia

ReadLine code examples shown below with both synchronous and asynchronous versions and a version using var that results in infinite loop. Be careful of that 😅 See code in sharplab.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#nullable enable
using System;
using System.IO;

Action<string?> log = Console.WriteLine;
var text = "1\n2\n3\n";

log("SYNC - classic");
{
  using var reader = new StringReader(text);
  string? line;
  while ((line = reader.ReadLine()) != null) { log(line); }
}
log("SYNC - is string");
{
  using var reader = new StringReader(text);
  while (reader.ReadLine() is string line) { log(line); }
}
log("SYNC - is {}");
{
  using var reader = new StringReader(text);
  while (reader.ReadLine() is { } line) { log(line); }
}

log("ASYNC - classic");
{
  using var reader = new StringReader(text);
  string? line;
  while ((line = await reader.ReadLineAsync()) != null) { log(line); }
}
log("ASYNC - is string");
{
  using var reader = new StringReader(text);
  while (await reader.ReadLineAsync() is string line) { log(line); }
}
log("ASYNC - is {}");
{
  using var reader = new StringReader(text);
  while (await reader.ReadLineAsync() is { } line) { log(line); }
}
log("DONE");

// Infinite loop version since `is var`
// does not perform implicit null check
var infinite = false;
if (infinite)
{
  using var reader = new StringReader(text);
  while (reader.ReadLine() is var line) { log(line); }
}

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SYNC - classic
1
2
3
SYNC - is string
1
2
3
SYNC - is {}
1
2
3
ASYNC - classic
1
2
3
ASYNC - is string
1
2
3
ASYNC - is {}
1
2
3
DONE

If you remove the async/await code and look at the lowered C# code e.g. via sharplab.io then the patterns result in similar code with slight and probably neglible differences.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Action<string> action = <>O.<0>__WriteLine ?? 
  (<>O.<0>__WriteLine = new Action<string>(Console.WriteLine));
string s = "1\n2\n3\n";
action("SYNC - classic");
StringReader stringReader = new StringReader(s);
try
{
    string obj;
    while ((obj = stringReader.ReadLine()) != null)
    {
        action(obj);
    }
}
finally
{
    if (stringReader != null)
    {
        ((IDisposable)stringReader).Dispose();
    }
}
action("SYNC - is string");
StringReader stringReader2 = new StringReader(s);
try
{
    while (true)
    {
        string text = stringReader2.ReadLine();
        if (text != null)
        {
            action(text);
            continue;
        }
        break;
    }
}
finally
{
    if (stringReader2 != null)
    {
        ((IDisposable)stringReader2).Dispose();
    }
}
action("SYNC - is {}");
StringReader stringReader3 = new StringReader(s);
try
{
    while (true)
    {
        string text2 = stringReader3.ReadLine();
        if (text2 != null)
        {
            action(text2);
            continue;
        }
        break;
    }
}
finally
{
    if (stringReader3 != null)
    {
        ((IDisposable)stringReader3).Dispose();
    }
}
action("DONE");
if (0 == 0)
{
    return;
}
StringReader stringReader4 = new StringReader(s);
try
{
    while (true)
    {
        string obj2 = stringReader4.ReadLine();
        action(obj2);
    }
}
finally
{
    if (stringReader4 != null)
    {
        ((IDisposable)stringReader4).Dispose();
    }
}

That’s all!

2024.11.29