2018年4月29日日曜日

自然なソート順序

突然ですが、Windowsのエクスプローラーのソートってなかなか気が利くと思いません?
ファイル名でソートを掛けると、上記の画像のような順に並び変わります。
それでは、これを通常のソートを掛けるとどうなるでしょう。
foreach(var s in new[] { "test01.txt", "test1.txt", "test3.txt", "test10.txt", }.OrderBy(p => p))
    Console.WriteLine(s);
あれ?エクスプローラーだとtest10.txtよりtest3.txtのほうが前に来ていたけどC#のOrderByだと逆転しているぞ?バグかな?

そんなことはありません。
OrderByは文字列を辞書順に並び替えます。もっと正確に言えば、文字コードの若い順から辞書順に並び替えます。ですので、1文字ずつ文字コードを比較していって、test10.txtの1とtest3.txtの3を見て、前者のほうが若番なので辞書的に前だと判断しているのです。

ですが、実際にファイル名を並び替えた人からしたらtest3.txtとtest10.txtは前者のほうが前に来てほしいですよね。これが「Natural Sort Order(自然なソート順序)」です。


さて、このようなソートをする方法ですが、ググるといろいろな方法が出てきます。丸投げしたいのならば、Nugetを漁ったりStrCmpLogicalW関数をP/Invokeで使うのも手でしょう。ですが、私はあまり気に入った実装が無かったので自分で実装してみることにしました。

まずは下準備として、文字列を数字と文字の境界で区切るメソッドを作ります。
internal static IEnumerable<string> SplitBy(this string Source, Func<char, char, bool> BorderSelector)
{
    int start = 0;
    for(int i = 0; i < (Source.Length - 1); i++) {
        if(BorderSelector(Source[i], Source[i + 1])) {
            yield return Source.Substring(start, i + 1 - start);
            start = i + 1;
        }
    }
    yield return Source.Substring(start, Source.Length - start);                
}
文字列の前後2文字を渡して境界かどうかを判定するデリゲートを渡せば、そこで区切った文字列を返してくれます。ちゃんと遅延評価も行えるように気を配って実装しました。
これを使って、IComparer<string>を実装します。
public class NaturalComparer : IComparer<string>, System.Collections.IComparer
{
    static Func<char, char, bool> NumberCharBorder = (p, n) => (('0' <= p && p <= '9') && !('0' <= n && n <= '9')) || (!('0' <= p && p <= '9') && ('0' <= n && n <= '9'));

    public int Compare(string x, string y)
    {
        using(var xe = x.SplitBy(NumberCharBorder).GetEnumerator())
        using(var ye = y.SplitBy(NumberCharBorder).GetEnumerator()) {
            while(true) {
                var xHasNext = xe.MoveNext();
                var yHasNext = ye.MoveNext();

                if(xHasNext && yHasNext) {
                    int ret = (ulong.TryParse(xe.Current, out ulong xi) && ulong.TryParse(ye.Current, out ulong yi)) ?
                        Comparer<ulong>.Default.Compare(xi, yi) :
                        Comparer<string>.Default.Compare(xe.Current, ye.Current);

                    if(ret != 0) return ret;
                } else
                    return (xHasNext ? 1 : 0) - (yHasNext ? 1 : 0);
            }
        }
    }

    int System.Collections.IComparer.Compare(object x, object y)
    {
        try {
            return Compare((string)x, (string)y);
        }
        catch(InvalidCastException e) {
            throw new ArgumentException(e.Message);
        }
    }

    public static NaturalComparer Default
    {
        get
        {
            if(_Default == null)
                _Default = new NaturalComparer();
            return _Default;
        }
    }
    static NaturalComparer _Default = null;
}
xとyを先ほどのSplitByで分割し、それを同時に列挙していっています。
MoveNext()Currentを自前で呼ぶのはできれば避けたかったのですが、 例えばZipで同時に列挙すると、xとyの分割したときの長さが分からなくなってしまうので、やむを得ず手動で列挙することにしました。
分割さえできれば後は簡単です。分割項が両方とも数字(ulong)として読めるならば数字として比較し、それ以外なら文字列として比較します。比較結果が一致しなかったら、その大小関係を返し、一致したら次の分割項へと移っていきます。分割項が片側無くなった場合は、まだ残っているほう(元の文字列が長いほう)を大として返します。
 絵にするとこんな感じです。文字列(青線)と数字(橙線)に分け、それぞれで大小比較をします。例2のように片側がすでに末尾に達して、もう片側は末尾に達していなかった場合は、後者を大としています。

