競技プログラミングする時のローカル環境整備

# はじめに

[AtCoder に登録したら次にやること ～ これだけ解けば十分闘える！過去問精選 10 問 ～](https://qiita.com/drken/items/fd4e5e3630d0f5859067)を見て取り掛かろうとしたところ、競技プログラミングって`Console.ReadLine`や`Console.WriteLine`が多用されていてローカルでデバッグしづらいんですよね・・・。

ということでローカル環境の整備の仕方を調べたのでまとめてみました！

# 結論

結論から言うと [Console.SetIn](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setin?view=net-7.0) / [Console.SetOut](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setout?view=net-7.0) を使えばよいです。
`TextReader`や`TextWriter`を用意して渡してあげればOKです！

# 手順

1. 競技プログラミングに渡すためのC#プロジェクトを適当に作ります。
    - 一旦デフォルトのままでよいです。
    - 対象の競技プログラミングサイトの設定に合わせてTargetFramework等を設定してください。
    - 2のプロジェクトから呼び出すため、namespaceやstatic classはちゃんと定義しておきましょう。
2. 上記プロジェクトを呼び出すBootstrap的なプロジェクトを作り、1のプロジェクトを参照します。
    - 普通にProjectReferenceするだけです。
3. 2のプロジェクトに対し、`TextReader`や`TextWriter`を用意し、[Console.SetIn](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setin?view=net-7.0) / [Console.SetOut](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setout?view=net-7.0)に設定します。
    - 詳しくは後述
4. 2のプロジェクトから1のプロジェクトの`Main`を呼び出します。
5. 2のプロジェクトを問題に合わせた入出力処理をするよう整備し、1のプロジェクトで提出するコードを書きます。
6. 2のプロジェクトを実行するとデバッグが可能です。

## `TextReader`の用意

`Console.ReadLine`に渡す標準入力を差し替えるためのものです。
2のプロジェクトから見ると`Writer`であり、1のプロジェクトから見ると`Reader`です。

[TextReader](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.textreader?view=net-7.0)には`Read`系のメソッドが複数用意されていますがoverrideするのは`int Read()`だけでよいです。
(他のメソッドからはこの`Read`が1文字ずつ呼ばれる形になっている)

あとは2のプロジェクトで必要な書き込みメソッドを用意しましょう。
ほとんどの場合`WriteLine`だけで事足りるはずです。
末尾に改行を入れるのを忘れないようにしましょう。

```cs
class Writer : TextReader
{
    private readonly Channel<string> _line = Channel.CreateUnbounded<string>();

    private ReadOnlyMemory<char> _current;

    /// <inheritdoc cref="TextReader.Read()"/>
    public override int Read()
    {
        // 読み込み中の行がなければ書き込まれるまで待つ
        if (_current.IsEmpty)
        {
            var r = _line.Reader.ReadAsync().AsTask();
            r.Wait();
            _current = r.Result.AsMemory();
        }
        var c = _current.Span[0];
        _current = _current[1..];
        return c;
    }

    /// <summary>
    /// <paramref name="s"/>の内容を標準入力として書き込みます。
    /// </summary>
    public ValueTask WriteLine(string s) => _line.Writer.WriteAsync(s + "\n");
}
```

あとは[Console.SetIn](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setin?view=net-7.0)に設定して終了です。

```cs
using var writer = new Writer();
Console.SetIn(writer);
```

## `TextWriter`の用意

殆どのケースでは入力するだけでなくても事足りるのですが、1のプロジェクトからの出力に対して2のプロジェクトから入力を行うケースでは必要です。

これも必須である`Encoding Encoding`と`void Write(char)`さえ実装すれば事足ります。

Read系メソッドも`ReadLine`さえあれば問題ないでしょう。

```cs
class Reader : TextWriter
{
    private readonly Channel<char> _line = Channel.CreateUnbounded<char>();

    public override Encoding Encoding => Encoding.Unicode;

    /// <inheritdoc cref="TextWriter.Write(char)"/>
    public override void Write(char value)
    {
        _ = _line.Writer.WriteAsync(value);
    }

    public async Task<string> ReadLine()
    {
        var sb = new StringBuilder();
        var reader = _line.Reader;
        while (true)
        {
            var c = await reader.ReadAsync();
            sb.Append(c);
            if (c == '\n') break;
        }
        return sb.ToString();
    }
}
```

これもサクっと[Console.SetOut](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setout?view=net-7.0)に渡して完了です。

```cs
using var reader = new Reader();
Console.SetOut(reader);
```

### 標準出力の用意

[Console.SetOut](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.setout?view=net-7.0)によって標準出力を差し替えた場合、単に`Console.WriteLine`するだけではログに出力されません。
`ILogger`などを実装するか、[Console.OpenStandardOutput](https://learn.microsoft.com/ja-jp/dotnet/api/system.console.openstandardoutput?view=net-7.0)へ出力できるようにしましょう。

```cs
using var output = new StreamWriter(Console.OpenStandardOutput());
```

## 1のプロジェクトの起動方法

基本的には`Task.Run`などで呼び出すだけでよいです。

```cs
var mainTask = Task.Run(Project1.Program.Main);
```

しかし、これだとエラーの検知ができないためContinueWithなどで早めにエラー検知できるようにしましょう。

```cs
var mainTask = Task.Run(SandboxConsole.Program.Main)
    .ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            output.WriteLine(t.Exception);
        }
        return t;
    });
```

最後に`await`してあげれば終了です。
`Task.Run`を直接`await`してもいいのですが、出力内容の検証などをすることを考えると別途`await`してあげるのが良いかと思います。

```cs
await mainTask;
```

## 使い方

問題の通りの入力を`writer.WriteLine`に渡し、`reader.ReadLine`によって出力を検証します。
AtCoderの`B - Interactive Sorting`用のコードを例に挙げます。

```cs
using System.Text;
using System.Threading.Channels;

#if false
const int N = 26;
const int Q = 1000;
#else
const int N = 5;
const int Q = 7;
#endif

var random = new Random();
var s = Enumerable.Range(0, N).Select(x => (char)('A' + x)).OrderBy(_ => random.Next()).ToArray();

// SetOutによって標準出力を差し替えるので
using var output = new StreamWriter(Console.OpenStandardOutput());
output.WriteLine(new string(s));

using var writer = new Writer();
Console.SetIn(writer);
using var reader = new Reader();
Console.SetOut(reader);

await writer.WriteLine($"{N} {Q}");

var mainTask = Task.Run(SandboxConsole.Program.Main)
    .ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            output.WriteLine(t.Exception);
        }
        return t;
    });

var q = 0;
while (true)
{
    var line = await reader.ReadLine();
    output.Write(line);

    if (line[0] == '!')
    {
        output.WriteLine(new string(line.AsSpan(2)).TrimEnd());
        output.WriteLine(new string(s));
        var b = line.AsSpan(2).TrimEnd().SequenceEqual(s);
        if (!b) throw new Exception("不一致");
        break;
    }

    q++;
    if (q > Q) throw new InvalidOperationException();

    var l = s.AsSpan().IndexOf(line[2]);
    var r = s.AsSpan().IndexOf(line[4]);
    var a = l < r ? "<" : ">";
    await writer.WriteLine(a);
    output.WriteLine(a);
}

await mainTask;

/// <summary>
/// 標準入力を書き込むための<see cref="TextReader"/>です
/// </summary>
class Writer : TextReader
{
    private readonly Channel<string> _line = Channel.CreateUnbounded<string>();

    private ReadOnlyMemory<char> _current;

    /// <inheritdoc cref="TextReader.Read()"/>
    public override int Read()
    {
        if (_current.IsEmpty)
        {
            var r = _line.Reader.ReadAsync().AsTask();
            r.Wait();
            _current = r.Result.AsMemory();
        }
        var c = _current.Span[0];
        _current = _current[1..];
        return c;
    }

    /// <summary>
    /// <paramref name="s"/>の内容を標準入力として書き込みます。
    /// </summary>
    public ValueTask WriteLine(string s) => _line.Writer.WriteAsync(s + "\n");
}

/// <summary>
/// 標準出力を受け取るための<see cref="TextWriter"/>です
/// </summary>
class Reader : TextWriter
{
    private readonly Channel<char> _line = Channel.CreateUnbounded<char>();

    public override Encoding Encoding => Encoding.Unicode;

    /// <inheritdoc cref="TextWriter.Write(char)"/>
    public override void Write(char value)
    {
        _ = _line.Writer.WriteAsync(value);
    }

    public async Task<string> ReadLine()
    {
        var sb = new StringBuilder();
        var reader = _line.Reader;
        while (true)
        {
            var c = await reader.ReadAsync();
            sb.Append(c);
            if (c == '\n') break;
        }
        return sb.ToString();
    }
}
```