SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

C#の復習 - C#7.0編

前回記事の続きです。

hiroronn.hatenablog.jp

今回は、C#7.0を見ていきます。前回と同じく、下記ページを見ながら書いていきます。

docs.microsoft.com

C# バージョン 7.0

この辺りから、関数型言語の影響受けたのかな?という機能がすごく増えたように思います。ソース短く書けるようにすること大事にしてますね。ほんと。

out 変数

リンク

if (int.TryParse(input, out int result)) {
    Console.WriteLine(result);
} else {
    Console.WriteLine("Could not parse input");
}

昔はこうでした。

int result;
if (int.TryParse(input, out result)) {
    Console.WriteLine(result);
} else {
    Console.WriteLine("Could not parse input");
}

outで結果を受けるために別途変数が必要でした。これ面倒だったんですよね…いい改良ですね。でも、新規に関数作るならタプル返したほうが良いと思うので、古いライブラリなどのための機能ですかね。

タプルと分解

リンク

色々増えてますね。Pythonのような他言語だと当たり前にできた機能を移植してきたよ、という印象です。

// Tuple[int, int]型のプロパティ
public (int, int) Value => (12, 13);

// (int a, int b)という形で受けられる
(int a, int b) v = Value;
Console.WriteLine(v.a);
Console.WriteLine(v.b);

// さらに短く
(int a, int b) = Value;
Console.WriteLine(a);
Console.WriteLine(b);

// 使わない変数は明示的に破棄する
(int a, _) = Value;
Console.WriteLine(a);

また、クラスにDeconstructというメソッドを宣言すると、クラスをTupleに分解できるそうな。場合によっては便利そうですね。

パターン マッチング

リンク

パターンマッチングは、Scalaのような関数型言語では一般的に搭載されている仕組みです。型を判定したり、タプルの値で分岐したり、条件分岐したり、いろいろできます。C#の場合は、isswitchが拡張されているようです。

isの拡張

以前のis演算子は、指定した変数に格納されているインスタンスの型を判定し、マッチしたらtrueを返すものでした。インターフェイスの具象型判定や、キャストする前の判定などに用いられていました。しかし、型判定できるだけでキャストは別途実施する必要がありました*1

object o = "Hello";
if (o is string) {
    var s = (string)o;
    Console.WriteLine(s);
}

これが、変数への割り当ても同時に行えるようになっています。

object o = "Hello";
if (o is string s) {
    // var s = (string)o;
    Console.WriteLine(s);
}

switchの拡張

従前は、使える型が非常に制限されており、基本は列挙型に使うような構文でした。これが大きく拡張されており、

  • 型は何でもよい
  • isと同様、新しい変数に割り当てることができる
  • when句を付与し、変数の状態に応じたチェックもできる

という追加機能があります。これにより、型の表現力が上がっています。scalaでよく見ていたUseCaseという型を考えてみます。

interface UseCaseResult { 
    bool IsSuccess { get; }
}

class Success : UseCaseResult
{
    public bool IsSuccess => true;
}

class Failure: UseCaseResult
{
    public Failure(string message) => Message = message;
    public bool IsSuccess => false;

    public string Message { get; }
}

ユースケースの処理結果を表現する型です。実際にはFailureは細分化します。そして、これを使用する場合はこんな感じです。

void Run()
{
    var result = DoSomething();
    switch (result)
    {
        case Success _:
            Console.WriteLine("Success!");
            break;
        case Failure f when f.Message.Contains("fail"):
            Console.WriteLine($"Failed: {f.Message}");
            break;
        case Failure f:
            Console.WriteLine(f.Message);
            break;
        default:
            break;
    }
}

ユースケースの成功や失敗を型で表現できるようになります。便利。breakを省略できないので多少冗長ですが、以前のC#では書けなかった形で書けるのはすごくいいです。

ローカル関数

リンク

メソッド中に関数を宣言できるようになったようです。

ローカル関数を使用すると、別のメソッドのコンテキスト内でメソッドを宣言することができます。 ローカル関数のおかげで、クラスを読み取る際に、ローカル メソッドはそれ自体が宣言されているコンテキストからしか呼び出されないことが、簡単にわかります。

とのこと。たしかに。これと同じような方法を使うなら、以前ならラムダ式Funcの変数に突っ込んでおけばよかったと記憶しています。

以前の記事で書いたメソッドを書き直してみます。

  • 書き直し前
using System.Net.Http;
using System.Threading.Tasks;

public async Task<string> GetResponseAsync(string url) {

    if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) {
        return "This URL is bad format!";
    }

    using (var client = new HttpClient()) {
        var response = await client.GetAsync(url, HttpCompletionOption.ResponseContentRead);
        return await response.Content.ReadAsStringAsync();
    }
}
  • 書き直し後

ローカル関数の宣言箇所はreturnの後じゃなくてもいいよ、ということで、return前でも宣言しています。現実的には、複数のローカル関数を宣言する場合はreturnの後にまとめたほうがよさそうです。

