この記事はHUITアドベントカレンダー19日目の記事です。はじめに断っておきますがネタ記事です。
はじめに
去る2021年11月、C#10および.NET6が公開されました。
C#10の新機能にはテンプレート的コードを書かなくて済むようにする機能が含まれており、単純なプログラムは単純に書けるようになりました。
今回はこの新機能を使って 短くて単純な プログラムを書いてみましょう。
C#10以前:おまじない問題
C#では、すべてのステートメントはメソッドに属し、メソッドはクラスに属し、クラスは名前空間に属しうる、という階層構造を取っています。
そのため最低限、何らかの処理すなわちステートメントを書くには、メソッドとクラスが必要です。
加えて現実的には名前空間を参照する using
ステートメントも必要でしょう。
すると、「最初のプログラム」になにかわけのわからないもの(初心者目線)が並ぶ結果になってしまします。
using System;
class Program
{
static void Main()
{
Console.WriteLine("hello world!");
}
}
こういうのを入門書では「おまじない」などと呼ぶことがありますが、おまじないが多いのは初心者に優しくないですね。
C#10の新機能:global using
global using
は、ファイル単位でusing
を書かずとも、プロジェクト単位でusing
できる機能です。
さらに、今後新しいプロジェクトを作成すると自動的にglobal using
されます。例えば、コンソールアプリケーションの場合
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
相当のglobal using
が自動挿入されます。よって、もうおまじないのようにusing
を書かなくても最初のプログラムが書けるのです。
C#10の新機能②:最上位レベルのステートメント
いきなりステートメントが書ける機能です。今まで書いていたクラスとMain
関数の定義を書かなくてすみます。
実際のところは糖衣構文になっていてクラスとMain
関数はコンパイラが生成します。
C#10でHello world
つまるところ、以下のようなコードが正しいC#プログラムとして認められ、コンパイルが通ります。
Console.WriteLine("hello world!");
たった1行になりました。もうおまじないに頼る必要はありません。
1行でコードを書く
条件
せっかく1行でコードが書けるようになったので色々なプログラムを1行にしてみましょう。
C#は(適切な位置にある)空白文字や改行文字がコンパイル結果に影響を与えない言語ですので、ほぼどんなコードも1行に折り畳めますが、そうではなく、ここでは1行のプログラムとは1つのステートメントからなるプログラムとします。
できないこと
以下の機能はそれで1つのステートメントになるので今回使えません。
- if文
- for/foreach/while文
- 変数宣言
結構厳しい条件ですが、それぞれ代わりになる機能(3項演算子、Linq、is演算子)があるのでなんとかなります。
1行でFizzBuzzする
手始めにFizzBuzzしてみましょう。
Linqを使う
Enumerable
クラスには集合を生成できるメソッドがあり、Enumerable.Range
では規定の個数の整数列を生成できます。これをSelect
すると実質for
相当のコードが書けます。
Linqが追加されたのはC#3の頃。随分と便利になりました(?)。
コード
Console.WriteLine((new[] { "", "Fizz", "Buzz", "FizzBuzz" }).ElementAt(Enumerable.Repeat(Enumerable.Range(0, 1).Select(_ => int.Parse(Console.ReadLine())).ToArray(), 2).SelectMany(x => x).Select((x, i) => i == 0 ? (x % 3 == 0 ? 1 : 0) : (x % 5 == 0 ? 2 : 0)).Sum()));
解説
Enumerable.Range(0, 1).Select(_ => int.Parse(Console.ReadLine())).ToArray()
まず集合{0}を生成し、これの各要素を標準入力を整数変換したものに置き換える、つまり{入力値}にし、遅延評価ではなく即時評価するために配列にします。
Enumerable.Repeat({上記の式}, 2)
さきほどの{入力値}を2回繰り返し、{{入力値},{入力値}}にします。
.SelectMany(x => x).Select((x, i) => i == 0 ? (x % 3 == 0 ? 1 : 0) : (x % 5 == 0 ? 2 : 0)).Sum()
{{入力値},{入力値}}を{入力値,入力値}に展開し、さらに
0番目の要素を3で割り切れるなら1に
1番目の要素を5で割り切れるなら2に
変換します。割り切れないならば0にします。最後に全要素を合計します。つまりフラグです。
(new[] { "", "Fizz", "Buzz", "FizzBuzz" }).ElementAt({上記の式})
あとはフラグから表示したい文字列を取得します。
is
で変数宣言する
C#7以降、型変換可能かどうかを調べるis
演算子で変数宣言ができます。
コード
Console.WriteLine(int.Parse(Console.ReadLine()) is int n ? n % 15 == 0 ? "FizzBuzz" : n % 5 == 0 ? "Buzz" : n % 3 == 0 ? "Fizz" : "" : throw new InvalidOperationException());
解説
意味がないis
型チェックをします。あとは3項演算子を大量に書きます。このように入れ子になった3項演算子は大変読みにくいので書かないようにしましょう。
out
で変数宣言する
C#7以降、out
参照渡し引数で式中の変数宣言が認められました。やることはさきほどと同じです。
Console.WriteLine(int.TryParse(Console.ReadLine(), out var n) ? n % 15 == 0 ? "FizzBuzz" : n % 5 == 0 ? "Buzz" : n % 3 == 0 ? "Fizz" : "" : throw new InvalidOperationException());
1行で問題を解く
せっかくなのでAtCoderの簡単な過去問を1行で解いてみましょう。残念ながら言語バージョン上そのままジャッジは通せないですが...
ABC231 A
Console.WriteLine(decimal.Parse(Console.ReadLine()) / 100);
全然読めますね。今回は必要ありませんがdecimal
使っておけば常に誤差問題とおさらばできるので楽です。
ABC231 B
Console.WriteLine(int.Parse(Console.ReadLine()) is int n ? Enumerable.Range(0, n).Select(_ => Console.ReadLine()).ToArray() is string[] str ? str.Select(x => (x, str.Where(y => x == y).Count())).OrderByDescending(x => x.Item2).ElementAt(0).Item1 : throw new NotImplementedException() : throw new NotImplementedException());
is
演算子戦略で入力行数を読み取り、Linqで残りの入力を読みます。
途中で ValueTuple
構造体を使っています。制約がゆるいので計算量はかなり甘えています。
ABC231 C
Console.WriteLine(string.Join(Environment.NewLine, int.Parse(Console.ReadLine().Split(' ').ElementAt(1)) is int q ? Console.ReadLine().Split(' ').Select(x => int.Parse(x)).OrderBy(x => x).ToArray() is int[] input ? Enumerable.Range(0, q).Select(x => Array.BinarySearch(input, int.Parse(Console.ReadLine())) is int index ? input.Length - (index < 0 ? ~index : index) : throw null) : throw null : throw null));
入力をソートして二分探索します。C#標準の二分探索メソッドは要素が見つからなかったときに、探している要素より大きくて最小の要素のインデックスの ビット否定 を返すことに留意してください。
後半throw null
が3つありますがこれはよくないコードですのでマネしないで下さい。
ABC230 A
Console.WriteLine($"AGC{(int.Parse(Console.ReadLine()) is int n ? (n > 41 ? n + 1 : n).ToString("d3") : throw null)}");
文字列補間内で3項演算子を使うときは括弧でくくる必要があります。
ToString
するときに文字列を引数で渡すと書式指定できます。16進数にする、桁数を指定するなど色々用意されていて便利ですね。
ABC230 B
Console.WriteLine((new string(Enumerable.Repeat("oxx", 100).SelectMany(x => x).ToArray())).Contains(Console.ReadLine()) ? "Yes" : "No");
制約がゆるいので愚直に行きます。今までで一番マシなコードかもしれない。
最後に
なにか制約を加えると灰色の問題でも楽しめます。1ステートメント縛りとILで書く縛りがおすすめです。頭の体操になりますね。