2018年9月24日月曜日

Synology NAS上にGitサーバーを立ててVisual Studio 2017からpushする

半年くらい前にSynologyという会社のNASを買いました。

NASと言ったらBuffaloのイメージが強かったんですが、このSynology NASはもうほとんどただのLinuxパソコンのようで、Webからログインするとデスクトップのような画面が表示されて、そこでパソコンを操作するかのようにNASを管理することができます。また、いろいろなアプリケーションがパッケージセンターに公開されていて、それらをインストールすることで自分好みのNASにしていくことができます。例えばDropboxのように自分の各PC間でデータを同期させたり…。非常に強力なNASで、もはやただのNASというより「自前のクラウド」と言ったほうが良いような代物でした。

ですので、ただのNASとして使って、ひたすらファイルを詰め込むだけじゃなんかもったいないなと思い、Gitサーバーを立ててみることにしました。
自分でしか使わないのにわざわざGitサーバーを立てる意味とは…

準備

  • Visual Studio 2017をインストールした環境を2つ以上
  • 各PCにGitクライアントをインストール
  • Synology NASを使えるようにする

NASの準備

 まずはパッケージセンターからGitをインストールします。

次はSSHを有効にしておきます。コントロールパネルの「端末とSNMP」から有効にします。
この設定画面に書いてありますが、Synology NASはadministratorグループに属するアカウントでしかSSH接続できません。何このクソ仕様…。


そしたら次はGit用の共有フォルダを作りましょう。名前は何でもいいですが、今回はGitRemoteにしてみました。


続いてGitアクセス用のユーザーグループを作ります。コントロールパネル→グループで作成します。今回は「gitgroup」にしました。なんでもいいですが、小文字のほうがセンスがあります。
もちろんアクセス権限はGitRemoteフォルダのみに限定して、読み書き両方の権限を与えます。

つづいてユーザーを作ります。今回は「gituser」にしました。例によってなんでもいいですが、小文字のほうがセンスがあります。

グループはgitgroupのほか、administratorに所属させる必要があります。これは外部からSSH接続するためです。

administratorだとデフォルトですべてのフォルダにアクセスできてしまいますので、余計なフォルダへのアクセス権は消しておきましょう。
まあ、administratorなので自身のアクセス権の変更もできてしまうのですが…。


最後にGitを起動し、gituserにアクセス権を与えて準備完了です。

Gitリポジトリの作成

つづいてリポジトリを作成します。
SSHでNASに接続して操作をします。ターミナルはSSHがつながればなんでもいいですが、今回はPuTTYを使いました。

ログインしたら作成した共有フォルダ(/volume1/GitRemote)に移動し、リポジトリとなるフォルダを作ります。今回は「TestProject.git」としました。そして、
git --bare init --shared TestProject.git
を実行することで空のGitリポジトリを作成します。

このままでは全ユーザーにアクセス権が与えられてしまいますので、gitgroupに限定しましょう。
chown -R gituser:gitgroup TestProject.git/
chmod -R 770 TestProject.git/ 
これでグループ内にのみrwxのアクセスが与えられました。
 
これでGitリポジトリの作成は完了です。


Visual Studioでプロジェクトを作る

Visual Studioでプロジェクトを作ります。新しいプロジェクトを作る際に「新しいGitリポジトリの作成」にチェックを入れるのを忘れないようにしてください。

プロジェクトが出来上がったら、「チームエクスプローラー」→「設定」→リポジトリの設定」からリモートの「追加」ボタンを押します。
リモート名はorigin、フェッチ/プッシュはリポジトリのURLを指定します。今回は「ssh://gituser@[NASのIPアドレス]/volume1/GitRemote/TestProject.git」になります。

つづいて「チームエクスプローラー」→「同期」を開きます。
ここにある「プッシュ」ボタンを押せば、先ほど指定したリポジトリにプッシュを行います。

Gitのパスワード入力画面が開くので、ここでgituserのパスワードを指定します。

正常にプッシュされました。めでたしめでたし。

リポジトリからのクローン

別PCでリポジトリをクローンしてみます。
Visual Studioはクローンには対応していない(はず)なので、こればかりはコマンドラインから実行します。Visual StudioのプロジェクトのディレクトリからPowerShellを開き、
git clone ssh://gituser@[IPアドレス]/volume1/GitRemote/TestProject.git 
と入力すればリモートリポジトリからクローンします。
 
ちゃんとクローンされたプロジェクトが開けました。めでたしめでたし。

pushからの別PCからのpull

せっかく別PCでクローンしたので、こちらで編集してプッシュしてみます。
Hello, worldのコードを追加し、「チームエクスプローラー」→「変更」からローカルリポジトリにコミットします。
 
つづいて「チームエクスプローラー」→「同期」からプッシュします。

パスワードを入力したら無事プッシュされました。

元のPCに戻って、「チームエクスプローラー」→「同期」から今度は「プル」を選択します。

プルが完了したら見事にHello, worldのコードが出現しました。めでたしめでたし。

まとめ

これで無事NASにてGitサーバーを運用し、複数PCでプロジェクトを同期することができるようになりました。

Synology NASはadministratorグループに属していないとSSH接続ができないというちょっとアレな仕様があります。まあ、SSHでログインするなんてroot取って何かしらアレなことをしたい人くらいしかいないだろうという想定なんでしょうが、この仕様から、NASを利用する一般ユーザーにNAS上のリポジトリを触らせるのにはセキュリティ上の問題があります。これがどうにかならない限り、ソロGitからは抜け出せないでしょう。
えっ?ソロGitってpushする必要性あるの?????

あと、たぶんですけどVisual Studioは公開鍵認証をする方法が無さそうです。せっかくSSHならば公開鍵認証で認証を行って、毎回パスワードを入力しなくてもいいようにしたいんですけどねえ。

2018年9月2日日曜日

ダイキンのエアコンのリモコンを解析する

身の回りの家電には赤外線リモコンで操作するものがたくさんあります。エアコンもその一つです。それらのリモコンと同じ赤外線信号を送れるデバイスを作ることができれば、例えばネットワーク経由で本来リモコンが届かないようなところから家電を操作することができ、便利です。

というわけで、ダイキンのエアコンのリモコンを解析してみました。
解析したのは「ARC478A18」という型番のリモコンです。

幸運なことに、世の中のリモコンはだいたい940nmくらいの赤外線を使っており、だいたい38kHzで変調されています。なので、市販の赤外線受信モジュールで読めてしまうのです。
写真に写っているのはDSO touchというオシロスコープで、時間軸方向で8192サンプルのデータを取って保存することができるので、ロジアナとしても使うことができます。
これである程度のデータを集めることができて、物理層としてのフォーマットがわかれば、後はArduinoでもなんでも適当な受信ソフトを作り、リモコンで様々な操作を行って大量のデータを取ればどのようなコマンドを送っているかわかってくるはずです。

物理フォーマット 

ズバリ、物理フォーマットはこのようになっています。
データはいわゆる「家製協(AEHA)フォーマット」です。詳しくは以下のページが参考になります。
赤外線リモコンの通信フォーマット
このサイトにT=425μs (typ.)と書いてありますが、このリモコンもそれくらいでした。1回のリモコン操作で2つのフレームを送っており、第1フレームが20バイト、第2フレームが19バイトです。第1フレームと第2フレームの間には35.2msの間隔(トレーラー含む)が空いており、第1フレーム開始の30ms前からON/OFF=1T/1T('0'の信号)を5回(+トレーラー)送るようです。
この冒頭の点滅信号は何なのかよくわかりません。我が家のエアコンでは特にこの点滅を送らなくても(いきなり第1フレームを送出しても)ちゃんと受信してくれました。同期信号か何かなんですかね。

データ内容

続いて肝心のデータ内容です。かなりめんどくさいです。
それもそのはずです。エアコンにはいろいろな機能がありますが、1回の送信ですべての機能の設定内容を送っています。前回送信分との差分だけ(例えば『設定温度+1℃』のような内容のみ)の送信では、エアコンがリモコンの信号を受信できなかったときに、それ以降のリモコンの表示内容とエアコンの設定との整合性が崩れてしまうからです。

1フレーム目


2フレーム目