using System.Net.Http;
using System.Threading.Tasks;

public Task<string> GetResponseAsync(string url)
{
    void checkUrl(string url)
    {
        if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
        {
            throw new ArgumentException("This URL is bad format!");
        }
    }

    checkUrl(url);
    return get(url);

    async Task<string> get(string url)
    {
        using (var client = new HttpClient())
        {
            var response = await client.GetAsync(url, HttpCompletionOption.ResponseContentRead);
            return await response.Content.ReadAsStringAsync();
        }
    }
}

長い。これだけだと可読性が上がった気がするだけです。この記法が真価を発揮しそうなのは、下記のページあたりでしょうか。

docs.microsoft.com

Task<T>を返す待機可能なメソッドをいったん変数で受けて、awaitした場合の挙動が異なるとのことです。これは良いですね。awaitする前に例外を吐くの、デバッグが楽そうです。

ローカル関数はラムダ式より冗長に思えるかもしれませんが、実際にはさまざまな目的に役立ち、用途もさまざまです。 ローカル関数は、別のメソッドのコンテキストからのみ呼び出される関数を記述する場合に、より効率が高くなります。

とある通り、良いことは多そうです。私もこれ何の役に立つんだ?と思いましたが、良い点も多そうです。先入観良くない。

拡張された式形式のメンバー

リンク

C#6.0では、関数とgetのみのプロパティを式形式で書ける、という改訂がありました(前回記事参照)が、それが拡張されています。

  • コンストラク
  • デストラクタ(ファイナライザ)
  • プロパティのgetter
  • プロパティのsetter

でも使えるようになりました。便利。なお、この辺りはVisual Studioリファクタリング機能でも修正候補に挙がります。

ref ローカル変数と戻り値

リンク

これは難しい…要するに、値型のコピーを避けてパフォーマンスを上げられるようにした、ということでしょうか?戻り値が本来値型であっても、参照を返すようにできる、ということですかね。

いや、これまでパラメータにしか指定できなかったrefを、メソッドの戻り値にも指定できる、ということですね。たしかに、これまでこれを実現しようとすると、一旦クラスにしてクラスの参照を使いまわす方法ぐらいしかありませんでした。

私の感覚では、Webサイト開発などではあまり使うことはないかなーと。上記リンクに

値のコピーを回避したり、逆参照操作を複数回実行したりすることで、より効率的なアルゴリズムを実現できます

とある通り、巨大な値を保持する場合にコピーを防ぐために使えそうです*2

数値リテラルの構文の改善

リンク

数値の区切り記号として_が使用できるようになりました。

class NumericLiteral
{
    private readonly decimal _million = 1_000_000m;
    private readonly decimal _twoMillion = 20_000__00m;
}

桁区切り記号は定数のどこにでも置くことができます。

とある通り、邪道な書き方もできます。やめましょう。

あと、数値リテラル0bが増えています。こちらに記載があります。0bは2進数、0xは16進数、何もついていなければ10進数です。0xはFlagsAttributeつけたEnumで使ったことあります。

throw式

リンク詳細

C# では、throw は常にステートメントでした。 throw は式ではなくステートメントであるため、使用できない C# コンストラクトがありました。 これには、条件式、null 結合式、および一部のラムダ式が含まれます。 式形式のメンバーが追加されたことにより、さらに多くの場所で throw 式が役に立つようになりました。 C# 7.0 では、このようなコンストラクトを記述できるように、throw 式が導入されています。

そうだったのか…

文はいわゆる構文で、式は評価されて値を返す(変数に代入できる)ものを指します。すべての場所でthrowが式として評価されるわけではなく、書いてある通り条件式、null結合式、ラムダ式のみのようです。なので以下のような記述はエラーとなります。変数に代入できるわけじゃないよ、ということらしいです。

class ThrowExpression
{
    // Compile error: このコンテキストではスロー式は許可されていません。
    private Exception _v = throw new ArgumentNullException("null");
}

文脈に応じて許可される式なんてものも実現できるんですね。scalaみたいにifやswitchも式として評価できるようにしてほしいです。超ムズイだろうとは思いつつ、そうなったらうれしい。

おわりに

out, refの改善のように、そこ直すのかというようなものがありつつ、タプルが使いやすくなったのは良いなと思いました。ほんと、タプル導入されたとき使い辛いなんてものじゃなかったので…ただ、このタプルを使うにはValueTupleという型が必要らしく、 .NET Frameworkだと4.7以降のみ使えるらしいですね。(リンク

次回はC#7.1について調べてみようと思います。

追記

GitHubに、今回書いたコードの一部上げました。

github.com

*1:asを遣えば変換できた場合のみ処理を書くことになるが、結局一緒

*2:値型は単に値を受け渡すとインスタンス自体がコピーされるため、巨大な値を保持するとコピーにかかる消費とメモリの圧迫が起こる