LoginSignup
0
1

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

Posted at

はじめに

AtCoder に登録したら次にやること ~ これだけ解けば十分闘える!過去問精選 10 問 ~を見て取り掛かろうとしたところ、競技プログラミングってConsole.ReadLineConsole.WriteLineが多用されていてローカルでデバッグしづらいんですよね・・・。

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

結論

結論から言うと Console.SetIn / Console.SetOut を使えばよいです。
TextReaderTextWriterを用意して渡してあげればOKです!

手順

  1. 競技プログラミングに渡すためのC#プロジェクトを適当に作ります。
    • 一旦デフォルトのままでよいです。
    • 対象の競技プログラミングサイトの設定に合わせてTargetFramework等を設定してください。
    • 2のプロジェクトから呼び出すため、namespaceやstatic classはちゃんと定義しておきましょう。
  2. 上記プロジェクトを呼び出すBootstrap的なプロジェクトを作り、1のプロジェクトを参照します。
    • 普通にProjectReferenceするだけです。
  3. 2のプロジェクトに対し、TextReaderTextWriterを用意し、Console.SetIn / Console.SetOutに設定します。
    • 詳しくは後述
  4. 2のプロジェクトから1のプロジェクトのMainを呼び出します。
  5. 2のプロジェクトを問題に合わせた入出力処理をするよう整備し、1のプロジェクトで提出するコードを書きます。
  6. 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 Encodingvoid 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();
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1