C#のLINQのOrderByにはIComparer<T>を受け取るオーバーロードがあるので、これに渡してあげればこの自然順ソートができます。
が、せっかくなので、親切に拡張メソッドも作っておいてあげましょう。
public static IOrderedEnumerable<T> NaturallyOrderBy<T>(this IEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.OrderBy(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyOrderBy(this IEnumerable<string> Source)
{
    return Source.OrderBy(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyOrderByDescending<T>(this IEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.OrderByDescending(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyOrderByDescending(this IEnumerable<string> Source)
{
    return Source.OrderByDescending(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyThenBy<T>(this IOrderedEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.ThenBy(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyThenBy(this IOrderedEnumerable<string> Source)
{
    return Source.ThenBy(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyThenByDescending<T>(this IOrderedEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.ThenByDescending(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyThenByDescending(this IOrderedEnumerable<string> Source)
{
    return Source.ThenByDescending(p => p, NaturalComparer.Default);
}
単純にOrderByなどのメソッドをラップしているだけです。このNaturallyOrderByにはstringの配列を渡すことが圧倒的に多いはずですので、KeySelectorを省略できるオーバーロードも用意してあげました。

今回のこのプログラムでは正規表現も使っていませんし、比較時の列挙も1回きりです。相当なパフォーマンスで動作してくれると思います。
これで、自分の作るソフトの中でも自然順ソートを気軽に使えるようになりました。



余談1
Windowsエクスプローラーがこのソート順序に対応したのはXPくらいからだったと思います。少なくともWindows98のときは対応しておらず、ちゃんとファイル名の数字の桁数をそろえて(桁数が少ないものは0埋めして)やらないとソートできなかった記憶があります。
StrCmpLogicalWがあってもStrCmpLogicalAが無いことからしても、Windows 9x系統では実装されていなかったんでしょうね。

余談2
英語圏の人が思いつく「自然なソート順序」はこれくらいかもしれませんが、日本語圏にいると「全角/半角を区別せずにソート」とか「漢数字も数字としてソート」とかいろいろ思いつくと思います。
もっと極端な例を言えば「前編」「後編」が付くファイル名があれば「前編」のほうが前に来てほしいですが、文字コードでは前>後です(おそらく音読みの「ゼン」「コウ」で五十音順に並べているため)のでそのようにはなりません。ただ、前原さんと後藤さんだったら後藤さんのほうが前に来てほしいですし、単純に「前」を前に持ってくればいいというわけでもありません。
そういう意味からしても「自然なソート順序」というのは非常に曖昧な定義で、網羅的に実装するのには無理があるでしょう。どこで決着をつけるかはプログラマーのさじ加減かと思います。

2018年4月7日土曜日

アンマネージドリソースをDisposeパターンで管理する

モダンな言語や環境はガベージコレクタ(GC)が付いているのが当たり前になった時代です。GCによってどこからも参照されなくなったインスタンスは適当なタイミングで解放されるため、「メモリを解放する」という本来ならばものすっごく神経を使う作業から人類は解放されました。それはそれで便利で歓迎されることなのですが、かと言ってありとあらゆるリソースの解放をGC任せにできるわけでもありません。

そこで、.NET FrameworkにはIDisposableというインターフェイスを用意されていて、もうこれ以上このインスタンスは使わないから明示的にリソースを解放したいと思ったときにはDisposeメソッドを呼べばいいようになっています。
このインターフェイスは様々なクラスで使用されており、例えばイベントの購読停止だったり、ファイルのロックの解除だったりといった終了処理が行われます。また、言語面でも優遇されており、using構文を使うことで自動的にtry-finally構文に展開され確実にDisposeメソッドを呼ぶことができるようにもなります。

逆に、自動で解放されないリソースを使用する場合、それを使うクラスでIDisposableインターフェイスを実装しなければなりません。
IDisposeインターフェイスがDisposeメソッド1つのみしか持たないので「そのDisposeメソッドの中で解放処理をすればいいんでしょ?」と思ってしまいたくなりますが、実は話はそんなに単純ではありません。Disposeパターンと呼ばれるもうちょっとしっかりした実装が必要になってくるので、この記事では自動で解放されないリソースをサンプルとして使いつつその実装のしかたを見ていきたいと思います。

Disposeパターン

さて、 Disposeパターンはどういう書き方をするのか、ひな形を暗記しなければならないのかと思ったそこのあなた、心配いりません。IntelliSenseがひな形を用意してくれています。
これに従ってひな形を作るとこのようなコードが自動生成されます。
public class ExcelApp : IDisposable
{
    #region IDisposable Support
    private bool disposedValue = false; // 重複する呼び出しを検出するには

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: マネージ状態を破棄します (マネージ オブジェクト)。
            }

            // TODO: アンマネージ リソース (アンマネージ オブジェクト) を解放し、下のファイナライザーをオーバーライドします。
            // TODO: 大きなフィールドを null に設定します。

            disposedValue = true;
        }
    }

    // TODO: 上の Dispose(bool disposing) にアンマネージ リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします。
    // ~ExcelApp() {
    //   // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
    //   Dispose(false);
    // }

    // このコードは、破棄可能なパターンを正しく実装できるように追加されました。
    public void Dispose()
    {
        // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
        Dispose(true);
        // TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。
        // GC.SuppressFinalize(this);
    }
    #endregion
}
コメントの翻訳がガバガバで多少見苦しいですが、目を瞑ってあげましょう。

まず注目すべきは、インターフェースで規定されたDispose()メソッド以外にDispose(bool disposing)メソッドが用意されています。Dispose()メソッドからはDispose(bool disposing)メソッドを呼び出しているだけになっていますね。

実は、リソース解放処理を行うのはDispose(bool disposing)メソッドのほうになっています。この引数のdisposingは「Dispose()メソッドの呼び出しによってリソース解放処理を行うのかどうか」を示す引数です。

GCがこのクラスを回収に来た時、すなわちファイナライザが呼ばれたときは、そのクラス内で使っているGCが回収できるリソース(マネージドリソース)を敢えて解放する必要はありません。なぜならば、それらもGCが回収するからです。ですが、わざわざDispose()メソッドを呼び出してリソースを解放しようとするときは、内部的に使っているマネージドリソースもしっかり解放してあげないと、当然そのタイミングではGCが回収しに来ないためリソース解放漏れになります。

逆に、アンマネージドリソースは、Dispose()が呼び出されたときでもファイナライザが呼び出されたときでも確実に解放する必要があります。ですので、if(disposing)のブロックの外に解放処理を書き、ファイナライザをコメントアウト解除する必要があります。
また、ファイナライザに呼ばれるより先にDispose()メソッドが呼ばれた場合は、解放処理が重複してしまいますので、GC.SuppressFinalize(this);を呼び出してファイナライザの作動を抑制する必要があります。

あとは、disposedValueフィールドですでにこのクラスが破棄されたかどうかを保持しておりますので、もしもDispose後にこのクラスのメソッドやプロパティにアクセスされたときはObjectDisposedExceptionを投げるようにしてあげましょう。

サンプルコード:COMによるC#からのExcel操作

さて、Disposeパターンの実装がわかったところで、自動で解放されないリソースをDisposeパターンで記述するサンプルコードを書いてみましょう。
自動で解放されないリソースの代表格としてCOMがあります。「C# Excel」とかでググるといくらでもCOMを使った記事が出てきますが、その記事の数だけ「ソフトを終了したのにタスクマネージャーを開いたらEXCEL.EXEが残ったままだ」というコメントが見られることからも、リソースの解放が重要になってくるリソースです。
using Excel = Microsoft.Office.Interop.Excel;

public class ExcelApp : IDisposable
{
    Excel.Application excel = null;
    Excel.Workbook workbook = null;
    
    public ExcelApp(string filename)
    {
        excel = new Excel.Application();
        excel.Visible = true;

        workbook = excel.Workbooks.Open(filename);
    }

    #region IDisposable Support

    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: Managed Objectの破棄
            }

            if(workbook != null) {
                workbook.Close();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
                workbook = null;
            }

            if(excel != null) {
                excel.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excel);
                excel = null;
            }

            disposedValue = true;
        }
    }

    ~ExcelApp()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion
}
さて、Disposeパターンに従って書くとこのような感じになります。
COMはアンマネージドリソースですので、ファイナライザやSupposeFinalizeメソッドのコメントアウトを解除する必要があります。

ときどきSystem.Runtime.InteropServices.Marshal.ReleaseComObject(obj);の必要性やについての議論や、ひどい場合は「これを書かないからEXCEL.EXEプロセスが残ってしまう」といった誤った記述がされているサイトがありますが注意してください。
Workbook.Close()でワークブックを閉じて、Application.Quit()でアプリケーションを閉じさえすればEXCEL.EXEプロセスは正常に終了されます。Marshal.ReleaseComObjectは、.NETからのCOMオブジェクト(=Excelを操作するためのインターフェイス)を解放するためのメソッドで、これの有無にかかわらずExcelはしっかりと終了してくれます。
リソースの解放をしたらそれ以降はExcelの操作をしないわけですから、COMオブジェクトを解放しておくべきでしょう。

こうすることで
using(var excel = new ExcelApp(@"test.xlsx")) {

}
このように書くだけで、usingブロックを抜けたときにExcelがしっかりと終了されます。この場合はDispose()メソッドが呼ばれることによりExcelの終了処理が行われていることがわかります。

他にも、敢えてDispose()を呼び出さないようなコードを書いても、ちゃんとアプリケーション終了時にGCがファイナライザを呼び出してExcelが終了されることが分かります。 このDisposeパターンは確実にアンマネージドリソースを解放する方法として有効でしょう。

※上記のDisposeパターンのプログラムは、Disposeパターンの説明をするために書いているものでExcelを終了させるうえで完璧ではない点に注意してください。例えば、Excelを開いている間にファイルに変更を加え、保存せずにDisposeメソッドが呼ばれるとExcelのメッセージボックスで終了処理がブロックされます。
利用者は、各自アレンジして使ってください。