syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

Windowsの画面キャプチャ取得方法

Windowsにおいて、画面のキャプチャ(スクリーンショット)を取得する方法はいくつかあるのですが、下記の標準機能は遥か昔(たぶんWindows 95あたり)から搭載されています。いずれもWindowsユーザーであれば誰もが知っていないとおかしいレベルの、初歩中の初歩です。

  • PrintScreenキー

デスクトップ全体を1つの画像として取得します。マルチディスプレイ(マルチモニター)の場合、合成された1つの画像となります。

  • Alt+PrintScreenキー

アクティブなウィンドウのみのスクリーンショットを取得します。ただしMDI子ウィンドウのみのキャプチャには対応していません。アクティブでない別のウィンドウがかぶさっている場合、そのウィンドウも映り込みます。

PrintScreenキーで取得したデータは、不可視のクリップボード(システム全体で共有するグローバルメモリ領域)にビットマップ画像データとして保存されます。オンメモリなので、当然Windowsをシャットダウンすると消失する揮発性のデータです。クリップボードに保存されたビットマップ画像データをファイルとして保存する場合は、お好みのペイントツールにてキャンバスに貼り付けて、所望のフォーマットで保存します。MS Paintを使う場合は、あらかじめキャンバスサイズを小さくしておけば、ペースト時に画像サイズに合わせて自動拡大してくれます。MS Word/Excel/PowerPointなど、オフィス系のソフトも直接クリップボード経由でドキュメント内に画像を貼り付けることができますが、設定によってはファイル埋め込みの際に、勝手に画像の解像度を落としてしまうこともあるので注意が必要です。サードパーティ製のアプリケーションを自由にインストールできる自分のプライベート端末では、起動と動作が軽快なIrfanViewを使ってクリップボードデータをファイル保存することが多いです。

ちなみにスクリーンショットを画像ファイルとして保存するときは、通例可逆圧縮PNGフォーマットを使います。非可逆圧縮JPEGは主に写真向けの圧縮フォーマットであり、画面スクリーンショットの保存には向いていません(色変化の激しい部分でモスキートノイズが目立ったり、PNGよりもファイルサイズが増加したりします)。

Windows Vista以降

Windows Vistaではキャプチャ用のSnipping Toolが搭載されました。デスクトップ全体・指定ウィンドウ以外に、選択領域のキャプチャもできます。しかし、個人的には上記のPrintScreenキーとペイントツールを使った方法よりもかえってめんどくさいので使っていません。また、PrintScreenキーを使った方法であれば、コンボボックスのドロップダウンリストを展開表示した状態や、特定のUI要素をマウスオーバーした状態でスクリーンショットを取得することができますが、Snipping Toolではそういったことができません。

Windows 8以降

Windows 8において、Windowsキー+PrintScreenキーを押すと、"%UserProfile%/Pictures/Screenshots"にデスクトップ全体のスクリーンショットPNG形式で自動保存されるようになりました。これはわりと有用な機能だと思いますが、Windows 7以前では使えないので、知っている人は少ないかもしれません。

ただし、Alt+PrintScreenのようにアクティブウィンドウのみをキャプチャして直接ファイル保存する機能がありません。この点がかなり不満です。

Windows 10

Windows 10 Creators Update (1703) ではゲーム録画の機能が追加され、Windows+Alt+PrintScreenでスクリーンショットを取得・保存できるようになりました。保存場所は"%UserProfile%/Videos/Captures"だそうです。「ゲーム録画」と銘打っていますが、ゲームアプリ以外でも使えます。しかし、利用にはXboxアプリへのMicrosoftアカウントを利用したサインイン(ネットワーク接続)が必要です。ローカルアカウント中心で使っているユーザーや、企業ユーザーの場合は使えない手段でしょう。