フレーム内容はこんな感じです。
AHEAフォーマットではデータは各バイトともLSB→MSBの順に送られている点に注意してください。補足欄に書いている数字はちゃんとMSBが左側になるように書いています。
最初の2バイトはカスタマーコード、3バイト目の下位4bitがカスタマーコード4bitずつをXOR取った値になっています。最後の1バイトはチェックサムで、それまでの送信データをすべて足し合わせた値になります。

なお、1フレーム目の「操作内容」は下表のとおりです。
操作内容
0x02 停止
0x03 設定温度変更
0x06 風向変更
0x07 風量変更
0x0C タイマー取消
0x0D 快適自動
0x0E 冷房
0x0F 除湿
0x10 暖房
0x16 風向左右変更
0x18 内部クリーン変更
0x1A 送風
0x1C ランドリー変更
0x1E 入タイマー変更
0x1F 切タイマー変更
0x2F おやすみ変更
0x34 風ないス変更
0x35 ストリーマ変更

何かボタンを押したときは、1フレーム目のText9にそのボタンに対応するこの値が入ります。

また、例えばランドリーモードは自動的に「風向上から2つ目」「切タイマー3時間」が入りますが、1frame目のText12の風向や2frame目のText11, 12の切タイマーはちゃんと風向上から2つ目、切タイマー3時間が入ります。

設定温度(2frame目Text6)はちょっとややこしいですが、絶対値の設定温度(冷房・暖房)の場合はそのままです。例えば25℃なら0b00110010(=50)です。設定温度は0.5℃単位で指定できますから、このように2倍の値を送っているのでしょうね。
相対値の設定温度(快適自動・除湿)の場合は下位5bitを使って設定します。上位3bitは0b110です。ですので、例えば+5℃設定なら0b11001010(下位5bitは0b01010=10)です。-5℃ならば0b11010110(下位5bitは10=0b01010の2の補数(反転させて1を足す)で0b10110)となります。

入タイマー/切タイマー時間は分単位で12bitで表されるので、例えば6時間だったら360分=0b000101101000になります。リモコンでは1時間単位でしか指定できませんが、ここに5分を指定したら本当に5分で止まるんでしょうかね。


それにしても、わざわざ2frameも送って設定をするのは何のためなんでしょうね。最初は昔の機器との互換性かと思っていましたが、大部分の設定内容が2frame目に入っている割には風向は1frame目にしか入っていませんし、すごく片手落ち感がありますよね。何に使っているかわからない(どんなボタンを押しても同じ1/0しか送信しない)ビットもたくさんありますし、1frameに圧縮して全データを送ることはできなかったんでしょうかね…。

2018年8月8日水曜日

100件目の記事

初めての投稿から4年ちょっと経ちましたが、読者の皆さんに支えられてついに100件目の記事に到達しました。ありがとうございます。この記事が100件目です。

1年ちょっと前にはトータルPVが10万件を突破したということで記事にしましたが、すでにもう20万件に達しようとしています(この記事執筆時点で195,044件です)。
3年で到達した10万件を今度は1年半程度で実現する勢いで、発言力が上がってきているのを日に日に感じてうれしい反面、そんなに責任を持った発言をしているつもりもないので複雑な気持ちです。

せっかくなので、今回もここ1か月間のトップ5の記事を紹介します。

1位MVVMとは何か2017/11/18702PV
2位ポケモン「サファイア」の時計を復活させる2016/01/22667PV
3位WPFのScrollViewerやScrollBarのスクロール位置を同期させる 2015/01/16313PV
4位プロキシ設定とレジストリ2015/11/09224PV
5位C#の排他制御 - UpgradeableReadLock 2017/11/05209PV

前回のトップ5のうち3件が今回もランクインしていますが、1位は最近書いたMVVMに関する記事になっているようです。
MVVMは難しいです。何をもってMVVMなのか、これはMVVMなのか、私も今でもよく迷います。でも、皆さんも同じく迷っているということなのでしょうね。

それにしても、トップ記事の月間PVが2倍くらいになっています。上にはトータルPVで見た話をしましたが、月間PVでも如実に表れていますね。これだけPV稼げるようになってきたのなら、広告を掲載したら小遣い稼ぎ程度にはなるんでしょうかね。


ま、これからも気負わず何か気づいたことがあったら記事にしていこうと思いますので、これからも遠くからあたたかな目で見守っていただければと思います。よろしくお願いいたします。

InteractionMessageActionの自作

MVVMのデータバインディング機構は「状態」を伝達するのに非常に便利です。一方で、一般的なグラフィカル・ユーザー・インターフェースでは「状態」だけでは伝えきれない動作もしばしばあります。

その代表的なものがウィンドウ遷移です。ViewModelが「ダイアログボックスを表示しなさい」という指示を出し、それを受け取ったViewがウィンドウを表示するという動作は、状態では表現できません。
ウィンドウも「表示状態」「非表示状態」の2種類の状態がある、と考えれば確かに状態で表現できそうな気もしますが、それはあまり自然な発想ではないですよね。

そのため、解決策として多くのMVVMライブラリにはそのような指示をViewModelからViewへ伝達する機能を備えています。
LivetではMessengerというものがそれに該当します。ViewModel側でInteractionMessageに伝えたい情報を載せてMessenger.Raiseすると、View側のInteractionMessageTriggerが発動し、その受け取ったメッセージをもとに様々な動作を行うことができます。

例えばメッセージボックスを表示する例です。
<Window x:Class="MessengerTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:MessengerTest.Views"
        xmlns:vm="clr-namespace:MessengerTest.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Info">
            <l:InformationDialogInteractionMessageAction />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        
    </Grid>
</Window>
ダイアログボックスはInteractionMessageTriggerの部分です。キーを設定し(今回は"Info")、そのキーに対してメッセージを送ると中のInformationDialogInteractionMessageActionが発動します。

肝心のメッセージはこのように送ります。
public class MainWindowViewModel : ViewModel
{
    public void Initialize()
    {
        Messenger.Raise(new InformationMessage("InformationMessage raised.", "Information", MessageBoxImage.Information, "Info"));
    }
}
Messenger.Raiseに必要な情報を与えて呼び出すと、このタイミングでInformationDialogInteractionMessageActionが発動します。
ポイントは、InteractionMessageとInteractionMessageActionは対になっていて、パラメーターの箱(Message)とそれを受けた動作(Action)をうまく定義することで、ViewModelからViewにいろいろな動作を働きかけることができるということです。
LivetでどのようなInteractionMessageやInteractionMessageActionが用意されているかというのは、以前書いたこちらの記事を参考にしてください。

InteractionMessageActionの自作

さて、このようなInteractionMessageActionを自作するにはどうしたら良いのでしょう。
ScrollViewerを任意の場所にスクロールするInteractionMessageActionを例に取りながら説明していきます。

ScrollViewerにはスクロール位置を示すVerticalOffsetプロパティHorizontalOffsetプロパティはあるのですが、これらは読み取り専用でスクロール位置を変更するのには使えません。スクロール位置を変更するにはScrollToVerticalOffsetメソッドScrollToHorizontalOffsetメソッドを呼び出す必要があります。
メソッドの呼び出しはデータバインディングで行うことはできませんので、InteractionMessageActionに頼る必要があります。

まずはInteractionMessageを自作します。
public class ScrollInteractionMessage : InteractionMessage
{
    public ScrollInteractionMessage() : base()
    {
        Command = ScrollCommand.None;
        Offset = 0;
    }

    public ScrollInteractionMessage(string messageKey) : base(messageKey)
    {
        Command = ScrollCommand.None;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command) : base()
    {
        Command = command;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command, string messageKey) : base(messageKey)
    {
        Command = command;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command, double offset) : base()
    {
        Command = command;
        Offset = offset;
    }

    public ScrollInteractionMessage(ScrollCommand command, double offset, string messageKey) : base(messageKey)
    {
        Command = command;
        Offset = offset;
    }

    public ScrollCommand Command { get; set; }

    public double Offset { get; set; }

    protected override Freezable CreateInstanceCore()
    {
        return new ScrollInteractionMessage(Command, Offset, MessageKey);
    }
}
大部分がコンストラクタで見にくいですが、要するにCommand(どのようなスクロールをするか)とOffset(オフセットへの移動の場合の値)を持たせたInteractionMessageを作りました。
ここで使っているScrollCommandはこのような列挙型です。
public enum ScrollCommand
{
    None,
    Home,
    End,
    Top,
    Bottom,
    LeftEnd,
    RightEnd,
    VerticalOffset,
    HorizontalOffset,
}

