注意
この記事は昔はてなブログに書いた記事C#でC++のcinっぽいのに詳細説明を加筆し、細部を修正したものです。
TL; DR
- C++ のcinと似た使い勝手のクラスをC#で再現してみました。
- ただし入力は最初にまとめて読み込みます。
- 最終的に以下のように標準入力が読めるようになります。
入力
5
John
100 foo 200
コード
static void Main(string[] args)
{
// 標準入力から読み込み
int N = cin;
string name = cin;
(int A, string str, int B) = cin;
// 標準出力へ書き出し
Console.WriteLine(N);
Console.WriteLine(name);
Console.WriteLine($"{A} {str} {B}");
}
出力(入力と全く同様)
5
John
100 foo 200
但し書き
- 実用目的というよりかは「C#でもこういうことできるよ!」という思考実験・例示の目的で書いています。
- 一般的なコードとして行儀の悪いことをしています。(型変換演算子が副作用を持つ)
- cin周りの構文を完全に真似はしていません。
- 仕様で保証されていない動作に依存しているかもしれません。(分解代入のあたりの評価順が保証されているのか調べていない)
C++のcinについて
C++ではcinを用いて空白・改行区切りのテキストデータを簡単に読み込むことができます。
先ほども例示した
5
John
100 foo 200
というデータは、
int N, A, B;
string name, str;
// 標準入力から読み込み
cin >> N;
cin >> name;
cin >> A >> str >> B;
// 標準出力へ書き出し
cout << N << endl;
cout << name << endl;
cout << A << " " << str << " " << B << endl;
というC++コードで読み込めます。
標準出力への書き出しのために用いているcoutについての説明は割愛し、cin周りのコードについて説明します。
cin >> N;では標準入力から数値を読み込んでおり、cin >> name; では標準入力から文字列を読み込んでいます。また、 cin >> A >> str >> B のように、コード1行で複数の値を読み込むこともできます。読み取られるデータは空白・改行に従って適切にトークン化されてから解釈されます。ここから考えるに、上のcinの用法が使い勝手が良い理由には以下のエッセンスがありそうです。
- 入力を適切にトークン化してくれている。
- メソッド呼び出しを書かずに値を取り出せる。
- 変数の型に応じて数値としても文字列としても値を取り出せる。
- 1行で複数の変数に値を取り出せる。
C#で実装
上記エッセンスを実現したCinクラスを作成します。
tokenに分割して読み込む機能
標準入力はクラスの初期化時にまとめて読み込むことにし、tokenに区切ってtokensプライベートフィールドに保存しておくこととします。tokensフィールドは先頭から一つづつ値を取り出していく用途で用いられるので、 Queueというデータ構造で持ちます。
class Cin {
private Queue<string> tokens;
public Cin() {
string line;
tokens = new Queue<string> ();
while ((line = Console.ReadLine ()) != null) {
foreach (var token in line.Split (' ')) {
tokens.Enqueue (token);
}
}
}
}
整数型として変数に読み込む
int N = cin; のような書き味を目指します。
Cinクラスのインスタンスcinからint型の値を取り出すために暗黙の型変換を活用(悪用?)します。
public static implicit operator int(Cin cin) => int.Parse(cin.tokens.Dequeue());
queueからtokenを一つ取り出し、intにパースした結果を返す型変換子を定義しています。この型変換は副作用を持っています。(Dequeueメソッドはqueueの先頭の値を返して、queueからその値を削除します。)行儀が悪いですが、行儀にはcinっぽい書き味の尊い犠牲になってもらい、今回は気にしないこととします。
ここまでで以下のようなコードが動くようになっています。
int a = cin;
int b = cin;
int c = cin;
トークンごとに整数型の値を読みだせるようになりました。書き手は変換メソッドを明示的に呼ぶ必要がありません。
文字列型として変数に読み込む
string str = cin; のような書き味を目指します。
実装は整数型の読み込みとほぼ変わりません。整数へのパースが不要な分、よりシンプルになっています。
public static implicit operator string(Cin cin) => cin.tokens.Dequeue();
ここまでで以下のようなコードが動きます。
int a = cin;
string str1 = cin;
int b = cin;
整数型か文字列型かを区別せずに、同一の = cin で値が取り出せています。
1行で複数の変数に値を取り出す
(int A, string str, int B) = cin;のような書き味を目指します。
csharp にはユーザー定義クラスによる分割代入を可能にするための機能があります。今回はこれを使用します。
(int a, string b) = cin を可能にするためには以下のdeconstructメソッドを定義すればよいです。
public void Deconstruct(out Cin o1, out Cin o2) => (o1, o2) = (this, this);
少し複雑なので順を追って説明します。まずo1, o2にはthisが代入されているので、(int a, string b) = cinは(int a, string b)=(cin, cin)と同じになります。この左右のcinがそれぞれ先ほど定義した暗黙の型変換子によりint, stringに変換されます。
これと同様のdeconstructメソッドを実用しそうなタプルの要素数まで定義します。以下では要素数8のタプルまで定義しました。
public void Deconstruct(out Cin o1, out Cin o2) =>
(o1, o2) = (this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3) =>
(o1, o2, o3) = (this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4) =>
(o1, o2, o3, o4) = (this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5) =>
(o1, o2, o3, o4, o5) = (this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6) =>
(o1, o2, o3, o4, o5, o6) = (this, this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6, out Cin o7) =>
(o1, o2, o3, o4, o5, o6, o7) = (this, this, this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6, out Cin o7, out Cin o8) =>
(o1, o2, o3, o4, o5, o6, o7, o8) = (this, this, this, this, this, this, this, this);
ここまでで以下のコードが動きます。
(int a, string b, int c) = cin;
1行で複数の変数に値を取り出せるようになりました。
まとめ
C#でもC++のcinと似た使い勝手のクラスを作成することができました。
今回のコードはここです。