なお、1703では余計な機能満載のPaint 3Dが標準インストールされるようになったのですが、さらにWindows 10 Fall Creators Update (1709) では、従来の標準ペイントツールMS Paint(ペイント)が廃止・非標準になりました。完全に廃止されたわけではなく、Windowsストア(Microsoftストア)から明示的にインストールすれば使えるらしいのですが、ストアへのアクセスにはやはりサインイン(ネットワーク接続)が必要となります。電卓アプリの劣化と同じような道をたどるようです。ぶっちゃけ「Creators Update」とかいいつつ、誰も望んでいない、使えないゴミアプリを搭載しただけのアップデートです。

MS Paint自体はAdobe Photoshopなどと比べると大した機能を持っていませんが、これまでは標準インストールされていたことが最大の強みでした。特に仕事で、好きなペイントツールが使えない(インストールされていない/できない)端末において、スクリーンショットを取得するときだけはMS Paintをよく使っていました。また、機能が少ないぶんシンプルなので、誰でも使えます。しかし、今後は仕事で他の人にスクリーンショットの取得を指示する場合、まずはPaint 3DもしくはSnipping Toolに慣れてもらう必要がありそうです。

余談

XPやVista/7はLunaやAeroといったVisualテーマを適用すると、ウィンドウの四隅が丸くなります。キャプチャするとこの部分は白色となりますが、個人的にはこの丸まった四隅がダサくて大嫌いでした。
また、過去のWindowsのタイトルバーはグラデーションが使われていたり、Windows Aeroの機能でウィンドウが透過していたりしたのですが、これによりPNGキャプチャ画像は無駄にファイルサイズが大きくなりがちでした。Windows 8.xではデザイン方針としてModern UIを採用することでタイトルバーがシンプルな単色になり、PNGキャプチャ画像のファイルサイズも削減されました。しかし、Windows 10 1709では、Fluent Design Systemという、これまたMSの自己満足的デザイン方針が採用され、AppleiOSで採用されているようなすりガラス効果が多用されることになったため、再びPNGキャプチャ画像のファイルサイズが無駄に増えそうです。

GeForceドライバー380系列のDirect3D 11バグ

3DグラフィックスとC++の研究目的で、DirectX 11 (Direct3D 11) を使った自前FBXビューアーを開発しているのですが、とある自作FBXファイル(約18,000ポリゴン程度)を開いて、カメラを回転させながら描画すると、レンダリングが停止する現象に遭遇しました。デバッグ レイヤーからは以下のようなエラーメッセージが出ます。いわゆるTDRハングアップです。