さて、このInteractionMessageの実装で重要なのが、CreateInstanceCoreのオーバーライドです。これをやらずにドツボにはまったことが私はありました。
InteractionMessageはFreezableを継承しており、Messenger.Raise中でFreezeされます。そしてその後にコピーされてInteractionMessageActionに渡されるのですが、その際、CreateInstanceCoreを呼び出してコピーされます。
これをオーバーライドしておかないと、InteractionMessage.CreateInstanceCoreでコピー作業が行われます。それはすなわち、InteractionMessageActionに渡されるのがInteractionMessage型のインスタンスとなってしまい追加のパラメーターがすべて消えることを意味しています。
ハマりポイントですので、ぜひ回避するようにしましょう。

また、当然ですが、CreateInstanceCoreメソッドでは同じパラメーターのインスタンスを生成するためのものですので、継承時に追加したプロパティはしっかりコピーするようにしてください。今回ならばCommandとOffsetですね。

これを受けるScrollInteractionMessageActionはこんな感じに実装しました。
public class ScrollInteractionMessageAction : InteractionMessageAction<FrameworkElement>
{
    protected override void InvokeAction(InteractionMessage message)
    {
        if(AssociatedObject is ScrollViewer ctrl && message is ScrollInteractionMessage msg) {
            switch(msg.Command) {
                case ScrollCommand.Home:
                    ctrl.ScrollToHome();
                    break;
                case ScrollCommand.End:
                    ctrl.ScrollToEnd();
                    break;
                case ScrollCommand.Top:
                    ctrl.ScrollToTop();
                    break;
                case ScrollCommand.Bottom:
                    ctrl.ScrollToBottom();
                    break;
                case ScrollCommand.LeftEnd:
                    ctrl.ScrollToLeftEnd();
                    break;
                case ScrollCommand.RightEnd:
                    ctrl.ScrollToRightEnd();
                    break;
                case ScrollCommand.VerticalOffset:
                    ctrl.ScrollToVerticalOffset(msg.Offset);
                    break;
                case ScrollCommand.HorizontalOffset:
                    ctrl.ScrollToHorizontalOffset(msg.Offset);
                    break;
            }
        }
    }
}
C#7.0からは「変数 is 型 新しい変数」で「変数を新しい変数にキャストしつつ、キャストできたかどうかをboolで返す」という構文が導入されたので、型判定と変換を同時に行っています。便利なものです。
AssociatedObjectはこのActionがぶら下げられたコントロール、messageはMessenger.Raiseに渡されたメッセージです。受け取ったmessegeのCommandをもとに、呼び出すべきスクロールメソッドに制御を振り分けています。

さて、XAMLはこのようになります。
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
    <Image Source="{Binding FileName}" />
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll">
            <a:ScrollInteractionMessageAction />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</ScrollViewer>
InteractionMessageTrigger(メッセージで発生するトリガー)にScrollInteractionMessageActionをぶら下げています。

ViewModelからはこのようにMessenger.Raiseを呼び出すだけです。
ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll")));
ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll")));
ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll")));
ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll")));
コマンドのコールバックとしてMessenger.Raiseを呼び出しています。パラメーターにはコマンド名とメッセージキーだけです。これらのコマンドをボタンに結び付ければ完成です。
ボタンを押すだけで四隅へ移動できるソフトが出来上がりました。

参考までにViewとViewModelの全容を載せておきます。
<Window x:Class="MessengerTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:MessengerTest.Views"
        xmlns:a="clr-namespace:MessengerTest.Views.Actions"
        xmlns:vm="clr-namespace:MessengerTest.ViewModels"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <DockPanel>
            <Grid DockPanel.Dock="Top">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                </Grid.ColumnDefinitions>
                <Button Grid.Column="0" Content="Left" Command="{Binding ScrollToLeft}" Margin="2" />
                <Button Grid.Column="1" Content="Top" Command="{Binding ScrollToTop}" Margin="2" />
                <Button Grid.Column="2" Content="Bottom" Command="{Binding ScrollToBottom}" Margin="2" />
                <Button Grid.Column="3" Content="Right" Command="{Binding ScrollToRight}" Margin="2" />
            </Grid>
            <Grid DockPanel.Dock="Bottom">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="100" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="Path: " VerticalAlignment="Center" Margin="2" />
                <TextBox Grid.Column="1" Text="{Binding FileName}" VerticalAlignment="Center" Margin="2" />
                <Button Grid.Column="2" Content="Browse..." VerticalAlignment="Center" Margin="2">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <l:OpenFileDialogInteractionMessageAction>
                                <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="FileSelected">
                                    <l:OpeningFileSelectionMessage Filter="Image file (*.jpg;*.png;*.gif;*.bmp)|*.jpg;*.png;*.gif;*.bmp" MultiSelect="False" />
                                </l:DirectInteractionMessage>
                            </l:OpenFileDialogInteractionMessageAction>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
            </Grid>
            <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
                <Image Source="{Binding FileName}" />
                <i:Interaction.Triggers>
                    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll">
                        <a:ScrollInteractionMessageAction />
                    </l:InteractionMessageTrigger>
                </i:Interaction.Triggers>
            </ScrollViewer>
        </DockPanel>
    </Grid>
</Window>
public class MainWindowViewModel : ViewModel
{
    public MainWindowViewModel()
    {
        ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll")));
        ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll")));
        ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll")));
        ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll")));
    }

    public void Initialize()
    {
    }

    #region FileName変更通知プロパティ
    private string _FileName;

    public string FileName
    {
        get
        { return _FileName; }
        set
        {
            if(_FileName == value)
                return;
            _FileName = value;
            RaisePropertyChanged(nameof(FileName));
        }
    }
    #endregion

    public ViewModelCommand ScrollToLeft { get; }
    public ViewModelCommand ScrollToRight { get; }
    public ViewModelCommand ScrollToTop { get; }
    public ViewModelCommand ScrollToBottom { get; }

    public void FileSelected(OpeningFileSelectionMessage msg)
    {
        if(msg.Response != null && msg.Response.Length == 1) {
            FileName = msg.Response.Single();
        }
    }
}
試してみたい方はこれとScrollInteractionMessageAction/ScrollInteractionMessageの2つのクラスで概ね動作させられるはずです。

おまけ

Livetを使っていると気づくかもしれませんが、WindowActionMessageをMessenger.Raiseしたときにうまく動きません。DirectInteractionMessage経由(XAMLで全部記述)ならばうまくいくのですが。
ですので、ViewModel側からウィンドウ操作をしたい場合は、WindowActionごとにInteractionMessageTriggerを用意して、キーで呼び分ける必要がでてきます。

なぜこんなことになっているのかというと、ただのバグです。
WindowActionMessageのCreateInstanceCoreのオーバーライドにてWindowActionのコピーを忘れています。ですので、Messenger.RaiseでどんなWindowActionを設定してもここの時点で闇に葬られるわけですね。
DirectInteractionMessageだとCreateInstanceCoreは呼ばれない(元のインスタンスのまま渡る)ので、設定した値がそのまま生かされます。

ま、もうLivetは事実上開発が止まっていますので、自分で修正してビルドして使うなりなんなりしてやるしかないですね。はい。

2018年6月16日土曜日

ソートアルゴリズムの限界 - 比較ソートと非比較ソート

ソートアルゴリズムはアルゴリズムの初歩で勉強することだと思います。
最悪計算時間、平均計算時間や安定なソート、不安定なソートなんかの話が出てきますよね。簡単なアルゴリズムの例としてバブルソートや選択ソート、挿入ソートなどが出てきて、そのあとに高速なアルゴリズムとしてクイックソートやマージソートが出てくると思います。

さて、それではソートのスピードに限界はあるのでしょうか。

計算時間の"オーダー"

その前に復習です。よく「計算時間のオーダー」というものが言われますね。話を厳密にするために定義しておきましょう。

ビッグオー記法

