Effective C# 3rd 読書メモ 50 例外フィルタの副作用を利用する

常にfalseを返す例外フィルタを定義することは奇妙に思えるかもしれないが、そうすべき理由がある。例外フィルタはスタックの一部として実行されるので、スタックが破棄される前に実行されるのだ。

これを利用すると、以下のように、例外の詳細な情報をログに書き込むことができる。

public static void Main()
{
    var failures = 0;
    var data = default(string);

    while (data == null)
    {
        try
        {
            data = MakeWebRequest();
        }
        catch (Exception e) when (ConsoleLogException(e))
        {
        }
        catch (TimeoutException e) when (failures++ < 10)
        {
            WriteLine("Time out error: trying again");
        }
    }
}

public static bool ConsoleLogException(Exception e)
{
    var oldColor = Console.ForegroundColor;
    Console.ForegroundColor = ConsoleColor.Red;
    WriteLine($"Error {e}");
    Console.ForegroundColor = oldColor;
    return false;
}

private static string MakeWebRequest()
{
    throw new TimeoutException();
}

以上で、『Effective C#』の第3版、読了です。C#は仕様の大きな言語で、それなりの歴史もあるので、同じことをするにも複数の書き方ができます。典型的な例はローカル変数の定義で、型を明示的に指定することもできれば、型推論に任せることもできます。初心者のうちは、どちらの書き方を使えばいいのかわかりませんでした。

本書には、そういったC#を書く上での迷いどころや落とし穴の回避方法がまとめられています。『やさしいC#』等の入門書を読んだ後、2冊目ないし3冊目あたりで読むと良いと思いました。

ちなみに、『Effective C#』の類書として『実戦で役立つ C#プログラミングのイディオム/定石&パターン』があります。こちらは入門書の次に読んでも大丈夫なくらいの内容で、C#で何かをしたいときに役立つ書き方が「イディオム」としてまとめられています。

Effective C# 3rd 読書メモ 49 例外フィルタを使う

通常のcatch節では、例外の型に基いて捕捉を行う。例外をキャッチしたらリトライするような実装では、最終的に、例外を再度throwすることになる。このやり方では、その後の分析が難しくなる。このような場合には、例外フィルタを使うべきである。

例外フィルタは、catch節にwhenキーワードを重ねて使用する。

public static void Main()
{
    var retryCount = 0;
    var data = default(string);

    while (data == null)
    {
        try
        {
            data = MakeWebRequest();
        }
        catch (TimeoutException e) when (retryCount++ < 3)
        {
            WriteLine("Operation timed out. Trying again");
            Task.Delay(1000 * retryCount);
        }
    }
}

private static string MakeWebRequest()
{
    throw new TimeoutException();
}

例外フィルタを使用すると、例外をキャッチした際にもコールスタックは破棄されないので、例外の発生した状況をあとで追いやすい。また、例外フィルタを使用したほうがパフォーマンスも良くなる。

Effective C# 3rd 読書メモ 48 例外:強い保証を使用する

例外が投げられると、処理の流れは中断される。不適切な方法で例外を投げると、それをハンドリングすることは困難である。例外の投げ方にはベストプラクティスがある。

  1. 基本的な保証
  2. 強い保証
  3. no-throw保証

基本的な保証は、例外が投げられた後でも、そのメソッドにおいて全てのオブジェクトがリークせず、有効な状態であることを意味する。強い保証は、基本的な保証に加えて、例外が発生した場合でも、プログラムの状態が変わらないことを意味する。no-throw保証は、処理が絶対に失敗せず、例外が投げられないことをいう。

このうち、強い保証を採用することが、安全性とシンプルさの点からバランスがよい。強い保証のもとでは、メソッドの結果は、処理が完了するか、何も変わらないかのいずれかである。中間点はない。強い保証の利点は、例外をキャッチした後でも処理を継続しやすいことだ。

強い保証のためには以下のような(今までに紹介してきた)テクニックが使用できる。

  • プログラムが使用するデータはイミュータブルな値型に入れる
  • LINQクエリのような関数型プログラミングのスタイルを使用する

データの書き換えは以下の様式に沿って行うべきである。

  1. 変更されるデータのコピーを作成する
  2. コピーに対して変更を行う
  3. コピーをオリジナルと置き換える
public class SalaryManager
{
    public void PhysicalMove(string title, decimal newPay)
    {
        // コピーを作成
        var d = new PayrollData(title, newPay);
        // オリジナルと置き換え
        PayrollData = d;
    }

    public PayrollData PayrollData { get; private set; }
}

public struct PayrollData
{
    public PayrollData(string title, decimal pay)
    {
        Title = title;
        Pay = pay;
    }

    public string Title { get; }
    public decimal Pay { get; }
}

しかし、強い保証を使用するとパフォーマンスの問題が発生することもある。たとえば、ループで大量のオブジェクトを書き換える場合には、コピーを作って差し替える方式は効率が悪い。

また、差し替え方式は、参照型では使用することができない。コピーとオリジナルと差し替えても、オリジナルを参照しているオブジェクトが残っている場合があるからだ。

実用的な対処法は、以下のように新しくデータを作り直すことである。

private List<PayrollData> data;
public IList<PayrollData> MyCollection
{
    get
    {
        return data;
    }
}

public void UpdateData()
{
    // 失敗する可能性のある処理
    var temp = UnreliableOperation();

    data.Clear();
    foreach (var item in temp)
        data.Add(item);
}