D3D11: Removing Device.
D3D11 ERROR: ID3D11Device::RemoveDevice: Device removal has been triggered for the following reason (DXGI_ERROR_DEVICE_HUNG: The Device took an unreasonable amount of time to execute its commands, or the hardware crashed/hung. As a result, the TDR (Timeout Detection and Recovery) mechanism has been triggered. The current Device Context was executing commands when the hang occurred. The application may want to respawn and fallback to less aggressive use of the display hardware). [ EXECUTION ERROR #378: DEVICE_REMOVAL_PROCESS_AT_FAULT]

具体的にどのメソッドコールやシェーダーがタイムアウトのトリガーになっているのかまでは調べ切れていないのですが、以前は同じFBXファイルをまったく問題なく表示できていました。検証した組み合わせ環境は以下の通りですが、どうやらNVIDIA GeForceドライバーを384.94に更新したことが原因のようです。

  • Win10 (1703) x64 + GeForce GTX 760 4GB + 384.94: hang
  • Win10 (1703) x64 + Quadro M4000 + 382.48: OK
  • Win10 (1703) x64 + Quadro M4000 + 386.01: OK

少なくともQuadro M4000では比較的新しいドライバーを適用しても問題が発生しないことを確認できています。Quadroのドライバーは基本的にGeForceよりも厳密度や品質・安定性が重視されているので当然と言えば当然かもしれません。Kepler固有の現象なのか、それともGeForceであればMaxwellPascalでも発生するのかは不明です。

Direct3D 11は現在のハイエンド3Dゲーム開発における中核ともいえるAPIで、すでに次世代ローレベルAPIであるDirect3D 12やVulkanが正式リリースされて2年ほど経つものの、アプリケーション開発のしやすさの点から言えば、いまだDirect3D 11の地位は揺るぎません。今回発見したバグは、現役のAPIに関する基本的なリグレッションなので、すでにゲーム開発者やゲーマーからNVIDIAにバグ報告が寄せられて修正されていてもいいような気がしますが、2017年12月にリリースされたドライバーでも修正されていないようです。

逆にある程度古いドライバーだとタイムアウト現象は確かに出ないのですが、古いドライバーにはセキュリティ脆弱性が潜んでいることがあるので、古いドライバーを使い続けるのも得策ではありません。もともと384.94は、重大なセキュリティ脆弱性が修正されたとかいう話だったので急遽インストールしたものです。

なお、年明け早々に大々的に報じられたCPU脆弱性Spectre/Meltdownのうち、Spectreの修正に対応した390系列のドライバーが先日公開されました。とはいってもGPU側の予測分岐機能などがシステムのセキュリティに影響を及ぼすわけではなく、たとえばWebブラウザ上で実行するJavaScriptのように、悪意のあるコードの踏み台となりえるソフトウェアプログラムが含まれていたせいか、もしくはパッチ適用によるシステム低速化を緩和させる目的で、おそらく今回の修正対象となったものと思われます。通例ドライバーはシステムメモリにカーネルモードでアクセスできるため、対策も必要となるのでしょう。近いうちに390系列も試す予定です(ただし人柱になるのは御免こうむりたいので、しばらくは様子見)。

余談:ドライバー更新時の問題

バージョン番号の新しいNVIDIAドライバーをインストール(バージョンアップ)する場合は通例上書きインストールできるものなのですが、検証などのためにロールバック(バージョンダウン)しなければならない場合、まず現在のドライバーをアンインストールするのが常套手段です。
しかし、NVIDIAのグラフィックスドライバーおよびHDオーディオドライバーをコントロールパネルからアンインストールした後、Windowsを再起動すると、Windowsによって勝手にハードウェアの認識と古いデフォルトドライバーのインストールが始まってしまうのが厄介です(しかも結構時間がかかる)。
このプラグ&プレイ自動インストール動作はWindows Vista以降で実装されたもので、ドライバーのインストール作業の方法が分からないビギナーにとってはありがたいものなのかもしれませんが、ドライバーバージョンを完全に自分でコントロールしたいパワーユーザーにとっては単なるおせっかいでしかない迷惑機能です。「デバイスのインストール設定」でWindows Update経由のドライバーインストールを無効化していても抑制できない模様で、またローカルのどこかにデフォルトドライバーのインストーラーパッケージを隠し持っているためか、ネットワークを切断していても抑制できません。どうやら抑制するにはレジストリ操作が必要らしく、面倒なので今回はあきらめることにしました。

OpenCL/OpenGL/OpenCVのバイナリキャッシュ機能は使ってはいけない

OpenCL/OpenGLには当初、カーネルおよびシェーダープログラムに関してSPIR/SPIR-Vのような中間表現(バイトコード)規格が用意されておらず、それゆえオフラインコンパイルがサポートされていませんでしたが、コンパイル済みバイナリ(ベンダー依存)のキャッシュ機能はありました。

OpenCL 1.0:

OpenGL 4.1 or GL_ARB_get_program_binary:

また、OpenCV 2.xには、oclモジュールにおいて使用されるOpenCLカーネルのバイナリを、アプリケーションで使用する画像処理関数のカーネルごとに、初回呼び出し時に指定ディレクトリにファイル保存させることのできるキャッシュ機能が備わっていました。なお、保存されるファイル名にはOpenCLプラットフォーム名とデバイス名が含まれ、.clbの拡張子が付けられます。ただしワイド文字列のサポートはなく、したがってWindows上ではUnicodeがサポートされません*1

キャッシュ機能はOpenCV 3.0でいったんサポート外となった後、3.4にてOpenCL APIの薄いラッパーとして形を変えて復活したようです。

問題点

NVIDIA GeForceドライバーのとあるDirect3D 11リグレッション検証のために、グラフィックスドライバーのロールバック作業をしていたんですが、ついでにOpenCV 2.4.13を使ってOpenCV-CLの動作確認テストも実施したところ、OpenCLバイナリキャッシュの前方互換性がないことに気付きました。
具体的に言うと、例えば新しい388.71で出力したOpenCLカーネルのバイナリ*2を、古い353.90や364.72で読み込もうとすると、CL_INVALID_BINARYのエラーが発生します。OpenCV 2.4.13だと以下のようなエラーメッセージとなります。

XXX\sources\modules\ocl\src\cl_programcache.cpp:445: error: (-217) CL_INVALID_BINARY in function cv::ocl::ProgramFileCache::getOrBuildProgram

一応、古いカーネルを新しいドライバーで読み込むことはできる(後方互換性はある)ようですが、それが確実に保証されるのかどうか不明です。たぶん保証はされないでしょう。少なくとも前方互換性に関しては確保されていないことは確かです。おそらくOpenGLのシェーダープログラムバイナリに関しても似たような状況となっていることが予想されます。

これのどこが問題かというと、もしアプリケーションがカーネルバイナリをファイルとしてストレージにキャッシュ(保存)した後で、ユーザーがデバイスドライバーを変更すると、互換性がなくなって次回起動時にそのキャッシュが読み込めなくなり、アプリケーションが動作しなくなる、ということが懸念されるからです。
もしどうしてもバイナリキャッシュ機能を使いたい場合は、ドライバーの実装(バージョン番号)に紐づけた管理をするべきですが、少なくともOpenCV 2.xの実装はそうなっていません*3

なお、ドライバー変更以外でも、OpenCVのバージョンを変更すると、画像処理関数内部のカーネルが変わってOpenCLバイナリに互換性がなくなる、というケースがありえます。いっそOpenCV-CLのキャッシュ機能は使わないほうがよいでしょう。
少なくともPCにおけるAMD/NVIDIA環境に関しては、PCをシャットダウンするまで有効なオンメモリの組み込みキャッシュ機能がOpenGL/OpenCLドライバー側に備わっているはず*4なので、そちらに期待したほうがよいと思います。PC起動後の初回のコンパイル時間を我慢できれば、わざわざアプリケーション側でファイルとしてキャッシュする必要はありません。

ちなみにNVIDIA GeForceの場合、x86とx64とでOpenCLカーネルのバイナリ互換性がないらしいです。x86プロセスにてコンパイルしたバイナリをx64プロセスのドライバーに食わせようとすると、ドライバーがやはりCL_INVALID_BINARYのエラーを吐き、最悪の場合TDR後にドライバーがクラッシュすることもあるようです。普通に考えれば、ホストCPUアーキテクチャに依らずGPUコード側は同じはずなんですが、CUDA共有メモリ機能関連などでインターフェイス仕様の差異があるのかもしれません。Quadroは不明。
AMD FireProの場合は、少なくともCatalyst 15時点でx86/x64間の互換性がある模様です。Radeonは不明。

余談

ドライバー353.06だと、OpenCV 2.4.13のCV-CLで下記のエラーが発生します。別のバージョンのドライバーだと正常に動作するので、353.06のOpenCLドライバーにバグがある模様。

XXX\sources\modules\ocl\src\cl_operations.cpp:219: error: (-217) CL_MEM_OBJECT_ALLOCATION_FAILURE in function cv::ocl::openCLMemcpy2D

古いNVIDIAドライバーにはAPIが正常に動作しないバグがあるだけでなく、セキュリティ上の脆弱性も多々あるので、できるかぎり古いドライバーは使わないほうがよいです。
ところで先日、32bit版OS向けNVIDIAドライバーのサポートが打ち切られることが発表されましたが、現時点でWindows 7/8.1の32bit版は64bit版同様まだサポート期限が切れていないのに、今後32bit版において新しいハードウェアの対応がなされないのは企業ユーザーにとってかなり痛手になると思われます。32bit OS向けのセキュリティ対策は2019年1月まで続けられるそうですが、余命宣告を受けたも同然です。いずれにせよ、個人的にはどうでもいいんですけどね。

結論

やはりNVIDIA GeForceOpenCL実装は貧弱極まりないですね。また、それ以上にOpenCL/OpenGL仕様のいい加減さにあきれます。だからバイトコード規格を先に定義しろと(以下略)

*1:設計思想の古いOpenCVにとっては平常運転です。

*2:NVIDIAの場合、内部的にはCUDAで使われるPTXフォーマットになる模様です。

*3:クロスプラットフォームかつベンダー非依存な方法でドライバーのバージョン情報を取得する手段が標準化されていないこともあり、これはある意味仕方ないと思われます。

*4:OpenCL CもしくはGLSLのソースコード文字列に変更がなければ、一度コンパイルしたカーネルやシェーダープログラムのキャッシュが次回以降も使われるようです。

volatileに関してそろそろ一言いっておくか

今更言うまでもありませんが、C/C++C#/Javaではキーワードvolatileの意味が若干異なります。

C/C++

C/C++言語のvolatile修飾子は、コンパイラに副作用を示唆し、メモリアクセスの最適化を抑制するために存在します。volatileは典型的な処理系依存機能のうちのひとつであり、解釈は各コンパイラの実装に委ねられています。

C/C++のvolatileは、ときどきマルチスレッド間の簡易的な同期・通信用として使われていることがあります。たとえばサブスレッドでの処理完了を待機するために、グローバル変数などを用いて定義した処理完了フラグをメインスレッドにて監視・ポーリングする、といった状況です。

#include <cstdio>
#include <windows.h>
#include <process.h>
#include <conio.h>

volatile bool g_completed;

UINT CALLBACK MyThreadFunction(void*) {
  printf("Begin of sub-thread.\n");
  // ヘビーな処理を実行。
  for (int i = 0; i < 3; ++i) {
    printf("Executing task #[%d]...\n", i);
    Sleep(1000);
  }
  g_completed = true;
  printf("End of sub-thread.\n");
  return 0;
}

int main() {
  printf("Main thread waiting for completion of sub-thread...\n");
  auto hThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, MyThreadFunction, nullptr, 0, nullptr));
  while (!g_completed) {
    // メッセージ処理などを行ないながら待機。
    ::Sleep(1);
  }
  ::WaitForSingleObject(hThread, INFINITE);
  ::CloseHandle(hThread);
  hThread = nullptr;
  puts("Press any...");
  _getch();
}