定義:ある関数$f(n)$に対して定数$a$,$c$が存在し、すべての$n>a$に対して$f(n) \leq c \cdot g(n)$を満たすとき、$f(n)=O(g(n))$と書く。
これがいわゆるオーダーと呼ばれるものです。どんな$a$を取ってきても、それより大きな$n$だったら必ず$f(n)$は$g(n)$の定数倍より小さくなる、ということを言っています。$n$が無限まで行っても必ず$g(n)$の定数倍にはかなわないため、最も大きくなるパターンを想定するうえで非常に有用です。

ビッグオメガ記法

定義:ある関数$f(n)$に対して定数$a$,$c$が存在し、すべての$n>a$に対して$f(n) \geq c \cdot g(n)$を満たすとき、$f(n)= \Omega (g(n))$と書く。
オーダーよりもちょっとマイナーな記法だと思います。ビッグオー記法との違いは不等号です。どんな$a$を取ってきても、それより大きな$n$だったら必ず$f(n)$は$g(n)$の定数倍より大きくなる、ということを言っています。こちらは逆に最も小さくなるパターンを想定する上で有用です。


これらの定義は一般化して数学的に厳密に行ったものです。アルゴリズムの良さの尺度として用いるときは、$n$はたいてい配列長とします。配列長が長くなるとソートにかかる時間$f(n)$がどうなるのか、というのが最も評価すべき指標だからです。
細かな話をすればそれ以外の評価指標もあるのですが、まずはここから入ります。

注意事項

1. 定数倍までは気にしない

これらの指標は定数倍の違いは許容しています。これは、あくまでも$n$に対する相関を考えているためです。ですので、同じオーダーでも計算時間が平気で2倍3倍、極端な話10倍100倍違うこともあり得ます。
そんなの困るじゃないかと思うかもしれませんが、逆に言えば、オーダーが同じアルゴリズムでは$n$がどんなに増えても高々定数倍しか計算時間は変わらないということになります。オーダーが変わると定数倍よりも大きな計算時間の差が出るため、それに比べれば高々定数倍しか差が出ないのは、アルゴリズムの良し悪しを検討する上では些細な問題なのです。逆に言えば、その些細な問題を気にするようなステージでは、この記事の議論は的外れであるという認識を持っていただいて構いません。

2. タイトな表現をすること

上記の定義に従えば、例えば$O(n^2)$のところを$O(n^{100})$と書いても間違いではありません。ですが、評価指標としては役に立たないものになってしまいます。ですので、特別な要求が無い限りは最もタイトな表現をすることとします。
タイトな表現とは、あるアルゴリズムについて$O(f(n))$と$O(g(n))$という2通りの書き方があったとき、$f(n)=O(g(n))$かつ$g(n) \neq O(f(n))$であるとき、$f(n)$のほうがよりタイトな表現と定義することができます。

比較ソートの理論限界

さて、種々の定義が終わったところで本題に移っていきます。
クイックソートとマージソートは高速で、平均計算時間は$O(n \log n)$だという認識を持っている人は多いと思います。
ではソートはこれ以上速くなることができるのでしょうか。

実は、大小比較に基づいたソートには理論限界があります。

比較とは、配列の中の2つの要素$a$,$b$を取り出して、$a \leq b$か$a>b$かを判定する作業のことを言います。
比較を1回行うと、「以上」か「未満」の2通りの答えが出てくるため、内容によって配列の並べ方が2通りに分岐します。そこからもう1回比較をするとさらに2通りに分岐するため、$t$回の比較では$2^t$種類の並べ方に分類することができます。
一方、ソートとはいくつもある並び替え方の中から唯一の並び方を決定する作業だと言いかえることができます。$n$個の要素の並び替え方は$n!$通りありますので、以下の式が成り立ちます。\[
2^t \geq n!
\]いくつもある並べ方の中から1つの並べ方を特定するには、すべての並べ方をカバーできるだけの比較はしておかなければなりません。逆に言えば(と言うよりか対偶に言えば)、すべての並べ方をカバーできない回数の比較では、1つの並べ方を特定することができません。なのでこの式が成り立つわけです。

ここからの式変形はスターリングの公式を使うとした文献が多いですが、実はそんなに大した計算ではないので自力で解いてみましょう。\[\begin{align*}
2^t &\geq n!\\
t &\geq \log n!\\
&\geq \sum_{k=1}^{n} \log k > \int_1^{n} \log x \mathrm{d}x
\end{align*}\]最後の不等式は面積を考えれば一発ですね。

\[\begin{align*}
\int \log x \mathrm{d}x &= x \log x - \log e \int x \cdot \frac{1}{x}\mathrm{d}x\\
&= x \log x - x \log e+C
\end{align*}\]\[
\therefore t>n \log n+(1- n) \log e =O(n \log n)
\]logの積分とか懐かしいですね。ただ、logは底が2なので積分時には注意する必要があります。
これによって、比較に基づいたソートである限り、最悪計算時間は$\Omega (n \log n)$であることがわかりました。クイックソートやマージソートは、オーダーの上では理論上最速なのです。

比較に基づかないソート

さて、前節でわざわざ比較に基づいたソートと書いたからには、比較に基づかないソートも存在するわけです。そうすれば、$O(n \log n)$を割り込むかもしれません。

バケットソート

配列は任意の要素を$O(1)$の計算時間で読み書きできます。ですので、並び替えたい数列の要素が0以上の整数で上限がわかっている場合、0~上限までの長さの配列を用意することで数列に含まれるすべての整数の個数を$O(n)$で数えきることができます。すべての個数がわかれば、小さいほうから順にその整数をその個数入れていけばソートの完成です。
const int max = 10;
Random rnd = new Random();
int[] array = new int[100];

//乱数を準備(値は0~9)
for(int i = 0; i < array.Length; i++)
    array[i] = rnd.Next(10);

Console.WriteLine("Before:");
Console.WriteLine(string.Join(", ", array));

//バケットソート
int[] result = new int[max];
for(int i = 0; i < array.Length; i++)
    result[array[i]]++; //個数をカウントする

int pos = 0;
for(int i = 0; i < max; i++) {
    for(int j = 0; j < result[i]; j++)
        array[pos++] = i;    //個数分だけその数を入れる
}

Console.WriteLine("After:");
Console.WriteLine(string.Join(", ", array));
途中で2段ループを回しているように見えますが、これは2段分合計でarray長しか繰り返さないので、$O(n)$になります。$O(n)$でソートが終わるとても高速なソートになります。

ただ、これの欠点は0以上の整数にしか使えないことで、さらに0~上限までのメモリが必要になります。上のサンプルコードでは上限を9としたので大したことありませんが、例えば32bit整数だったら4GiBのメモリが必要になってきます。実用上かなり不便です。

鳩ノ巣ソート

上記のバケットソートは数字の数を数えているだけですので、数字以外のソートには使えません。数字を付帯する何かしらのデータ(例えばファイルサイズを持っているファイル情報データ)をソートしたい場合に一工夫必要です。
そこで、ソートしたい整数に対応した配列の各要素として今度はこのデータを収める配列をそれぞれ確保すればバケットソートを適用できます。これを鳩ノ巣ソートと呼びます。

この時、必要なメモリは、並び替えたい整数の種類$k$と配列長$n$を使って$kn$と表せます。例えば32bit数値のデータ1024個を並び替えたければ4TiBのメモリが必要になります。もはやアホくさいレベルです。
(並び替えるデータを片方向連結リストとすることでバケットソートレベルまで使用するメモリを減らせます。が、まだまだ非現実なレベルです。)

基数ソート

バケットソートや鳩ノ巣ソートではとてつもない大きさのメモリが必要になるので、その作業を何回かに分けてメモリの使用量を抑えたのが基数ソートになります。
まず1の位だけを見て鳩ノ巣ソートを行います。この時、10進数ならば取り得る数字は10種類だけなのでメモリ量も配列長の10倍ほどで済みます。
次は、並び替え終わったデータに対して今度は10の位で鳩ノ巣ソートを行います。安定なソート前提だと、10の位がソートされ、同じ10の位については1の位がソートされた状態になります。すなわち下2桁がソートし終わったことになります。
次は100の位、1000の位と同様の処理をしていきます。最大桁数まで処理を終えたら終了です。

コンピューターは通常8bit単位でデータを扱いますので、256進数とみなして基数ソートを行えば、例えば32bit値ならば4回のループで計算が終わります。鳩ノ巣ソートと同様、片方向連結リストを使うことでメモリの使用量もそこそこ抑えることができます。