上の方法は絶対確実な方法とはいえない。絶対確実にするためには、以下のように、データを包含するクラス(Envelope)を作成し、そのクラスに置き換えを行わせる必要がある。

public class Envelope : IList<PayrollData>
{
        private List<PayrollData> data = new List<PayrollData>();

        public void SafeUpdate(IEnumerable<PayrollData> sourceList) {
            // コピーの作成
            List<PayrollData> updates =
                new List<PayrollData>(sourceList.ToList());
            // 差し替え
            data = updates;
        }

        public PayrollData this[int index]
        {
            get { return data[index]; }
            set { data[index] = value; }
        }

        public int Count => data.Count;

        public bool IsReadOnly =>
            ((IList<PayrollData>)data).IsReadOnly;
            public void Add(PayrollData item) => data.Add(item);

        public void Clear() => data.Clear();

        public bool Contains(PayrollData item) =>
            data.Contains(item);

        public void CopyTo(PayrollData[] array, int arrayIndex) => data.CopyTo(array, arrayIndex);

        public IEnumerator<PayrollData> GetEnumerator() =>
            data.GetEnumerator();

        public int IndexOf(PayrollData item) =>
            data.IndexOf(item);

        public void Insert(int index, PayrollData item) =>
            data.Insert(index, item);

        public bool Remove(PayrollData item)
        {
            return ((IList<PayrollData>)data).Remove(item);
        }
        public void RemoveAt(int index)
        {
            ((IList<PayrollData>)data).RemoveAt(index);
        }
        IEnumerator IEnumerable.GetEnumerator() =>
            data.GetEnumerator();
}

このクラスは以下のように使用する。

private Envelope data;
public IList<PayrollData> MyCollection
{
    get
    {
        return data;
    }
}

public void UpdateData()
{
    data.SafeUpdate(UnreliableOperation());
}

所感

「強い保証」の考え方はRDBMSのトランザクションっぽくて面白い。1メソッドを1トランザクションと考えて、そのメソッド内で処理が完了すればコミット、何かうまくいかないことがあればロールバック。

Effective C# 3rd 読書メモ 47 アプリケーション固有の例外クラスを作る

例外は、発生した場所から遠く離れた場所で処理されることがある。例外オブジェクトには、エラーの発生原因に関する情報が全て含まれていなければならない。アプリケーション固有の例外を作りたくなるかもしれないが、そのためには様々な事項を考慮する必要がある。

はじめに、新しい例外を作る場合とその理由、作成方法について理解する必要がある。独自の例外クラスは、あなたのライブラリの利用者が、その例外をキャッチして特別な処理を行いたいと考えられる場合にのみ追加すべきである(たとえば、引数がnullの場合に例外を投げるだけなら、ArgumentExceptionで十分かもしれない)。

例外クラスの名前の末尾は「Exception」でなければならない。System.Exceptionか、あるいはそれ以外の適切な例外クラスを継承すべきである。また、ベースクラスの全てのコンストラクタを引き継ぐべきである。System.Exceptionをベースにした例外クラスは以下のように定義できる。

[Serializable]
public class MyException : Exception
{
    // デフォルトコンストラクタ
    public MyException() : base()
    {
    }

    // メッセージ付き
    public MyException(string s) : base(s)
    {
    }

    // メッセージと内部例外
    public MyException(string s, Exception e) : base(s, e)
    {
    }

    // 入力ストリームから作成
    public MyException(SerializationInfo i, StreamingContext c) : base(i, c)
    {
    }
}

モダンJavaScriptの練習に:『Electronではじめるアプリ開発』

Electronは、Webの技術(HTML+CSS+JavaScript)を使用してデスクトップアプリケーションを構築するためのフレームワークです。Atom、Visual Studio Codeといったモダンなエディタや、Slackのデスクトップ版などに利用されています。ChromiumというChromeのベースとなっているブラウザを使用することで、Windows/macOS/Linuxといった様々なOS上で動作する、クロスプラットフォームなデスクトップアプリケーションを構築することができます。

本書では、ES2015 + Reactという組み合わせでアプリケーションを構築していきます。ES2015やReactについての詳しい解説は無いので、あらかじめES2015のクラス構文やアロー関数式、Reactの基本的な使い方を身に着けておいたほうが読みやすいでしょう。

ES2015については『改訂新版JavaScript本格入門』がおすすめです。Reactについては、公式チュートリアルをやっておけば大丈夫でしょう。
以下のようなソースを見て、だいたい何をやっているかわかればOKです。

// Previewer.jsx
import React from "react";
import marked from "marked";
import style from "./Previewer.css";
import emojione from "emojione";

marked.setOptions({sanitize: true});

const renderer = new marked.Renderer();
renderer.text = (text) => {
    return emojione.shortnameToImage(text);
};

export default function Previewer(props) {
    return (
        <div
            id="previewer"
            className={`${props.className} ${style.previewer}`}
        >
            <span
                dangerouslySetInnerHTML={{__html: marked(props.value, {renderer})}}
            />
        </div>
    );
}

本書には、様々なライブラリを活用して、手早く高機能なアプリケーションを構築する方法が記載されています。また、各章はチュートリアル形式になっているので、実際に手を動かしながら学ぶことができます。Electronに興味がある人だけでなく、モダンなJavaScriptアプリケーションの組み方について理解を深めたい人にもおすすめできる内容です。