処理完了フラグはサブスレッドで書き換えますが、上記のようなケースにおいてフラグ変数をvolatileで修飾しない場合、コンパイル時の最適化により、フラグ変数へのアクセスを(メインスレッド-サブスレッド間で共有する)メインメモリではなく、(スレッドローカルな)レジスタにて実行するようなコードを出力してしまうことがあります。そうなると上記のポーリング用whileループの継続条件式は常に真となってしまい、メインスレッドはループを永遠に脱出できなくなります(無限ループ)。

ただし、こういったスレッド間の同期・通信用途はC/C++本来のvolatileの守備範囲ではなく、避けるべきです。そもそも、C++03規格およびそれ以前では、スレッドという概念そのものが標準化されていません。このようにvolatileがマルチスレッド同期に乱用されるようになった背景として、MSVCにおける独自の勝手な言語拡張*1があります。

VC++のvolatile拡張仕様では、なんとメモリバリアまで張ってくれるらしいです。つまり読み書き操作が暗黙的にアトミック処理になります。ただしx86/x64アーキテクチャARMアーキテクチャとでは既定の動作(コンパイラオプション)が異なるらしいので注意が必要です。移植性を考えると、VC++のvolatile拡張仕様に依存するべきではなく、メモリバリアはスレッドライブラリに用意されている同期オブジェクトを使って明示的に実装するべきです。