スリープソート

いつもネタ枠扱いされていますが、これも立派な非比較ソートです。

並び替えたい配列の各要素に対して、例えばその数×1ミリ秒待つタイマーを一斉にスタートさせ、タイマーが鳴ったものから順に結果を格納していけばソートが完了するというものです。
バケットソート、鳩ノ巣ソート、基数ソートは整数しかソートできませんでしたが、時間は配列と違って連続ですので、スリープソートは小数に対しても行うことができます。すごい!!

ただ、これを現代の一般的なコンピューターにやらせようとすると、実際は中には1つのストップウォッチのみが入っていて、タイマー時間をもとに数字を並び替えて、小さいほうから順に鳴らしていくということになってしまいます。スリープソートをしたつもりで裏でOSが別の手法でこっそりソートをしているんですね。
また、「一斉にタイマーをスタートさせる」というのも一般的なコンピューターでは現実的に難しく、 スタートのタイミングの誤差だったり、もしくはスレッド切替時の誤差で正確なソートができなくなってしまうこともあります。

そういった点からいつもネタ扱いされていますが、私個人としては、バケットソートをメモリ空間軸上から時間軸上に動かして考案された、なかなか面白いアイディアだと思っています。
現実を見ていないソートアルゴリズムなのではない、現実が追い付いていないだけなのだ。

計算時間の比較

実際にマージソートと基数ソートを実装し、計算時間を比較してみました。
お約束ですが、実装や環境で速度そのものについては変わってくると思います。ただ、オーダーについての考え方は同じはずですので、その着眼点で見てください。
まずは単純な比較です。
マージソートの$O(n \log n)$の見た目が$O(n)$とほとんど変わらないのでパッと見ではわかりませんが、近似直線を引くと確かに直線より上側に沿っている(2階微分が正)のがわかります。
一方で基数ソートのほうは近似直線に沿っています。確かに$O(n)$です。

おまけですが、これの原点付近を拡大するとこうなります。

配列長が200前後でマージソートと基数ソートの計算時間が逆転しています。

これがオーダーの威力です。配列長が小さいと実装の差が出てきますが、配列長が大きくなるとオーダーが圧倒的な支配力を持ってきます。ですので、まずはオーダーの小さなソート手法を確立するのが先決なのです。

2018年6月10日日曜日

仮想ネットワークドライバ「TAP-Windows」をC#から操作する - Tap.NET 0.1.0

ネットワークの勉強をしていると、イーサネットパケットレベルで他のコンピューターとやり取りをしてみたくなることがあります。パソコンを2台起動して、リモートデスクトップを使いながらいろいろ実験するのも悪くないですが、1台ですべて完結できたら便利ですよね。Wiresharkでキャプチャしたパケットを自身のPCに送り直すくらいのことなら、仮想ネットワークドライバをインストールして、ソフトウェア的にエミュレートすることもできるはずです。

ググっていると、OpenVPNに付属している仮想ネットワークドライバ「TAP-Windows」というものを見つけました。本来はVPNで使用するための仮想ネットワークドライバのようですが、Win32APIを介してパケットのやり取りができるようです。

というわけで、この.NETラッパーを作ってC#等から使えるようにしてみました。

1. TAP-Windowsのインストール

まずはTAP-Windowsをインストールしなければ始まりません。
Download - OpenVPN.net
この中からOpenVPNのWindows用インストーラをダウンロードしインストールをします。OpenVPN本体自体は不要ですので、インストールにて他のチェックボックスは極力外しておきましょう。

インストールが終わったらネットワークと共有センターを開いてちゃんとインストールできているか確認しておきましょう。名前をわかりやすいものに変更しておくといいかもしれません。

余談ですが、Wiresharkがパケットキャプチャで使っているWinPcapサービスは起動時に存在するネットワークアダプタをスキャンしているようです。なので、WinPcapからTAP-Windowsを見るためには一旦パソコンを再起動する必要があります。

2. ラッパーを作る

TAP-WindowsにアクセスするにはWin32APIのCreateFileReadFileWriteFile関数などを呼び出します。概ねファイルの読み書きと同様にできると言っていいでしょう。
C#からP/Invokeで頑張るのもありかもしれませんが、だんだんわけがわからなくなってきたり、結局定数の中身を除くために延々とwindows.hを辿ったり、unsafeを強要されそうになってしまったり…というお約束のルートが見えています。ですので、プログラミング界の黒魔術(※)と呼ばれているC++/CLIでラッパーを作ります。あまり深入りはし過ぎないように、ほどほどの作りこみにしておきましょう。
※私が勝手に呼んでいるだけです。
 

コンストラクタ

/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="guid">TAPデバイスのGUID</param>
TapDotNet::Tap::Tap(String ^ guid)
{
    String^ path = "\\\\.\\Global\\" + guid + ".tap";
    pin_ptr<const wchar_t> pszPath = PtrToStringChars(path);

    ULONG status = TRUE;
    DWORD dwLen;


    //TAPデバイスを開く
    hTap = CreateFileW(pszPath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);

    if(hTap == INVALID_HANDLE_VALUE)
        throw gcnew InvalidOperationException("Cannot open the device");

    //TAPデバイスをアクティブに
    status = TRUE;
    if(!DeviceIoControl(hTap, CTL_CODE(FILE_DEVICE_UNKNOWN, 6, METHOD_BUFFERED, FILE_ANY_ACCESS), &status, sizeof(status), &status, sizeof(status), &dwLen, NULL)) {
        CloseHandle(hTap);
        hTap = NULL;
        throw gcnew InvalidOperationException("Cannot activate device");
    }
}
まずはデバイスの初期化をします。
CreateFileのファイル名を指定するパラメーターには"\\.\Global\{GUID}.tap"を渡します。GUIDの取得方法はまた後程。その他のパラメーターはなんとなくわかると思います。
デバイスを開いたら次はDeviceIoControl関数を呼び出してデバイスをアクティブにします。この時点でWindowsはネットワークケーブルが差し込まれたと認識します。

パケット送信

/// <summary>
/// データを送信するメソッド
/// </summary>
/// <param name="data">パケット</param>
void TapDotNet::Tap::SendData(array<Byte>^ data)
{
    if(data == nullptr) throw gcnew ArgumentNullException("data");
    if(hTap == NULL) throw gcnew ObjectDisposedException("Tap");

    pin_ptr<Byte> pinned = &data[0];
    DWORD dwWritten;

    if(!WriteFile(hTap, pinned, data->Length * sizeof(Byte), &dwWritten, NULL))
        throw gcnew InvalidOperationException("WriteFile Error: " + GetLastError().ToString());
}
データの送信にはWriteFile関数を使います。書き込んだバイトを取得するパラメーターは必要無くてもNULLにしてはいけないようです。Windows10では動きましたがWindows7ではAccessViolationExceptionを吐いて動かず、しばらくハマってしまいました(ドキュメンテーションにもOverlapped構造体とともにNULLにはできないと書いてありました…)。

パケット受信

void TapDotNet::Tap::StartWatching()
{
    if(!ReceiveWatching) {
        WatcherThread = gcnew Thread(gcnew ThreadStart(this, &TapDotNet::Tap::WatchReceive));
        WatcherThread->Start();
    }
}

void TapDotNet::Tap::StopWatching()
{
    if(ReceiveWatching) {
        WatcherThread->Abort();
        WatcherThread->Join();
        WatcherThread = nullptr;
    }
}

void TapDotNet::Tap::WatchReceive()
{
    try {
        UCHAR Buf[1518] = { 0 };
        DWORD dwLen = 0;

        while(true) {
            if(ReadFile(hTap, Buf, sizeof(Buf), &dwLen, NULL))
                if(Filter(Buf, dwLen))
                    RaiseReceiveEvent(Buf, dwLen);
            else {
                DWORD lasterr = GetLastError();
                if(lasterr != 0) {
                    RaiseErrorEvent("ReadFile Error", lasterr);
                    break;
                }
            }            
        }
    }
    catch(ThreadAbortException^) {
    }
}
データの受信は受信用スレッドを作って行います。
えっ、今の時代Thread直作りは流行らないって?C++/CLI自体が流行らないので許してください…。

