ろうでんのブログ

ただのブログ

C# 9.0でワンライナーしてみよう

この記事はAkatsuki Advent Calendar 2020 8日目の記事です。 ジョーク記事です。 adventar.org

はじめに

この記事では、AtCoderの練習問題を題材にして、Top-level statementsを活用してワンライナーするためのテクニックについて触れます。 ワンライナーとは、「1行で何でもかんでも処理するソースコード」のことです(ざっくり)。 実用的なワンライナー*1もありますが、ここではパズル的な娯楽のためのワンライナーを扱います。 この記事は「C# 6.0~8.0で普通のコーディングをしたことはあるが、過剰なワンライナーをしたことはない」といった人向けの内容になっていると思います。この記事を読み終えた後は、以下のような状態になっているかもしれません。

  • C# 9.0新構文のTop-level statementsについて知っている
  • LINQ等を悪用したメソッドチェーンによって何でもかんでも1行で書きたくなる

ワンライナー」の定義は色々あると思いますが、この記事では

①1つの文から成り
②ブロックを持たず
セミコロンを1つしか含まない

もののみをワンライナーとして認めることにします。 なお、この記事ではショートコーディングは目指しません。 そして、実行速度も度外視しています。 あしからず。 また、コード例すらも本当に1行で書いてしまうと非常に読みづらくなるため、適宜改行したものを例示します(ワンライナーとは……?)。