なお、アトミック操作の実現手段としては、C++11規格以降は基本的に標準ライブラリのstd::atomicを使うべきです。ただし、C++/CLI (/clr) では残念ながらstd::atomicが使えないそうで、代わりに.NET標準クラスライブラリで用意されている同期オブジェクトやアトミック処理用のSystem.Threading.Interlockedクラスなどを使います。処理系依存のvolatileを使うのは最後の手段にしましょう。

C#/Java

C#のvolatileは、VC++のようなメモリバリアまではなされないものの、最適化を抑制する効果があります。C/C++と違い、処理系依存ではなくれっきとした言語仕様となっています。Javaのvolatileと同様です。C#/Javaは当初からマルチスレッドを考慮した言語設計がなされており、volatile仕様に関してもマルチスレッドが考慮されているため、限定的ながらvolatileをスレッド間の同期・通信に使うこともできます。

C#/Javaのvolatileフィールドに対するインクリメント・デクリメントなどはアトミック操作にならないので、当然そういった用途にはInterlockedクラスなどを使うべきですが、単純な代入と参照による状態管理用途であればvolatileでもOKな場面もあります。運用制限を設けて賢く使いましょう。

*1:うわさによると、VC7.1 (VC++ .NET 2003) では、volatile関連で /Oa というさらにぶっ飛んだ最適化オプションがあったそうですが、このオプションはVC8.0 (VC++2005) で削除されたそうです。