Filter関数は受信したパケットにフィルターを掛けるものです。全てのパケットをイベントでRaiseしていてはイベントの嵐になってしまいますので、そういうのはできるだけ根っこで止めるということで、受信直後にフィルターを掛ける機能を実装しました。
ただ、ここで受信するのはイーサネットフレームで、そのヘッダで得られる情報は発信元と宛先のMACアドレスと、IPv4などのネットワーク層水準のプロトコルの種類くらいです。
トランスポート層のプロトコル種類(TCPとかUDPとか)やIPアドレスなどを判別する機能も付けたいところではありますが、じゃあ同じようにAppleTalkのフィルターも作るのかと言ったらそんなの作る気にもなれず、ひとまずそのレベルのフィルターでとどめておくことにしました。
C++/CLIですからね。深入りは厳禁です。

デストラクタ/ファイナライザ

TapDotNet::Tap::~Tap()
{
    StopWatching();
    this->!Tap();
}

TapDotNet::Tap::!Tap()
{
    if(hTap != NULL) {
        CloseHandle(hTap);
        hTap = NULL;
    }
}
最後は終了処理をします。C++/CLIではDisposeパターンが自動的に実装されるらしく、C#でのディスポーザはC++/CLIではデストラクタとして実装するそうです。
マネージドリソース(今回はThread)はデストラクタ、アンマネージドリソース(CreateFileのハンドル)はファイナライザにて解放処理を行います。

3. 実際に使う

やっとC++/CLIの呪縛から逃れました。
前述のTAP-WindowsのGUIDを求める方法は超簡単です。NetworkInterfaceクラスIdプロパティがそれに相当します。ですのでこのようなプログラムでどうでしょうか。
var tapnic = NetworkInterface.GetAllNetworkInterfaces().SingleOrDefault(p => p.Description == "TAP-Windows Adapter V9");

using(var tap = new Tap(tapnic.Id)) {
    tap.DataReceived += Tap_DataReceived;
    tap.ReceiveWatching = true;
    Task.Delay(TimeSpan.FromSeconds(10)).Wait();
    tap.ReceiveWatching = false;
    tap.DataReceived -= Tap_DataReceived;
}
バージョン違い等でDescriptionが書き換わったらこれではうまく動かなくなってしまいますが…となるとユーザーに選択させるとかですかね。まあ、サンプルコードとしては十分でしょう。
また、これはコンソールアプリケーションで作ったので、Task.Delay().Wait();ができますが、GUIアプリケーションに移植する際は気を付けてください。あくまでもTaskはasync/awaitすべきです。
private static void Tap_DataReceived(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine($"======== {e.Data.Length} bytes ========");

    const int linecnt = 16;
    const int separatedcnt = 8;

    foreach(var (list, index) in e.Data.Buffer(linecnt).Select((list, index) => (list, index))) {
        var line = new StringBuilder();

        line.Append($"{index * linecnt:X4}  ");
        line.Append(string.Join("  ", list
            .Select(p => p.ToString("X2"))
            .Concat(Enumerable.Repeat("  ", linecnt - list.Count))
            .Buffer(separatedcnt)
            .Select(p => string.Join(" ", p))));
        line.Append("  ");
        line.Append(string.Join(" ", list
            .Select(p => (char)p)
            .Select(p=>char.IsControl(p) ? '.' : p)
            .Concat(Enumerable.Repeat(' ', linecnt - list.Count))
            .Buffer(separatedcnt)
            .Select(p => new string(p.ToArray()))));

        Console.WriteLine(line);
    }

    Console.WriteLine();
}
データを受信したときにカッコよくダンプするコードを書きました。
ValueTupleとIxのBuffer()を使っているので、相応のC#バージョンと参照が必要です。

送信は超簡単です。
using(var tap = new Tap(tapnic.Id)) {
    tap.SendData(ping);
}
pingはpingのイーサネットフレームのバイト列です。Wiresharkの下のほうに見えるアレです。
Tap.NETから送ったpingパケットをWiresharkで無事拾えました。

4. ダウンロード

例によってNugetに置いておきました。現時点ではひとまずプレリリース扱いにしております。
Tap.NET 0.1.0-beta1

2018年6月2日土曜日

OrderByとThenBy - IOrderedEnumerableの仕組み

LINQには要素を並び替えるOrderByと、OrderByで同じ大きさだった要素をその中でさらに並び替えるThenByという拡張メソッドが用意されています。
var files = Directory.GetFiles(@"(中略)\OrderByTest")
    .Select(p => new FileInfo(p))
    .OrderBy(p => p.Extension.ToLower())
    .ThenByDescending(p => p.Name);

foreach(var file in files)
    Console.WriteLine(file.Name);    
例えばソースコードのフォルダに対してこんなコードを実行してみました。
まずは拡張子に対して昇順でソートをかけ、拡張子が同じものについては降順でファイル名にソートをかけています。
 確かに拡張子は昇順(config→cs→csproj)ですが、ファイル名は降順(Program→Enumerable)になっています。

ちょっと考えれば、ThenByが無くてもファイル名で降順にソートしてから拡張子で昇順にソートすれば(安定なソートである前提で)同じ結果が得られることがわかります。まあ、2回ソートをすることになりますが。

ですが、OrderBy().ThenBy()という書き方は「まずソートして、同じだったらさらに条件を追加して」となり流れが非常にわかりやすいです。また、詳しくは後述しますが、このやり方ではソートは1回しか行われておらず、上記の2回ソートする方法に比べれば効率が良いです。

さて、どうなっているか仕組みを見てみましょう。

IOrderedEnumerable<T>

当然ですが、ThenBy()はソートされたシーケンスにしか適用できません。すなわち、IEnumerable<T>に対する拡張メソッドではないのです。OrderBy()やThenBy()はIOrderedEnumerable<T>を返し、これに対する拡張メソッドとしてThenBy()が定義されています。

IOrderedEnumerable<T>はIEnumerable<T>とIEnumerableを継承しており、
IOrderedEnumerable<T>としてはCreateOrderedEnumerable()メソッドが追加されています。
IOrderedEnumerable<TElement> CreateOrderedEnumerable<TKey>(
    Func<TElement, TKey> keySelector,
    IComparer<TKey> comparer,
    bool descending
)
最初このメソッドを見たときは「なんやねんこれ」と思いました。何をやるメソッドかまるでわからない、悪いネーミングのメソッドです。

実はこれ、単にThenBy()が自分が渡されたパラメーターを渡しているだけなのです。
ソースコードを見れば一目瞭然です。
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
    if (source == null) throw Error.ArgumentNull("source");
    return source.CreateOrderedEnumerable<TKey>(keySelector, null, false);
}
すなわちこれがThenByの実体なわけですね。第1引数はキーを返すデリゲート、第2引数はキーの比較クラス、3つ目の引数は昇順降順を指定するbooleanです。
ThenBy()メソッドは第1引数にOrderByなどが返したIOrderedEnumerableを受け取りますから、この実体に対してCreateOrderedEnumerableを呼び出してThenByの動作を規定するわけです。