本記事で示すコードは、Visual Studio Community 2019 (16.8.2) を用いた.NET 5.0向けコンソールアプリケーションのプロジェクトでの実行を想定しています。 AtCoderのジャッジシステムは用いていません(ジャッジシステムがC# 9.0未対応のため)。


流れとしては、最初にTop-level statementsについて軽く触れた後、実演的な形で非ワンライナーのコードからワンライナーのコードに変形させていくテクニックについて書き連ねます。

目次

C# 9.0とは?

C# 9.0は、プログラミング言語C#の最新のメジャーバージョンです。2020年11月10日にリリースされた.NET 5.0 *2でサポートされるようになりました。 C# 9.0では、新構文が色々追加されました。 今回用いるTop-level statementsもその内の1つです。 それぞれの機能の詳細については岩永さんの記事が詳しいです。

Top-level statementとは?

C# 9.0で追加された新構文(糖衣構文のような感じ)です。

C# 8.0まででは、C言語で言うところのmain関数内に記述するような処理を、メインメソッド内に記述する必要がありました。 また、メインメソッドを持つクラスも定義する必要がありました。 これでは素の状態からコーディングするのは億劫ですね。

// C# 8.0までのHello World
using System;

// メインメソッドを入れるためのクラス
class Program
{
    // メインメソッド
    static void Main()
    {
        // 処理はメインメソッド内に書く必要がある。
        Console.WriteLine("Hello World!");
    }
}

// 出力:
// Hello World!

ところで、Python等の言語では、トップレベルにいきなり書くことができますね? これをC#でできるようにするものがTop-level statementsです。 ちょっとしたコードを書く際に嬉しい構文ですね。

// C# 9.0からのHello World
using System;

// トップレベル(クラス外・メインメソッド外)に処理を書くことができる。
Console.WriteLine("Hello World!");

// 出力:
// Hello World!

もちろん、このように複数行書くこともできます。

// C# 9.0
using System;

Console.WriteLine("Hello World 1");
Console.WriteLine("Hello World 2");

// 出力:
// Hello World 1
// Hello World 2

本題:AtCoderの練習問題をワンライナーで解いてみる

さて本題です。 ここからは、AtCoder練習用コンテストA問題(要ログイン)を解くためのプログラムをワンライナーしていきます。

問題は次のようなものです。

整数a, b, cと、文字列sが与えられます。a+b+cの計算結果と、文字列sを並べて表示しなさい。

制約:

  • 1≤a, b, c ≤ 1,000
  • 1≤|s|≤100

入力:

入力は以下の形式で与えられる。

a
b c
s

出力:
a+b+cとsを空白区切りで1行に出力せよ。

まずはあまり糖衣構文を用いない古典的かつ最も冗長的なスタイルでコーディングしてから、ちょっとずつワンライナーに仕上げていきます。

古典的・冗長的なスタイル

using System;

class Program
{
    static void Main()
    {
        string line1 = Console.ReadLine(); // 1行目の入力(aが文字列として入る)
        string line2 = Console.ReadLine(); // 2行目の入力(bとcがスペース区切りで文字列として入る)
        string s = Console.ReadLine(); // 3行目の入力(sが入る)

        int a = int.Parse(line1); // 文字列→整数 の変換

        string[] bc = line2.Split(' '); // スペース区切りで2つの文字列に分解
        int b = int.Parse(bc[0]); // 文字列→整数 の変換
        int c = int.Parse(bc[1]); // 文字列→整数 の変換

        int sum = a + b + c; // 3つの整数の和
        string sumStr = sum.ToString(); // 整数→文字列 の変換
        string result = sumStr + " " + s; // a+b+cの文字列とsをスペース区切りで結合
        Console.WriteLine(result); // 結果を1行に出力
    }
}

ローカル変数への導入を省略 その1

さっきのコードはローカル変数への導入を過剰に行っていました。 これを省略して、文を減らしていきます。 ついでにvarを用いた型推論も活用しています。

using System;

class Program
{
    static void Main()
    {
        var a = int.Parse(Console.ReadLine()); // 1行目の入力を整数に変換
        var bc = Console.ReadLine().Split(' '); // 2行目の入力をスペース区切りで分解
        var b = int.Parse(bc[0]); // 文字列→整数 の変換
        var c = int.Parse(bc[1]); // 文字列→整数 の変換
        var s = Console.ReadLine(); // 3行目の入力
        Console.WriteLine((a + b + c).ToString() + " " + s); // 結果を1行に出力
    }
}

ローカル変数への導入を省略 その2

もっと詰め込んでいきましょう。 このあたりから非推奨な領域に入っていきます。 ついでに文字列補間 (string interpolation) を活用してイマドキの書き方に直します。 文字列補間自体は推奨です*3

using System;

class Program
{
    static void Main()
    {
        var a = int.Parse(Console.ReadLine()); // 1行目の入力を整数に変換
        var bc = Console.ReadLine().Split(' '); // 2行目の入力をスペース区切りで分解

        // 文字列補間式内には式をガッツリ書けるので、ここで int.Parse() したり
        // Console.ReadLine() しちゃってもOK!
        Console.WriteLine(
            $"{a + int.Parse(bc[0]) + int.Parse(bc[1])} {Console.ReadLine()}"
        );
    }
}

LINQを活用

Console.WriteLine()内の文字列補間式内に色々詰め込めるようになってきましたね。

さて、ここで困ったことに、「2行目の入力を受け取りつつ、b, cの2つの整数にパース」を上手いこと文字列補間式に詰め込めない状態になってしまいました。 例えば、a + b + cの部分を

// ローカル変数に導入せず、その場で Console.ReadLine() しちゃえ!
a + int.Parse(Console.ReadLine().Split(' ')[0]) + int.Parse(Console.ReadLine().Split(' ')[1])

としてしまうと、結果が不正なものになってしまいます。 bにあたる箇所で2行目を、cにあたる箇所で3行目を読み込んでしまうからです*4

いかにローカル変数に導入せずに「2行目の入力を受け取りつつ、b, cの2つの整数にパース」できるか。 こういうときLINQが便利なんです。 Enumerable.Sum()を使って以下のように書き換えてみます。 Enumerable.Sum() *5によって、string.Split() *6して得られたstring[] *7に対して、str => int.Parse(str) *8で各要素をintに変換しつつ、その総和を計算しています。

using System;

// Sum() が System.Linq.Enumerable クラスにある拡張メソッド(後述)であるため、
// Sum() を拡張メソッドとして使うためにはこのusingディレクティブが必要。
using System.Linq;

class Program
{
    static void Main()
    {
        var a = int.Parse(Console.ReadLine());

        // LINQの Sum() を使って、パース後の整数b, cの和を求めている
        Console.WriteLine(
            $"{a + Console.ReadLine().Split(' ').Sum(str => int.Parse(str))} {Console.ReadLine()}"
        );
    }
}

ローカル変数への導入を省略 その3

b, cを1つの式に詰め込むことができたため、aも詰め込めるようになりました*9

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        Console.WriteLine(
            $"{int.Parse(Console.ReadLine()) + Console.ReadLine().Split(' ').Sum(str => int.Parse(str))} {Console.ReadLine()}"
        );
    }
}

