はじめに
AtCoder に登録したら次にやること ~ これだけ解けば十分闘える!過去問精選 10 問 ~を見て取り掛かろうとしたところ、競技プログラミングってConsole.ReadLine
やConsole.WriteLine
が多用されていてローカルでデバッグしづらいんですよね・・・。
ということでローカル環境の整備の仕方を調べたのでまとめてみました!
結論
結論から言うと Console.SetIn / Console.SetOut を使えばよいです。
TextReader
やTextWriter
を用意して渡してあげればOKです!
手順
- 競技プログラミングに渡すためのC#プロジェクトを適当に作ります。
- 一旦デフォルトのままでよいです。
- 対象の競技プログラミングサイトの設定に合わせてTargetFramework等を設定してください。
- 2のプロジェクトから呼び出すため、namespaceやstatic classはちゃんと定義しておきましょう。
- 上記プロジェクトを呼び出すBootstrap的なプロジェクトを作り、1のプロジェクトを参照します。
- 普通にProjectReferenceするだけです。
- 2のプロジェクトに対し、
TextReader
やTextWriter
を用意し、Console.SetIn / Console.SetOutに設定します。- 詳しくは後述
- 2のプロジェクトから1のプロジェクトの
Main
を呼び出します。 - 2のプロジェクトを問題に合わせた入出力処理をするよう整備し、1のプロジェクトで提出するコードを書きます。
- 2のプロジェクトを実行するとデバッグが可能です。
TextReader
の用意
Console.ReadLine
に渡す標準入力を差し替えるためのものです。
2のプロジェクトから見るとWriter
であり、1のプロジェクトから見るとReader
です。
TextReaderにはRead
系のメソッドが複数用意されていますがoverrideするのはint Read()
だけでよいです。
(他のメソッドからはこのRead
が1文字ずつ呼ばれる形になっている)
あとは2のプロジェクトで必要な書き込みメソッドを用意しましょう。
ほとんどの場合WriteLine
だけで事足りるはずです。
末尾に改行を入れるのを忘れないようにしましょう。
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に設定して終了です。
using var writer = new Writer();
Console.SetIn(writer);
TextWriter
の用意
殆どのケースでは入力するだけでなくても事足りるのですが、1のプロジェクトからの出力に対して2のプロジェクトから入力を行うケースでは必要です。
これも必須であるEncoding Encoding
とvoid Write(char)
さえ実装すれば事足ります。
Read系メソッドもReadLine
さえあれば問題ないでしょう。
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に渡して完了です。
using var reader = new Reader();
Console.SetOut(reader);
標準出力の用意
Console.SetOutによって標準出力を差し替えた場合、単にConsole.WriteLine
するだけではログに出力されません。
ILogger
などを実装するか、Console.OpenStandardOutputへ出力できるようにしましょう。
using var output = new StreamWriter(Console.OpenStandardOutput());
1のプロジェクトの起動方法
基本的にはTask.Run
などで呼び出すだけでよいです。
var mainTask = Task.Run(Project1.Program.Main);
しかし、これだとエラーの検知ができないためContinueWithなどで早めにエラー検知できるようにしましょう。
var mainTask = Task.Run(SandboxConsole.Program.Main)
.ContinueWith(t =>
{
if (t.IsFaulted)
{
output.WriteLine(t.Exception);
}
return t;
});
最後にawait
してあげれば終了です。
Task.Run
を直接await
してもいいのですが、出力内容の検証などをすることを考えると別途await
してあげるのが良いかと思います。
await mainTask;
使い方
問題の通りの入力をwriter.WriteLine
に渡し、reader.ReadLine
によって出力を検証します。
AtCoderのB - Interactive Sorting
用のコードを例に挙げます。
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();
}
}