IOrderedEnumerableの実体は、LINQの場合はOrderedEnumerableクラスが担っています。このクラスはinternalなのでライブラリ外からは見えません。
上記のソースコードを追っていくとわかりますが、OrderedEnumerable.CreateOrderedEnumerableは自身を親条件に持つOrderedEnumerableを返します。そして、実際に列挙されはじめたとき(GetEnumeratorが呼び出されたとき)にクイックソートを用いてソートを行いますが、そのソート時の比較で親条件から順に適用していき、比較結果が"等価"になったときに再帰的に子条件を用いることでThenByを実現しています。
internal override int CompareKeys(int index1, int index2) {
    int c = comparer.Compare(keys[index1], keys[index2]);
    if (c == 0) {
        if (next == null) return index1 - index2;
        return next.CompareKeys(index1, index2);
    }
    return descending ? -c : c;
}
(ソースコードはこちら


余談ですが、クイックソートは比較ソートの中では最も速い(ソート時間の期待値が最も小さい)と言われていますが、安定なソートではありません。そこで、この比較メソッドで、値が等価ならインデックスの大小関係を返すことで安定なソートを実現しているようです。


まとめ

  • OrderBy()やThenBy()はIEnumerable<T>を継承したIOrderedEnumerable<T>を返す
  • IOrderedEnumerable<T>.CreateOrderedEnumerable()はThenByの条件を追加したIOrderedEnumerable<T>を返す
  • IOrderedEnumerable<T>のLINQ実装であるOrderedEnumerableクラスはクイックソートを行い、その比較時に最も親の条件から順に適用してThenByを実現している (すなわちソートは1回のみ)
ということで、もしも俺の考えた最強のソートアルゴリズムをLINQで使いたかったら、IOrderedEnumerable<T>を実装した(CreateOrderedEnumerable()を実装した)クラスを返すことで、自動的にThenByも定義できてしまうということになります。

まあ、比較ソートの中では最速のクイックソートをがOrderByに使われている以上、OrderByを改善したくなることはまず無いとは思いますけどね。意図的に最悪なピボットを選ばせるようなシーケンスを与えない限り、マージソートのほうが良いなんていうことは無いでしょう(ちなみにシーケンスの中央の値をピボットとするアルゴリズムを採用しているようなので、ソート済みシーケンスに対してOrderByを呼び出しても$O(n^2)$にはならないようです。

2018年5月26日土曜日

オブジェクト指向とは何か

オブジェクト指向の「オブジェクト」とは「object型」のことではない
私のブログを読んでくださっている方なら、オブジェクト指向言語には慣れ親しんでいるかと思います。でも、「オブジェクト指向」をちゃんと説明できますか?

「オブジェクト指向」の疑問

オブジェクト指向の初学者は、まず最初にオブジェクト指向の3要素を学ぶと思います。
  • カプセル化
  • 継承
  • 多相性(ポリモーフィズム)
言い方に多少の差はあれど、だいたいこんな感じかと思います。
復習しておくと、カプセル化は内部の動作手順を隠蔽し、許可された手法のみを使って操作をできるようにすること、継承はクラス内容を引き継ぎつつ追加の機能を付けたすこと、多相性はメソッドを呼んだ時のふるまい等を実行時の型に合わせて決定することですね。
C++やJava、C#などの代表的なオブジェクト指向言語では、このような要素に対して「クラス」「継承」「メソッドのオーバーライド」などといった具体的な機能が備わっており、これらの要素と言語機能の習得を並行して行うと理解がよく深まるかと思います。

ですが、オブジェクト指向に関する勉強がこれで終わっていないでしょうか。
これはオブジェクト指向の特徴を挙げただけであり、そもそもなぜ「オブジェクト指向」という名前かということには何も触れていません。

そして、そのままオブジェクト指向言語を習得していくと、
  • ありとあらゆるクラス・型の最上位にはobject型がある(すべてのクラス・型はobject型を継承している)
  • ありとあらゆるメソッドは何かしらのクラス(=object型を継承したもの)に属さなければならず、オブジェクトへの操作として定義される
みたいな言語機能を見かけて、「ああ、ありとあらゆる操作がobject型に対する操作だからオブジェクト指向言語なんだな」みたいな納得感を持ってしまっている人も少なからずいるでしょう。私もそうでした。

そもそも、上の箇条書きで出てきた「オブジェクト」って何ですか?クラスのこと?それともインスタンスのことでしょうか。なんとなく意味も分からず、クラスやインスタンスとの違いも明確にしないまま適当に横文字をしゃべっているように見えません?かといって、objectを辞書で引くと「モノ」という訳が出てくるだけで、そんな抽象的な名詞では何も理解が進むことはありません。

だいたい、「オブジェクト言語」ではなく「オブジェクト指向言語」と呼ばれるのもなぜなのでしょうか。上の箇条書き程度の理解では、全然「指向」の意味がわかりませんよね。「指向」すなわち「oriented」を辞書で引くと「~の方向を向いている」とか「~を中心に考える」と言った訳が出てきますが、「オブジェクト」が何かよくわからないのに、そっちを向いているとかそれが中心だとか言われてもわけわかりません。

モジュール化

さて、「オブジェクト指向」とは何かについて紐解いていく前に、ここで一旦「モジュール化」の話をしましょう。

コンピューターが生まれたばかりの頃はごく小規模なプログラムが動いていましたが、コンピューターが発達するにつれて非常に大規模で複雑なプログラムが必要になってきました。
ですが、各プログラムの部分が複雑に絡み合っていては全貌が見えにくくなり、また、複数人での分担してプログラムを書くこともしにくくなります。当然、管理も難しくなって、機能の変更や追加があった時や不具合があった時に太刀打ちできなくなってしまいます。
なので、プログラムを何らかの基準で分割し、整理する必要が出てくるのです。こういうのを一般にモジュール化と言います。

ではどこで分割するのが良いでしょうか。
デイビッド・パーナスは、自身の論文で次のように述べています。
We have tried to demonstrate by these examples that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others. (Parnas D.L. (December 1972).)

(拙訳)私たちは、フローチャートに基づいてシステムのモジュール化をするのはおおむね不適切だということをこれらの例を用いて示した。その代わりに、難しい設計上の決定や、変更されやすい設計上の決定に基づいて行うことを提案する。これらのモジュールは、それらの決定を他から隠すような設計となる。
フローチャートに基づいてモジュール化するというのは、大きなプログラムの流れをフローチャートで書いてから、その個々の中身をモジュールとして実装する、というような意味のようです。長~い関数を書いてしまう人がその次のステップとしてやってしまいそうなことですね。
モジュール化をするときはそういうやり方をするのではなく、難しいことや変更されやすいことの単位で行うことのほうが効果的だそうです。

では、難しいことって何でしょう。 変わりやすいことって何でしょう。

難しいことや変わりやすいことというのは、往々にして何かの「内部表現」であることが多いです。例えば、内部的にはUTF8を使うのか、Shift-JISを使って処理をするのかとか、リストは配列を使うのか、連結リストを使うのか、などといったことが挙げられます。
なぜならば、やることと言うのはプログラムそのものの存在意味なのでそうそう変わるものではないからです。その実現手段や、内部でのデータ表現の方法はいくらでも変えることができますし、状況が変われば変わり得るでしょう。

そうすると、プログラムを「内」と「外」で分けたくなってくるわけです。内部表現は隠蔽し、外部とのやり取りは定められたインターフェースに則って行う、という形が、モジュール化をする上で大変うまく行く方法だということです。

"モノ視点" で分割する

じゃあどうやって内と外を分けるかと言ったら、1つの方法として、「モノ視点」というのが挙げられるでしょう。

例えば自動車。自動車は運転手から見れば「アクセルペダル」「ブレーキペダル」「シフトレバー」「ハンドル」などの決められたインターフェースがあり、それを外(自動車の駆動機構やエンジン等が入っている場所の外という意味)から操作することで、運転手の意図したとおりに加速、減速、曲がるなどといった動作を出力してくれます。ここで自動車の内部の状態(スロットルがどのくらい開いている、燃料の流量はどれくらい、エンジンの回転数は、温度は、シャフトの回転数は…などなど)は運転手がいちいち気を使う必要が無いですよね。

もうわかったでしょうか。

「オブジェクト指向プログラミング」というのは、「モノを中心に内と外を分けるプログラミング」ということなのです。「オブジェクト」が指しているのはインスタンスでもクラスでもなく、「モノ」なのです。
「モノ」と対になる言葉 として「コト」があります。上記の「フローチャートに基づいて分割する方法」は大まかな処理の流れ(コト)に着目して分割するということですが、オブジェクト指向言語はモノの単位で内外を分割してモジュール化をしようという発想が原点にある言語、という意味なのです。

なので、「オブジェクト指向言語ってどんな言語?」と聞かれたら、「モノ視点で内外を分けてモジュール化する言語」と答えれば良いでしょう。

モノ視点でオブジェクト指向3要素を見る

さて、冒頭で紹介したオブジェクト指向の3要素ですが、決してあれを軽視しているわけではありません。当然、「モノ視点で内外を分けてモジュール化する」という原点からいろいろと考えを深めていくと、それらの3要素に行きつくわけです。

カプセル化

これは「内外に分ける」ということですね。オブジェクト指向ならモノ視点です。
外からインターフェースを介してモノの内部状態を変化させるのがカプセル化です。

継承

これは「モノの分類」に当たります。そもそも「class」という言葉が「分類」ですしね。
例えば、自動車は普通車や大型車などに分類でき、大型車はバスやトラックなどに分類でき、例えばバスならば「自動車の基本機能」+「大型車の追加機能」+「バスの追加機能」みたいな関係性があることが言えます。
モノを階層構造で分類し、その差分を表現するのが継承になります。

多相性(ポリモーフィズム)

これは「分類が同じでも実体は別々になり得る」という意味です。
例えば同じ「ヒト」であっても一人ひとり個性があって違いますよね。その場合、同じ働きかけ(外からの操作)をしてもふるまい方(結果の出力)は異なるでしょう。
分類としてのふるまいではなく、モノそれぞれでふるまうことができるというのが多相性ということになります。

オブジェクト指向以外のモノの見方

さて、ここまで説明してきたオブジェクト指向ですが、欠点が無いわけではありません。

例えばオブジェクト指向を扱ったことがある人ならば、「分類の視点」について悩みを持ったことがあるでしょう。
自動車を大型車、普通車という車種で分類することもできるかもしれませんが、例えばトヨタ車、ホンダ車、日産車…という分類もできるはずです。一つの見方で使うだけならばそれでいいのですが、場合によって見方を変えなければいけないような場合では困ります。
が、多重継承にも様々な問題があり、多重継承構造を認めていないオブジェクト指向言語も少なくありません。

そこで、サブジェクト指向言語というものが開発されました。
「見方によって階層構造が変わってくるから、階層を別に定義できるようにしよう」という発想で、「主題」=「subject」で分割しようという言語です。
Hyper/Jなどという言語があるそうです。
ただ、サブジェクト指向はいろいろな問題があったようです。

そういった背景もあってか、オブジェクト指向のように1つの階層構造をとりあえず作って、様々な階層に横断的に適用させる記述をできるような言語も開発されました。それをアスペクト指向言語と言います。

例えばログの出力コードのような全く階層構造が違うようなクラスに横断的にかかわるようなコードはいろいろな場所に散在しがちです。
こういった関心ごとを分離してまとめるのがアスペクト指向なわけです。

ま、正直この辺りは私はあまり詳しくないのですが。





さて、一通りの説明が終わりましたがいかがでしたでしょうか。

実際にプログラムを書くと、「変わりやすいところを隠蔽」「モノ単位で分割」とか言葉で言うのは簡単でも実際にやるのはなかなか難しいことに気がつくでしょう。ただ、このブログで基本的なオブジェクト指向の考え方はわかっているはずですから、迷ったら原点に立ち返っていろいろと考えていきましょう。

ではでは。

2018年5月23日水曜日

ESP8266にDTRとRTSで自動書き込みをする

ESP8266を搭載したESP-WROOM-02が日本の組み込み市場に出てきてから約3年になりますが、あまりこれに関する記事を見なかったので(少しはあるようです)。
やっぱり組み込み系の記事を見ていると圧倒的にソフト屋さんのブログが多く、ハード屋さんが全然いないんだなって。

ESP8266のリセット回路

ESP8266のリセット回路はこのようになっています。IO0ピンをLにした状態で立ち上げる(リセットする)とブートローダーが起動し、UARTからプログラムが書きこめるようになります。すなわち、IO0と~RESETのスイッチを両方押して、~RESETを離してからIO0を離して書き込みボタンを押すという操作が必要になります。
ですが、書き込みをするたびにスイッチ2つを操作しないといけないのはいささか不便です。可能ならばArduino IDE上で書き込みボタンを押したらそのまま勝手に書き込まれてほしいものです。

そんな誰もが考えることは、ちゃんとSDKの提供者たちも考えてくれていて、その解決策が提供されています。
ズバリ、DTR端子とRTS端子を使います。

シリアル通信のフロー制御

DTR端子とRTS端子を使うと言いましたが、TXDやRXDとは違ってあまり聞きなれない端子です。これはいったい何なのでしょう。

UARTなどのシリアル通信では、送信側が送信したいタイミングでデータの送信をできてしまいます。ですので、受信側が受信する準備ができていない場合、勝手にデータを送信されても取りこぼしてしまいます。そのようなことがあっては不都合な場合、相手方に「受け入れ可能だよ」と言うことを伝える制御を行います。その制御のことをフロー制御と言います。
信号線名 名称 説明
TXD Transmit Data 送信データ
RXD Receive Data 受信データ
TXDを受ける。
DTR Data Terminal Rady 端末準備完了
機器の電源が入りポートが開かれると1になる。
DSR Data Set Ready データセットレディ
DTRを受ける。
RTS Request To Send 送信要求
自分の受信バッファーに十分な空きがあり、データの受け入れが可能にになると1となる。
CTS Clear To Send 送信可能
RTSを受ける。
SG Signal Ground 信号線の接地ライン

フロー制御ありのシリアル通信は、通常このように接続します。DTRで相手にこちらの端末が生きていることを伝え、RTSでデータ受け入れ可能であることを伝えます。
このことから、RTSが1になるときは必ずDTRも1になっているということがわかります。こちらの端末が通信できる状態じゃないのにデータ受け入れ可能になるわけはないですから。
FT232などのUSB-シリアル変換ICにもこのフロー制御用のピンは出ており、ソフトウェアから任意に出力を変更可能です。ただし、FT232ではDTRとRTSの論理が逆転しているので注意が必要です。
FT232R Datasheet - FTDI Chip より引用)

DTRとRTSを使ったリセット回路

さて、これを踏まえてDTRとRTSを使ったリセット回路を作る必要がありますが、大前提として「通常使用時(ESP8266の通常動作時)にシリアル通信をしてもリセット指令が出ない」というのが最低限の要件として挙げられます。
フロー制御を使っていないと言いつつも別用途でそれらの線を結線してしまっているわけですから、例えばPCで汎用ターミナルソフトを使ってESP8266とシリアル通信したときに、そのソフトが気を利かせてフロー制御を行ったがゆえに偶然リセットがかかっては困ります。
そこで先ほどの赤字を思い出してください。RTSが1になるときは必ずDTRも1になっているので(RTS,DTR)=(1,0)は通常のフロー制御ではありえないということになります。
なので、この状態をリセットに割り当てればいいわけです。
その条件をもとにトランジスタ1個で作った回路がこちらになります。FT232に合わせてDTRとRTSの論理を逆転させています。
(~RTS,~DTR)=(0,1)のときのみ~RESETが0になり、他の時は1になります。
そのほか、~RTS=IO0になりますので、~DTR=0にした状態で~RTSを操作することでIO0を変更することもできます。
これで無事にリセットシーケンスが送れるようになりました。めでたしめでたし。

ただ、1つ問題があります。
IO0を出力に使えません。また同じ系統の問題として、手動用のスイッチと共存させることができません。そうすると、RTSとIO0の間に1段トランジスタを挟みたくなりますよね。それならばせっかくなので対称な回路にしてしまいましょう。
これでよく見かける回路になりました。完成です。

真理値表を書くとこんな感じになります。
~RTS ~DTR ~RESET IO0
L L H H
L H L H
H L H L
H H H H
(~RTS,~DTR)=(L,H)のときのみ~RESETがLになり、他のパターンでは~RESET=1となります。そのほか、リセットにかかわらずIO0を制御する方法も確保できており、出力はプルアップとなっているためスイッチとの共存も可能です。
トランジスタ2石で完璧なリセット回路が出来上がりました。

自動書き込みを行う

さて、回路が出来上がったら実際に書き込みます。
Arduino IDEのツールメニューからReset Methodを"nodemcu" (NodeMCU)に指定してあげれば書き込みの直前でDTRとRTSを操作してくれます。
実際に書き込みの時のRESET端子とIO0端子を観察した画像がこちらです。青が~RESET、黄色がIO0です。最初にリセットをかけてから、その入れ替わりでIO0をLにしています。たいていRESET後はクロックが安定するまで(数~十数ms程度)マイコンは立ち上がりませんので、 RESETを終了させるのと同時にIO0をLにしても十分間に合うわけですね。


書き込みが始まるとIO0の黄色線がガタガタ震え始めています。
これは実際にマイコンとデータのやり取りを始めてRTSが変化しているためですね。フロー制御が行われているだけですが、目論見通り~RESETは安定してHを保てています。

これで幸せなESPライフが送れるようになりました。

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のメッセージボックスで終了処理がブロックされます。
利用者は、各自アレンジして使ってください。