Visual StudioのビルドイベントでPowerShellを踏み台にしてC#を使う

Visual Studioでプロジェクトをビルドする際に、複雑な前処理・後処理を記述する場合、通例バッチコマンドによるカスタマイズをします。ただ、Windowsのコマンドは貧弱で、Unix/Linux環境のシェルなどとは比べ物になりません。
従来のバッチコマンドの代わりにPowerShellを使うのが近代的なWindowsプログラマーですが、個人的にはPowerShellの文法が好きではありません。言語機能も従来のバッチコマンドと比べると遥かに柔軟かつ高機能ですが、我々の大好きなC#言語と比べるとかなり書きづらいです。できればPowerShellよりもF#スクリプトC#スクリプトを使いたいのですが、これらはOS機能として統合・標準化されていないのが難点です。

そこで、PowerShellからC#コンパイラを使い、C#ソースコード文字列を渡してコンパイルし、C#で書かれたクラスをPowerShellから利用するという方法をとってみます。

param($rootDir)

$assemblies = (
#"System" # 不要。
"Microsoft.CSharp" # dynamic 型を使用するために必要。
)

$source = @"
using System;
using System.Runtime.CompilerServices;
public static class Test
{
  public static void CheckLambdaSpec()
  {
    var data = new[] { 1, 2, 3, 4, 5 };
    Action a = null;
    foreach (var x in data)
    {
      a += () => Console.WriteLine(x);
    }
    a();
  }