(C# 6.0) 式形式のメンバメソッドに変形

晴れて、メインメソッドの中身が Console.WriteLine() のみの1文となりました。

ここで、この記事でのレギュレーションを振り返ってみましょう。

②ブロックを持たず

これは、もちろんメソッドブロックにも適用されるでしょう *10。 そのため、C# 6.0からある式形式のメンバメソッドという糖衣構文を活用して、メインメソッドのメソッドブロックを省略しましょう。 (後々この工程は意味をなさなくなりますが……)

using System;
using System.Linq;

class Program
{
    static void Main() =>
        Console.WriteLine(
            $"{int.Parse(Console.ReadLine()) + Console.ReadLine().Split(' ').Sum(str => int.Parse(str))} {Console.ReadLine()}"
        );
}

usingディレクティブの省略 その1

もう1回、この記事でのレギュレーションを振り返ってみましょう。

セミコロンを1つしか含まない

先程のコードではまだセミコロンが3つありますね? 3つの内、最初の2つはusingディレクティブ *11と呼ばれるプリプロセス命令の1つです。 usingディレクティブは、名前空間名を省略したいとき等に用います。 例えば、Console.WriteLine()でお馴染みのConsoleクラスは、System名前空間内にあります。 なので、名前空間名を省略せずに呼び出す場合には

System.Console.WriteLine()

となります。 いちいちSystem.と書くのは邪魔くさいですね。 そこでusing System;のようにusingディレクティブを書くことで、

Console.WriteLine()

だけでOKになります。

これを逆手に取って、練習問題のコードのusing System;を排除してみましょう。 これにより、セミコロンが2つに減ります。

using System.Linq;

class Program
{
    static void Main() =>
        System.Console.WriteLine(
            $"{int.Parse(System.Console.ReadLine()) + System.Console.ReadLine().Split(' ').Sum(str => int.Parse(str))} {System.Console.ReadLine()}"
        );
}

usingディレクティブの省略 その2

まだusing System.Linq;が残っていますね? このusingディレクティブは、Enumerable.Sum()を使うために書いたものでした*12。 使っている箇所は~~~.Split(' ').Sum(~~~)です。

Sum()Enumerableという静的クラスの静的メソッドですが、Split()の返り値string[]のメンバメソッドかのように書かれています。 このような書き方を拡張メソッドと呼びます。 拡張メソッドはあくまでも静的メソッドなので、普通の静的メソッドと同様に書くこともできます(ほとんど出番はありませんが)。 例えば、

new[] { 2, 3 }.Sum()

であれば

Enumerable.Sum(new[] { 2, 3 })

と書けます。 また、

new[] { "2", "3" }.Sum(str => int.Parse(str))

であれば

Enumerable.Sum(new[] { "2", "3" }, str => int.Parse(str))

と書けます。

これを逆手に取って、練習問題のコードの.Sum()を普通の静的メソッドの形式に変形しつつ、using System.Linq;を排除してみましょう これにより、セミコロンが1つに減ります。

class Program
{
    static void Main() =>
        System.Console.WriteLine(
            $"{int.Parse(System.Console.ReadLine()) + System.Linq.Enumerable.Sum(System.Console.ReadLine().Split(' '), str => int.Parse(str))} {System.Console.ReadLine()}"
        );
}

(C# 9.0) Top-level statements導入

さて、ここまではC# 8.0までの機能でワンライナーに取り組んでみました。 C# 8.0までは、ワンライナーするにもここが限界でした。

改めてこの記事でのレギュレーションを振り返ってみましょう。

①1つの文から成り
②ブロックを持たず
セミコロンを1つしか含まない

①は達成できてそうですね。 クラス定義はではありませんし、メインメソッドに連ねて式形式で書いた部分のみが文となっています。 なので1文で構成されていると言えますね。

一つ飛ばして、③についても一目瞭然、達成できてますね。

さて②についてはどうでしょう? クラス定義の波括弧ってブロックな気がしませんか……?(もしかすると違うかもしれません。ツッコミください) どちらにせよ、排除したくなったので排除しましょう。 ここでTop-level statementsの登場です。 晴れて、この記事でのレギュレーションを満たすことができましたね。 お疲れ様でした。

System.Console.WriteLine(
    $"{int.Parse(System.Console.ReadLine()) + System.Linq.Enumerable.Sum(System.Console.ReadLine().Split(' '), str => int.Parse(str))} {System.Console.ReadLine()}"
);

オマケ:さらなる高みへ

ワンライナーにすることはできました。 が、潔癖を働かせて、より高度なコード(抱腹絶倒ギャグ)にしてみましょう。

さて、System.Console.ReadLine()が行の数だけ3回出てきてまどろっこしいですよね? LINQをこねくり回して合体させましょう。 Enumerable.Range()Enumerable.Aggregate()がミソです。 ついにずっと使ってきた文字列補間式が崩れました。 それにより、適宜改行できるようになりました。 どうだ読みやすくなったろう。

System.Console.WriteLine(

    // Aggregate() は複雑な集計方法も叶えられるスゴいヤツ
    System.Linq.Enumerable.Aggregate(

        // 1行ごとの文字列と行番号を要素とした IEnumerable<(string line, int index)> を生成
        System.Linq.Enumerable.Select(

            // 3行分のループを生み出す
            // IEnumerable<int> を返す
            System.Linq.Enumerable.Range(0, 3),

            // 行の文字列と行番号のタプル (string line, int index) に変換するラムダ式 Func<int, (string line, int index)>
            // ここのindexは、後の条件演算子のところで使う
            index => (line: System.Console.ReadLine(), index)
        ),

        // Aggregate() で必要な「アキュムレータ (int sum, string str) の初期状態」を渡している
        // 整数と文字列を別々に扱った方が簡素になりそうなのでこうした
        (sum: default(int), str: default(string)),

        // 要素を1つずつ受け取り、アキュムレータ (int sum, string str) に集計していくラムダ式 Func<(int sum, string str), (string line, int index), (int sum, string str)>
        (accum, item) =>
            item.index == 2

                // 3行目の文字列をアキュムレータの str に格納
                ? (accum.sum, item.line)

                // 1・2行目の文字列を整数にして、行ごとに Sum() で和を取り、更にアキュムレータの sum を使って1行目での和と2行目での和を合算している
                : (accum.sum + System.Linq.Enumerable.Sum(item.line.Split(' '), str => int.Parse(str)), accum.str),

        // アキュムレータで別々に集計した整数と文字列を合体している
        accum => $"{accum.sum} {accum.str}"
    )
);

おわりに

イカカデシタカ? これからはあなたもレッツエンジョイワンライナーライフ*13

気が向いたらB問題の方もやってみます。 OrderBy()で綺麗に書ける気がします。

最後にオマケのコードを短くして締めくくります。

System.Console.WriteLine(System.Linq.Enumerable.Aggregate(System.Linq.Enumerable.Select(System.Linq.Enumerable.Range(0,3),i=>(l:System.Console.ReadLine(),i)),(n:0,s:""),(a,v)=>v.i>1?(a.n,v.l):(a.n+System.Linq.Enumerable.Sum(v.l.Split(' '),s=>int.Parse(s)),a.s),a=>$"{a.n} {a.s}"));

謝辞

岩永さんの記事にはいつもお世話になっております。 ありがとうございます。

*1:デバッガのウォッチ式を1行で書く裏技としては便利っちゃ便利です。あと、式木にするときに嬉しいかも?

*2:C#VB.NET、F#、C++/CLIなどが動く開発環境&実行環境のこと。今まではWindows専用の.NET Framework系列(4.8が最後)と、マルチプラットフォームだがWPFなどの機能を持たない.NET Core系列(3.1が最後)がありました。それらの正統後継者として.NET 5が生まれました。名前とバージョニングがややこしいですね。 https://docs.microsoft.com/ja-jp/dotnet/core/dotnet-five

*3:文字列補間式はただの糖衣構文で、実行時にはstring.Format()と等価です。文字列補間式はコンパイル時に構文解析されるため、ミスりづらくてありがたいです。文字列の結合よりは高速だし使わない手はありません。ただし、ボックス化 (boxing) には注意。

*4:入力によって動作は異なりますが、ArgumentOutOfRangeExceptionや、無限の入力待機が起きうる状態です

*5:これは拡張メソッド

*6:これはstringクラスのメンバメソッド

*7:string[] は IEnumerable を第1引数に取る拡張メソッドを使える

*8:stringを受け取ってintを返すラムダ式。型で表すと Func<string, int> となる。

*9:これまでの状態では、Console.ReadLine()の実行タイミングがズレてしまわないようにわざわざローカル変数に導入していました。

*10:Microsoftによると、メソッドはコードブロックとのことです。

*11:using文とは別物です

*12:拡張メソッドを使うためには、その拡張メソッドが定義されている静的クラスのある名前空間名をusingディレクティブに書かなければならない

*13:業務では使わないようにしましょう