2019年2月13日水曜日

リフルシャッフルで元に戻る回数

「52枚のトランプに対して正確なリフルシャッフルをすると8回で元に戻る」——どこかで聞いたことがあるようなフレーズです。私は昔何かのテレビ番組で見たような記憶があります。

リフルシャッフルとは、カードを2山に分けてパラパラと1枚ずつ交互に重ねていくシャッフル方法です。マジシャンとかがやると非常にかっこよく見えるシャッフル方法ですが、私はうまくできません。2枚以上のダマが入り込んでしまいます。

実はこれ、「シャッフル」と名が付いていますが全然混ざっていないんですよね。正確なリフルシャッフルは元のカード束に対して一意の結果が出てくるのはもちろんのこと、同じ側から2枚以上連続して入ってしまう下手なリフルシャッフルでも最悪ケースで並び順は$2^{N-1}$通りしかありません。完全にランダムなシャッフルの場合は$N!$通りですからリフルシャッフルのほうがはるかに混ざっていません。52枚のトランプを想定すると、リフルシャッフルでは$2.25\times10^{15}$通り、完全にランダムなシャッフルでは$8.07\times10^{67}$通りですから一目瞭然ですね。
ですので、リフルシャッフルを利用するマジックは、たいていこの「実はよく混ざっていない」を使っているんですね。Norman L. Gilbreathがギルブレスの原理として数学的に体系化しているらしいです。観客に対して混ざったと思わせるには十分大きいパターンがありますが、マジックにするには十分の混ざらなさ具合というわけですね。

さて、話を戻します。52枚のトランプでは8回正確なリフルシャッフルをすると元の配列に戻ります。これを証明していきます。

$0,1,2,3,...,24,25,26,27,28,...,50,51$の番号が付いたカードがこの順に並んでいるとします。これを1回正確なリフルシャッフルをすると、それぞれのカードは$0,2,4,8,...,48,50,1,3,5,...,49,51$番目へ移動します。カードの番号がこの順に並び変わるのではなく、それぞれのカードが何番目へ移動するかを表した数列ですので注意してください。
これは、最後の51番目のカードを除いて、$k$番目なら$2k$を51で割った番目へ遷移していることがわかります。これを$R(k)$と表すとすると、\[R(k)\equiv2k\pmod{51}\]と書けます。
さらに、2回正確なリフルシャッフルをすると$R(R(k))$へ遷移することになりますので、これを$R^2(k)$と表すとすると$R^n(k)$が定義できます。\[R^n(k)=R(R^{n-1}(k))\equiv 2R^{n-1}(k)\pmod{51}\]と書けますので、再帰的に適用すれば\[R^n(k)\equiv 2^nk\pmod{51}\]となります。非常にシンプルな式ですね。
カードの配列が元に戻ると言うことは、$0\leqq k<51$の全ての整数$k$に対して\[k\equiv 2^nk\pmod{51}\]が成立するということになります。もうちょっとわかりやすく式変形すれば\[2^n-1\equiv 0\pmod{51}\]になります。
ちなみに、一番下の51番目のカードを除いて計算してきましたが、一番下のカードは毎回一番下から動かないので、考慮に入れなくても何も問題はありません。
この式が成立する最小の$n$は8ですね($2^8-1=255$で$255\div51=5$ですね)。

さて、証明できましためでたしめでたし。
ただ、これだけだとプログラミング関係のブログというテーマに反してしまいますので、無理やりにでもプログラミング要素を入れていきます。

52枚のトランプだけでなく、$2m$枚のカード束について考えていきます。
52枚でしたので$\bmod{51}$でしたが、$2m$枚なら$\bmod{2m-1}$にすればいいだけですね。
static int ReturnToOriginalCount(int CardCount)
{
    if(CardCount <= 0) throw new ArgumentOutOfRangeException(nameof(CardCount));
    if((CardCount & 1) != 0) throw new ArgumentException($"{nameof(CardCount)} must be even.");

    BigInteger exp = 1;
    int i = 0;
    do {
        exp <<= 1;
        i++;
    } while((exp - 1) % (CardCount - 1) != 0);

    return i;
}
これでカード枚数から何回で元に戻るかの関数が完成です。ちなみに2のべき乗はシフト演算で実現しています。これを使って100枚以下の枚数のカード束で何回で元に戻るかを計算するとこうなります。
N [枚] k [回]
2 1
4 2
6 4
8 3
10 6
12 10
14 12
16 4
18 8
20 18
22 6
24 11
26 20
28 18
30 28
32 5
34 10
36 12
38 36
40 12
42 20
44 14
46 12
48 23
50 21
52 8
54 52
56 20
58 18
60 58
62 60
64 6
66 12
68 66
70 22
72 35
74 9
76 20
78 30
80 39
82 54
84 82
86 8
88 28
90 11
92 12
94 10
96 36
98 48
100 30
52枚は周辺の枚数に比べて突出して少ないですね。トランプの52枚というのはなかなか神秘的な枚数です。ちなみにジョーカーを含めて54枚とすると52回も正確なリフルシャッフルをしなければ元に戻りません。
ちなみに元の式を見てもらえばわかりますが、$2^n$枚のカードのときは$n$回で元に戻ります。これも突出して周囲より低い回数で元に戻る枚数ですね。

せっかくなので、ついでにk/Nも計算してみます。数学的な意味はともかく、「枚数の割には早い回数で元の並びに戻る枚数」と考えることができます。
3万枚以下でk/Nが小さい順に並べると、トップ10は次のようになります。
N [枚] k [回] k/N
29128 18 0.000617962
21846 16 0.0007324
25576 20 0.000781983
28680 24 0.00083682
28198 24 0.000851124
16384 14 0.000854492
25306 24 0.000948392
27306 28 0.001025416
23206 24 0.001034215
19066 20 0.001048988
この中で$2^n$枚なのは16384枚だけですね。29128枚もあるカードでも、18回正確なリフルシャッフルをするだけで元に戻ってしまうんですね。
誰かやってみてください…。

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