  public static void DoCallerInfoTestImpl([CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
  {
    Console.WriteLine(string.Format("Member name = \"{0}\"", memberName));
    Console.WriteLine(string.Format("Source file path = \"{0}\"", sourceFilePath));
    Console.WriteLine(string.Format("Source line number = {0}", sourceLineNumber));
  }

  public static void DoCallerInfoTest()
  {
    DoCallerInfoTestImpl();
  }

  public static void DoTest(string dir)
  {
    //string path = System.IO.Path.Combine(System.IO.Path.Combine(dir, @"Properties"), @"AssemblyInfo.cs");
    //using (System.IO.StreamReader reader = new System.IO.StreamReader(path))
    var path = System.IO.Path.Combine(dir, @"Properties", @"AssemblyInfo.cs");
    using (var reader = new System.IO.StreamReader(path))
    {
      string line;
      //Console.WriteLine(line?.Length.ToString() ?? "null");
      dynamic x = "hoge";
      Console.WriteLine(x.Length);
      while ((line = reader.ReadLine()) != null)
      {
        if (line.StartsWith("[assembly: AssemblyVersion("))
        {
          Console.WriteLine(line);
          break;
        }
      }
    }
  }
}
"@

try
{
  # ここでは、プロジェクトの出力先がカレント ディレクトリになる模様。
  #[System.Environment]::CurrentDirectory
  #$PSVersionTable
  $rootDir
  $rootDir.Length
  # コンパイル時に "%LocalAppData%/Temp/" にテンポラリ ソースファイル (*.cs) が生成される模様。
  Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $source -Language CSharp
  #Add-Type -TypeDefinition $source -Language CSharp
  [Test]::DoTest($rootDir)
  [Test]::DoCallerInfoTest()
  #[Test]::CheckLambdaSpec()
}
catch
{
  #Write-Error($_.Exception)
  Write-Error($_.Exception.Message)
  exit -1
}

上記PowerShellスクリプトをプロジェクトディレクトリに"test.ps1"として保存し、ビルド前あるいはビルド後イベントのコマンドラインとして、

powershell -ExecutionPolicy RemoteSigned -File "$(ProjectDir)test.ps1" "$(ProjectDir)\" ";exit $LASTEXITCODE"

を入力しておきます。
ここで、Visual Studio 環境変数PowerShell スクリプトコマンドライン引数として渡すとき、"$(ProjectDir)\" などとします。"$(ProjectDir)" ではダメなようです。末尾になぜか余計なダブルクォーテーションが入ります。

C#コンパイラのバージョン

前述のPowerShellからC#コンパイラを利用する手法自体に関してはWeb上のあちこちで言及されているのですが、そのC#コンパイラのバージョンに関してはほとんど言及がないようです。なので少し調べてみました。

前述の例はC# 5.0対応コンパイラが使えるという前提で記述しています。そもそもプリインストールされているPowerShellのバージョンもWindows OSによって異なるので、もしプロジェクトでPowerShellスクリプトを使う場合は最小バージョンに合わせて記述するか、開発者全員にPowerShellのバージョンアップを促しましょう。

Directory.Existsメソッドのタイムアウト時間

.NETのSystem.IO.Directory.Exists()メソッドは指定ディレクトリの存在有無をチェックするメソッドですが、タイムアウト時間が設けられています。ローカルドライブのディレクトリにアクセスする場合は、よほど低速なシステムか膨大なストレージでないかぎり、メソッドの実行は一瞬(数ミリ秒オーダー)で終わりますが、チェック対象がネットワーク共有フォルダーなどの場合、アクセスできないときはシステム規定のタイムアウト時間が経過するまで延々とリトライを続けます。この動作はSystem.IO.File.Exists()も同様です。なので普通、これらのI/O処理はメインスレッドで直接実行したりせず、サブスレッドに処理を逃がして非同期で実行するのがセオリーです。今どきのC#であればTPLやasync/awaitを使うのが常套手段でしょう。

Windows向けの.NET 4.x実装では、Directory.Exists()は内部でWin32 Shell APIのPathFileExists()とPathIsDirectory()を使っているものと思われます。確かネットワークアクセスのタイムアウト時間はWindows OSのシステム設定に依存するはずで、既定値は20秒だったと思います。また、タイムアウト時間はアプリケーション側では指定できないはずです。無効なネットワーク共有フォルダーにアクセスして、意図的にタイムアウトさせて時間を計測するために下記のC#スクリプトを試してみましたが、環境によって約20-30秒と開きがあるようです。ときどき40秒近くかかることもありました。ネットワーク共有フォルダーへのアクセスのタイムアウト時間に関しては、おそらく名前解決など、システムのネットワーク構成にも依存するものと思われます。

Action<string> checkDir = (path) => { var sw = new Stopwatch(); sw.Start(); Print(Directory.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkDir(@"\\1.2.3.4\hoge");

Action<string> checkFile = (path) => { var sw = new Stopwatch(); sw.Start(); Print(File.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkFile(@"\\1.2.3.4\hoge\fuga.txt");

C#スクリプト

C#コードをスクリプトとして対話的に実行できる「C# Interactive」はVisual Studio 2015 Update 1で追加された機能ですが、こういったAPIの挙動を調査するのに重宝しています。PrintメソッドはInteractiveScriptGlobalsクラスの静的メソッドのひとつですが、おそらくC# 6.0で追加されたusing static機能をC# Interactive内部で暗黙的に使い、using static InteractiveScriptGlobals;とすることでクラス名を省略できているものと思われます。using static自体はJava 5におけるimport staticの後追いのようなもので、不要な混乱を招きかねないため積極的に使うべき類の機能ではありませんが、こういったスクリプトコードに関してはC/C++Pythonなどのようにグローバルメソッドが簡潔に使えるということは重要ですね。

ちなみにC#スクリプトの拡張子は.csxで、C# Interactive外のVisual Studioコードエディターでもコード補完が効きますが、この拡張子のファイルをVisual Studio 2015 Update 3で編集していると、IDEが突然クラッシュするという残念な不具合があるようです。VS2017は試していません。

スレッドの強制終了について

I/O処理のタイムアウト時間が過ぎる前に処理を強制的に中断しようとして、ワーカースレッドをメインスレッドからSystem.Threading.Thread.Abortメソッドで強制終了しようなどとするのはご法度です。中断の結果として起こり得る未定義動作の危険性が説明されています。
Thread.Abort Method (System.Threading)
Thread.Abort Method (System.Threading) | Microsoft Docs

Abortメソッドの危険レベルとしてはWin32 APITerminateThread関数と同程度で、よほどのことがないかぎり使うべきではありません。現に.NET Coreの仕様では、Thread.Abortメソッドは削除されているそうです。

C# 6.0コンパイラーの興味深い挙動

C#言語はnullに関連する演算子が豊富です。

null合体演算子

null合体演算子は左側オペランドがnullでない場合は左側オペランドを返し、nullの場合は右側オペランドを返します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 2.0で追加されました。

x ?? alt

三項演算子で書くと以下のようになります。

x != null ? x : alt

null条件演算子

null条件演算子オペランドがnullでない場合はメンバーアクセス(.もしくは[])を実行します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 6.0で追加されました。

x?.SomeMethod();

if文で書くと以下のようになります。

if (x != null) { x.SomeMethod(); }

もしここでSomeMethod()が値型Tの戻り値を持つ場合、x?.SomeMethod()の評価結果はNull許容型T?となります。三項演算子で書くと以下のようになります。

x != null ? x.SomeMethod() : default(T?)

応用

これらの演算子を応用して、「引数がnullでない場合はToString()を呼び出し、nullの場合は代替のリテラル文字列"N/A"を返す」という処理をスマートに書いてみます。

// (1)
public static string ConvertToNAIfNull<T>(T x)
{
  return x?.ToString() ?? "N/A";
}

もしこれらの演算子を使わずに書くとすれば、以下のようになります。

// (2)
public static string ConvertToNAIfNull<T>(T x)
{
  return x != null ? x.ToString() : "N/A";
}

いずれにせよ1行で書けますが、(2)はxが2回出現して冗長なので、特にメソッドではなくインラインで書く際に不便そうですね。厳密に言うと、(1)と(2)は完全互換ではありませんが、通例ToString()は空文字列""を返却することはあってもnullを返却することはないので実用上は問題ないはずです。

ここで興味深いのが、(1)のジェネリクスには参照型やNull許容型だけでなく、通常の値型を渡しても実体化できるということです。csc 2015 (Visual C# 2015) でもgmcs 4.6.2でも動作します。

Console.WriteLine(ConvertToNAIfNull(new int?(10)));
Console.WriteLine(ConvertToNAIfNull(default(int?)));
Console.WriteLine(ConvertToNAIfNull(10));

値型に対する?.演算子呼び出しは無効なので、一見コンパイルエラーになってしまう気がしますが、(1)のジェネリクスC#コンパイラーが内部で以下のように展開するせいで、値型を渡してもコンパイルエラーにはならず実体化できる、というからくりになっているようです。

// (3)
public static string ConvertToNAIfNull<T>(T x)
{
  if (x != null) {
    var t = x.ToString();
    if (t != null) {
      return t;
    }
  }
  return "N/A";
}

値型に対してはx != nullコンパイルエラーにはならず常に真なので、(2)や(3)のジェネリクスに値型を渡せるのは当然です。いずれにせよ、ConvertToNAIfNull()に値型を渡すと冗長なメソッドになってしまいますが。