Hatena::ブログ(Diary)

やこ〜ん SuperNova2 このページをアンテナに追加 RSSフィード

2010-08-19

.net framework での現在時刻取得のオーバーヘッドについてのメモ

| 16:04 |  .net framework での現在時刻取得のオーバーヘッドについてのメモ を含むブックマーク  .net framework での現在時刻取得のオーバーヘッドについてのメモ のブックマークコメント

.net にて、タイムスタンプ*1を低精度で良いので高速に得たかったのだが、どの方法が早いのかについてあまり情報がなかったので、やってみた。

やった実験

以下のプロパティをそれぞれ 1048576 (2^20) 回呼び出すメソッドを 4 回呼び出すコードを Debug ビルドし、 Windows 7 (32 bit) 環境にて実行した。

  • (空)
    • なにもしない空のループを回す
  • Environment.TickCount
  • DateTime.Now.Ticks
  • DateTime.UtcNow.Ticks
  • Stopwatch.ElapsedMilliseconds

結果(概略)

2〜3 回目の結果を集計すると、以下のようになった(全データは後述)。

プロパティ2^20 回 [ms]一回当たり [μs/call]
Environment.TickCount2.530.0024
DateTime.Now.Ticks156.850.15
DateTime.UtcNow.Ticks18.840.018
Stopwatch.ElapsedMilliseconds43.440.041
  • 2^20 回 [ms]: (ループ全体所要時間の平均) - (空ループ 2^20 回の平均)
  • 一回当たり [μs/call]: 2^20 回、の値を 2^20 で割った値 (マイクロ秒に換算)

所感

  • Environment.TickCount が高速である
    • DateTime.UtcNowの 7 倍以上、Stopwatch の 17 倍以上
  • DateTime.Now はかなり遅い
    • ローカル時間への変換が遅いのでは、とのこと*2
    • 32bit 環境であることも影響しているのかもしれない*3
  • 実は Stopwatch はそれほど極端には遅くない
    • 高精度な代わりに遅い、と巷ではよく言われているが・・・
    • DateTime.UtcNow の 2.3 倍程度である
    • DateTime.Now よりははるかに速い

要するに、とにかく高速に時間を取得したい場合には、Environment.TickCount を使った方がよさそうだ。

ただし、Environment.TickCount は約 24.9 日であふれる*4という仕様であり、いまどき 30 日程度連続稼働する PC など珍しくはない(はず)なので、扱いにとても注意が必要である。たとえ 30 日も連続稼働しないであろうプログラムでも、約 24.9 日の境界をまたぐと値の大小関係が狂うので注意が必要であることに変わりがない。

また、高精度タイマーが遅いという先入観を根拠とした判断*5は必ずしも妥当ではないことも頭の片隅に置いておいてよいだろう。

加えて、ついつい使ってしまいがちな DateTime.Now は、ローカル日時が必要な場合に限って使うべきであり、ある二つの時点の時間差を知る用途には UtcNow (か Stopwatch や Environment.TickCount)を使うべきであろう。

結果(詳細)

Empty roop: 3.5261 [ms], 3.36275100708008E-06 [ms/call] 
Environment.TickCount: 6.346 [ms], 6.05201721191406E-06 [ms/call] 
DateTime.Now.Ticks: 162.7851 [ms], 0.000155243968963623 [ms/call] 
DateTime.UtcNow.Ticks: 22.4571 [ms], 2.14167594909668E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 54.7306 [ms], 5.21951675415039E-05 [ms/call] 
---------------- 
Empty roop: 5.4742 [ms], 5.22060394287109E-06 [ms/call] 
Environment.TickCount: 6.6567 [ms], 6.34832382202149E-06 [ms/call] 
DateTime.Now.Ticks: 163.9675 [ms], 0.000156371593475342 [ms/call] 
DateTime.UtcNow.Ticks: 21.4703 [ms], 2.04756736755371E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 46.5926 [ms], 4.44341659545898E-05 [ms/call] 
---------------- 
Empty roop: 3.1857 [ms], 3.03812026977539E-06 [ms/call] 
Environment.TickCount: 6.5811 [ms], 6.27622604370117E-06 [ms/call] 
DateTime.Now.Ticks: 149.1435 [ms], 0.000142234325408936 [ms/call] 
DateTime.UtcNow.Ticks: 23.2336 [ms], 2.21572875976563E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 45.3947 [ms], 4.32917594909668E-05 [ms/call] 
---------------- 
Empty roop: 3.1815 [ms], 3.03411483764648E-06 [ms/call] 
Environment.TickCount: 6.204 [ms], 5.91659545898438E-06 [ms/call] 
DateTime.Now.Ticks: 169.2678 [ms], 0.00016142635345459 [ms/call] 
DateTime.UtcNow.Ticks: 23.645 [ms], 2.25496292114258E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 50.1856 [ms], 4.78607177734375E-05 [ms/call] 
---------------- 
|tickCount|2.53346666666667|0.00241610209147135| 
|dt_ticks|156.8458|0.149579811096191| 
|dt_UTCticks|18.8358333333333|0.0179632504781087| 
|sw_ms|43.4438333333333|0.0414312680562337|

実験に使ったコード


public void Test() { 
     this.result["tickCount"] = new List<TimeSpan>(); 
     this.result["dt_ticks"] = new List<TimeSpan>(); 
     this.result["dt_UTCticks"] = new List<TimeSpan>(); 
     this.result["sw_ms"] = new List<TimeSpan>(); 
     for (var i = 0; i < 4; i++) { 
          TestCore(); 
          Console.WriteLine("----------------"); 
     } 
     foreach (var name in this.result.Keys) { this.ShowSummary(name); } 
} 

private List<TimeSpan> empty = new List<TimeSpan>(); 
private Dictionary<string, List<TimeSpan>> result = new Dictionary<string, List<TimeSpan>>(); 
private void ShowSummary(string name) { 
     var sum = result[name].Skip(1).Average((ts) => ts.TotalMilliseconds) - empty.Skip(1).Average((ts) => ts.TotalMilliseconds); 
     var per = sum / Times; 
     Console.WriteLine(string.Format("|{0}|{1}|{2}|", name, sum, per * 1000)); 
} 

private const int Times = 1024 * 1024; 
private void TestCore() { 
     var sw = new Stopwatch(); 

     sw.Start(); 
     for (var i = 0; i < Times; i++) { 
     } 
     sw.Stop(); 
     Console.WriteLine("Empty roop: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.empty.Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = Environment.TickCount; 
     } 
     sw.Stop(); 
     Console.WriteLine("Environment.TickCount: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["tickCount"].Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = DateTime.Now.Ticks; 
     } 
     sw.Stop(); 
     Console.WriteLine("DateTime.Now.Ticks: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["dt_ticks"].Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = DateTime.UtcNow.Ticks; 
     } 
     sw.Stop(); 
     Console.WriteLine("DateTime.UtcNow.Ticks: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["dt_UTCticks"].Add(sw.Elapsed); 

     sw.Restart(); 
     var stopwatch = new System.Diagnostics.Stopwatch(); 
     for (var i = 0; i < Times; i++) { 
          var t = stopwatch.ElapsedMilliseconds; 
     } 
     sw.Stop(); 
     Console.WriteLine("Stopwatch.ElapsedMilliseconds: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["sw_ms"].Add(sw.Elapsed); 
}

*1:日時でなくても、時間の差が分かればよい

*2http://stackoverflow.com/questions/62151/datetime-now-vs-datetime-utcnow

*3:TickCount は Int32 だが、DateTime.Ticks などは Int64 である。

*4:さらにほおっておくと、値が過去の値と重なる

*5:パフォーマンスと扱いやすさの両立のために Stopwatch を避けて DateTime を使う、など

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20100819

2010-05-03

VisualStudio 2010, 2008, 2005 のソリューション(.sln や .csproj など)を強引にコンバートするツールを作ってみた

17:31 | VisualStudio 2010, 2008, 2005 のソリューション(.sln や .csproj など)を強引にコンバートするツールを作ってみたを含むブックマーク VisualStudio 2010, 2008, 2005 のソリューション(.sln や .csproj など)を強引にコンバートするツールを作ってみたのブックマークコメント

作ったもの

  • SUtils.Development.VS.SolutionConverter.exe
    • .sln や .なんたらproj (.csproj や .vbproj など) をコンバートできる。
    • VS2010 の .sln などを VS2008 向けにコンバートしてビルド&動作までは確認
      • ただしいろいろと強引
      • 自分の使ってない VS の機能についても、それなりに大丈夫そうな気がするが、気がするだけである。
  • SUtils.Development.VS.FileFormat.dll
    • .sln のパーサーなど
    • VS2010 や VS2008、VS2005 のはき出す .sln を読み書き可能
      • アドオン(AnkhSVN とか)が付加する情報なども欠損せずに取り扱い可能

SolutionConverter.exe

VS2010 のソリューションから VS2008 のソリューションを機械的に作り出したかったので作った。

(自分の開発は VS2010 でやるが、VS2008 でもコードをいじれるようにしておく必要があったため。)

使いどころ

f:id:saiya_moebius:20100503171147p:image

f:id:saiya_moebius:20100503171146p:image

特に、

  • 同一のコードベースについて VS2010 と VS2008 版のソリューションとプロジェクトを用意したい
    • .cs や .vb といったコードは一つのファイルで、それを異なる VisualStudio 向けのプロジェクトから参照する
    • 上の画像のような感じに、同じフォルダに .sln や .なんたらproj が複数並ぶようにする
  • 新しい方の VisualStudio のソリューションから古い方の VisualStudio のソリューションを生成したい
    • アップグレードウィザードの逆
  • その他、VisualStudio の機能からはできない書き換えを、多少強引でも実行したい

という場合に使えるであろうツールである。

使い方

バイナリ: http://my-svn.assembla.com/svn/SToolsMirror/Development.VS.SolutionConverter

ソースコード: http://www.assembla.com/code/SUtils/subversion/nodes/trunk/CSharp/SUtils 以下

  • バイナリを落とす
  • SUtils.Development.VS.SolutionConverter.exe をたたく
    • SUtils.Development.VS.SolutionConverter.exe --help で簡易ヘルプが出る。

コマンドの例

SUtils.Development.VS.SolutionConverter.exe --sln=SUtils.sln --ver=vs2008 --fx=3.5

このように呼び出した場合、SUtils.sln ソリューションとそこから参照されているプロジェクトを全て読み込み、それらを

に変更して、SUtils.vs2008.sln などとして書き出す。

SUtils.Development.VS.SolutionConverter.exe は、VisualStudio (2005, 2008 or 2010) ソリューションファイルを読み込んで、VisualStudio (2005, 2008 or 2010) ソリューションファイルをはき出すことができる。

また、その際に、ソリューションから参照されているプロジェクトもコンバートし、プロジェクトの参照設定なども修正する。

注意点

当たり前だが、.net 4.0 にしかないメソッドや、.net 4.0 にしかないアセンブリが .net 3.x で使えるようになる!といったことは無理である。

そのため、コンバート先のフレームワークに存在しないメソッドなどは当然コンパイルエラーになる。

SolutionConverter.exe は、ビルド設定を書き換えて定数を追加 #define する機能がある(--const 引数で指定)ので、これを与えた上で、#ifdef を使う必要があるだろう。

// NET_3_x と NET_3_5 定数を追加で定義
// C# 側のコードでは、バージョン依存のコードを適宜 #ifndef NET_3_x なりでくくる。
SUtils.Development.VS.SolutionConverter.exe --sln=SUtils.sln --ver=vs2008 --fx=3.5 --const=NET_3_x,NET_3_5

また、VisualStudio がサポートしてない行為であるし、そもそも実現手段からして強引であるので、当然結果を保証できない。

大事なファイルに対して適用する場合は、バックアップなりを事前にとっておいた方が・・・。

マニアックな点

SolutionConverter.exe では、ほかにも以下の機能に対応している。

  • アセンブリ参照の強制削除(デフォルト: --refDel="System.Xaml, Microsoft.CSharp")
    • 特定のアセンブリへの参照を抹消する。
    • デフォルトでは、上記二つの、.net framework 4.0 にしかないアセンブリを参照から消す。
  • ファイル名の変換規則の変更(--slnFileName および --projFileName オプション)
    • SolutionConverter.exe が書き出す .sln や .なんたらproj のファイル名のパターンを変更する。
    • デフォルトは "{0}.{2}.{1}" (ファイル名.VSプロダクト名.拡張子)
    • String.Format のフォーマット文字列になっている。
    • ここを "{0}.{1}" (元のファイル名.拡張子) にすると、ファイルを上書きするようになる。
  • .sln に関連づけられたエディションの変更(--edi オプション)
    • 例えば、"Visual Studio 2010 に関連づけ" "Visual Basic Express 2005 に関連づけ" のように、エディションを変更できる。
    • "--edi=C#" や "--edi=Studio" のようにして指定。

SUtils.Development.VS.FileFormat.dll

ダウンロード元などは SolutionConverter.exe と同じ。

これの入っている SUtils は Apache ライセンスなので、ライセンスの範囲内でご自由にどうぞ。

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20100503

2010-04-20

Mac OS X Leopard (10.5) から Snow Leopard (10.6) へ移行したことで発生した問題めも

18:45 | Mac OS X Leopard (10.5) から Snow Leopard (10.6) へ移行したことで発生した問題めもを含むブックマーク Mac OS X Leopard (10.5) から Snow Leopard (10.6) へ移行したことで発生した問題めものブックマークコメント

最近、メインのノートPC(macbook pro)の環境を Snow Leopard へ移行*1したのだが、さまざまな問題が発生して苦しんだ。

ので、後々のためにも、Snow Leopard になって劣化した部分についてメモってみた。


スリープから復帰しなくなった

症状
  • bluetooth キーボードを接続したままの状態でスリープさせる。
  • すると、スリープから復帰しない。
    • 画面が真っ暗で、マウスカーソルも出ない。
解決策と副作用
  • "Allow Bluetooth devices to wake this computer" を off にする。
    • System Preference -> Bluetooth -> Advanced
    • Bluetooth キーボードやマウスからスリープ解除できなくなるので不便・・・
前からあった問題: スリープ中にデバイスを脱着すると危ない

筆者は、Bluetooth キーボードと外付けディスプレイ(DisplayPort -> DVI-D)を macbook に繋いで自宅にて使っている。

そして、外出する際には、それら外付けデバイスを外してから macbook をスリープさせて持ち出すようにしていた。

わざわざ外付けデバイスを外してからスリープしているのは、スリープ中にデバイスが増えたり減ったりすると、レジュームに失敗することがあるためである(画面が真っ暗かつ無反応になる)。

この問題は、Leopard (10.5) の頃から存在し、Snow でも治っていなかった。

解決策としては、スリープ前に外付けデバイスなどを外すようにするぐらいであろう。

今回の bluetooth デバイスの問題は、Snow Leopard でこの問題がより酷くなったものと推測される。


スリープからの復帰後にマウスカーソルしか出なくなった

発生する条件と症状
  • 外付けディスプレイをメイン画面に設定してあって、そのディスプレイを外してからスリープする。
    • メイン画面: メニューバーを表示する画面の意。
  • スリープやスクリーンセーバからの復帰時に、マウスカーソルだけが出た状態になる。
    • 画面は真っ暗。
解決策と原因(憶測)
  • "Requrie password (時間) after sleep or screen saver bigins" を off にする。
    • System Preference -> Security
    • スリープからの復帰時にパスワードを要求しないので、セキュリティ的に怖い・・・。

もしこの現象に遭遇してしまった場合、画面にはなにも出ていないがパスワード入力欄にフォーカスが当たっているので、落ち着いてパスワードを打ち込んで Enter を押すと、ロックを解除できる。

メイン画面に設定されてた外付けディスプレイが外されたことで、ノート PC 本体のディスプレイがメイン画面になったにも関わらず、ロック解除のためのパスワード入力画面が外付けディスプレイのあった座標に表示されてしまうのが原因かと推測される。


キーボードショートカットで仮想画面を切り替えると、キーボード操作が一切出来なくなるようになった

発生する条件と症状
  • Ctrl + (矢印キー) などのキーボードショートカットによって、Spaces の仮想画面を切り替える。
  • 仮想画面を切り替えたことを示す表示が消えなくなる。
  • あらゆるキーボード入力が効かなくなる。
    • ミュートやボリューム調整などの特殊なキーの一部だけが機能する。
解決策
  • Spaces をキーボードショートカットで使わない。
  • もし現象が発生してしまったら、あきらめてリブートする。
    • (10/04/21 追記) Finder を再起動するだけでも治る。
    • Finder を再起動するには、メニューバーのリンゴをクリックして、強制終了のためのメニューを出す。

メニューバーの一部が描画されなくなった

症状

f:id:saiya_moebius:20100420184902p:image

  • メニューバーの一部が描画されなくなる。
    • 日本語入力の状態やバッテリー残量 .etc が見えなくなる
  • 原因や発生条件は不明
解決策
  • 絶賛募集中

EMOBILE HW Utility が動作しなくなった

症状
  • Snow Leopard 対応の EMOBILE HW Utility を入れても、"failed" とだけ言われて接続に失敗する
  • Snow Leopard 非対応バージョンだと、Utility が何も言わずに落ちてしまう。
解決策
  • 絶賛募集中
  • しょうがないので VMWare 上の windows で e-mobile につないでます・・・

バックライトが点かない&キーやマウス入力を受け付けない現象が起こるようになった

// (10/04/21 追記)

症状
  • スリープから復帰させる
  • バックライトが点かない (よく見ると画面は表示されている)
  • キーやマウス入力に全く反応しない
  • macbook 限定の現象、だろう。

しばらく放置したが戻ってこなかった。

解決策
  • macbook の蓋を開閉してみる
    • してみたが、筆者の事例では効果がなかった。時間をおくといいのかもしれない・・・?

2010/05/01 追記

問題の Snow Loepard 環境について、以下の処置を施した。

  • Snow Leopard の修復(?)インストール
    • Snow Leopard の DVD から普通にインストール
  • Spotlight を停止
    • メモリを 700 MB 以上食っていたので

その上で、ここまでに述べた、確実に固まってしまう運用はしないように心がけた。

その結果、現在までにはフリーズなどを経験することなく、まともに運用できている。

*1:といっても、クリーンインストールである。

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20100420

2009-10-17

C# での Singleton についてまとめ

02:17 | C# での Singleton についてまとめを含むブックマーク C# での Singleton についてまとめのブックマークコメント

type initializer を使った Singleton パターンの実装のスレッドセーフ性について訊かれたことがあったので、それについて調べたことをメモってみた・・・らいつの間にかまとめっぽくなってしまったのであった。

参考資料

関連エントリ

履歴

  • 09/10/18: 誤字修正(beforefirstinit -> beforefieldinit)
  • 11/01/20: コード例の誤り修正、細かい表現の修正

基本

C# で singleton を実現する場合、基本的には以下のようにするのが、よく知られたイディオムである。

sealed class Singleton{
	public static readonly Singleton Instance = new Singleton();
	private Singleton(){ /* ... */ }
}

ただし、このコードには、以下の注意点がある。

  1. Singleton の static フィールドへの初回アクセス以前の「任意の時点」で Instance が初期化される
  2. side-effect などについてスレッドセーフ性が保証されない
  3. シングルトンの初期化が循環依存する場合に注意する必要
  4. Instance 取得時の処理のカスタマイズ性の問題
  5. シリアライズ時にインスタンスが増えてしまう問題

それぞれの注意点について詳細を以下に述べ、最後にガイドライン的なものを示す。

フィールドへの初回アクセス以前の「任意の時点」で Instance が初期化される

これについてはネット上の既存の多くの記事でも言われているが、先述のコードでは Instance の初期化(Instance = new Singleton())は Singleton の static フィールドへの初回アクセス「かそれ以前の任意のタイミング」で行われる。

なぜなら・・・

  • C# コンパイラは、static コンストラクタの無い型の type initializer には、beforefieldinit 属性を付加する。
  • beforefieldinit 属性のついている type initializer は、フィールドへの初回アクセス以前の任意の時点で呼ばれる (CLI 仕様)。
  • 詳しくは Type initializer についてまとめ (同一 blog) を参照のこと。

この挙動が望ましくないのなら、以下のように static コンストラクタを定義する手がある。

sealed class Singleton{
	public static readonly Singleton Instance = new Singleton();
	static Singleton(){} // suppress beforefieldinit
	private Singleton(){ /* ... */ }
}

このようにすることで、type initializer に beforefieldinit 属性が付加されなくなる。それによって、Instance の初期化は、Singleton.Instance に初めてアクセスした瞬間に行われることが保証される。

なお、ネット上の記事などでは inner class を用いることで初期化タイミングを保証するといった記述もあるが、それらは CLI 仕様ではなく実装依存な挙動であり、実行環境に依存するので注意が必要である。

ただし、ここで述べている static コンストラクタを書くという方法も、C# コンパイラの(beforefieldinit 属性についての)実装依存であるので、コンパイラに依存しないコードを書く場合には注意が必要である。

side-effect などについてスレッドセーフ性が保証されない

beforefieldinit 属性が付加されることによるもう一つの注意点として、スレッドセーフ性についての保証が破られうるという問題がある。

beforefieldinit 属性がついていないのなら、

  1. 確実に一回だけ、type initializer が呼ばれる
  2. type initializer が完了するまで、他のスレッドが Singleton.Instace にアクセスしたとしても待たされる

ということが保証されるが、beforefieldinit 属性がついていると、CLI 仕様ではこれらが保証されない。これは、type initializer のパフォーマンスを稼ぐための beforefieldinit の(CLI)仕様によるものである(単純に side-effect なしで static フィールドを初期化するためだけの type initializer のパフォーマンスを稼ぐため)。詳しくは Type initializer についてまとめ (同一 blog) なども参照のこと。

そのため、マルチスレッドに Singleton.Instance にアクセスされうる場合には、beforefieldinit を付けさせないようにする(→ static コンストラクタを書いておく)必要があるだろう。

シングルトンの初期化が循環依存する場合に注意する必要

以下のように二種類(かそれ以上)の Singleton なクラスがあり、それらの初期化処理が相互依存している場合には、ある Singleton 初期化処理中にもう片方の Singleton の参照が得られない(null になる)ことがあり得るので注意が必要である。

sealed class SingletonA{
	public static readonly SingletonA Instance = new SingletonA();
	private SingletonA(){
		SingletonB.Instance ...
	}
}
sealed class SingletonB{
	public static readonly SingletonB Instance = new SingletonB();
	private SingletonB(){
		SingletonA.Instance ...	
	}
}

この場合、Singleton{A, B} の type initializer から間接的に呼ばれている Singleton{B, A} の type initializer が実行完了しているかどうかが保証されないため、上のコードで Singleton{A, B}.Instance にアクセスした結果が null になりうる。

Instance 取得時の処理のカスタマイズ性の問題

先述のコードの場合、Instance フィールドが露出しているため、後からプロパティに差し替えた場合、依存しているコードの再コンパイルが必要になりうる。

そのため、この Singleton のコードに依存するアセンブリがあり、かつそれらが Singleton のコードとは別にコンパイルやリリースされるのであれば、以下のようにプロパティでラップしておくべきだろう。

sealed class Singleton{
	private static readonly Singleton _instance = new Singleton();
	public static Singleton Instance{ get{ return _instance; } }
	private Singleton(){ /* ... */ }
}

デシリアライズ時にインスタンスが増えてしまう問題

シリアライズ可能なオブジェクトから Singleton が参照されていたり、Singleton それ自体をシリアライズ可能にする場合、[Serializable] 属性を付けることになる。

しかし、単に [Serializable] 属性を付けただけの場合、Singleton がデシリアライズされる際に Singleton の新しいインスタンスが作られてしまうという問題がある。

これを防ぐためには、ISerializable インタフェースを実装した上で、SerializationInfo.SetType や IObjectReferece を用いることで対処することが出来る。

具体的なコードは、ISerializable Interface に詳しいのでそれを参照のこと。

ガイドライン

上記を踏まえると、C# で Singleton を実現する場合、エントリ先頭で示した基本となるコードに加えて、以下のようにした方が良いのだろう。

  • 以下の場合には、static コンストラクタを必ず書く (beforefieldinit 属性を付けさせない)
    • スレッドセーフな Singleton にする必要がある場合
    • Singleton の初期化タイミングを、確実に Instance アクセス時にしたい場合
    • ただし、コンパイラ非依存なコードにしたい場合には、static readonly ではなく lock などを用いた実装にする
  • Singleton の初期化処理が相互に依存しないように気をつける
    • それが難しいのなら、初期化処理にて他の Singleton の参照が null になることを考慮したコードを書く
    • beforefieldinit 属性を付けさせないことによってタイミングを保証させるのも一つの手である
  • 別のコンパイル/リリース単位から参照される場合、プロパティでラップしておく
  • Singleton を [Serializable] にする場合、単純にシリアライズさせないようにする

Type initializer についてまとめ

02:17 | Type initializer についてまとめを含むブックマーク Type initializer についてまとめのブックマークコメント

参考資料

  • ECMA-335 (4th edition)

Type initializer とは

Type initializer は CLI 仕様で規定されている、型それ自体を初期化するための特別なメソッドである*1

要するに(C# で言うところの) static メンバの初期化子や static コンストラクタを実行してくれるアレである。

CLI 仕様書(ECMA-335)の 10.5.3 (Type initializer) 以下では、下に述べる事柄が述べられている。

Type initializer の満たすべき条件

Type initializer には以下の制約が与えられている。これらは、普通の C# コンパイラを使ってコード生成する限りは気にする必要はないだろう。

  • static である
  • パラメタを持たない
  • 戻り値を持たない
  • rtspecialname, specialname を持ち、".cctor" という名前である

Type initializer で出来ること

Type initializer では、普通のメソッドとして出来ること以外に以下の事ができる。

  • initonly 属性を持つ static フィールドに書き込める

Type initializer の実行について、CLI によって保証されること

Type initializer の実行については、以下の点が守られることが、CLI 仕様によって保証されている(し、CLR*2 や mono はそれを守っている)。

共通して言えること

  • type initializer の実行タイミングについて
    • "all-zero" な値型や、null な参照型のメンバアクセスについてのみは、無視されうる
      • つまり、null なインスタンスのメンバにアクセスした場合などは、事前に type initializer が実行されるとは限らない
  • static field はいかなるアクセスよりも前に、known state に初期化されている
    • "all-zero" 値や null 値などであるということ

beforefieldinit 属性がないなら

  • 以下のうちいずれかの時に type initializer が実行される
    • その型の static field への初回アクセス時 または
    • その型の static method の初回の呼び出し時 または
    • その型のコンストラクタの初回の呼び出し時
  • 確実に一回だけ*3、与えられた型について実行される
    • ただし、ユーザーによって明示的に呼び出さた場合はこれは保証されない。
  • Type initializer の実行が完了していない型のフィールドには、type initializer によって呼ばれたメソッド以外はアクセスできない

beforefieldinit 属性でマークされているなら

  • 以下の時に type initializer が実行される
    • その型の static field への初回アクセス時 か それ以前

なお、beforefieldinit 属性がある場合、beforefieldinit ない場合で述べた事柄は保証されなくて良い。特に、最後の保証(type initializer の完了を待機する)が保証されないことがありうる。(ECMA-335 の 10.5.3.2 の Rationale 曰く、最後の保証は、複数 AppDomain 環境ではコストが高くつくうえ、滅多に必要がない*4ため、とのこと。)

また、この beforefieldinit の挙動は、初期化コードが特に有意な side-effect を持たない時のために用意されているそうだ(ECMA-335 8.9.5 の Note より)。

C# コンパイラの生成するコード

なお、C# コンパイラは、以下の処理を順番にひっつけて、Type initializer を生成する。

  1. static メンバの初期化処理
  2. static コンストラクタの中身

また、static コンストラクタが無い場合には、type initializer に beforefieldinit 属性を付加する。

*1:CLI 仕様(ECMA-335) 10.5.3 より、"a special method called a type initializer, which is used to initialize the type itself."

*2.net framework で使われている CLI 実装

*3:executed exactly once

*4:多くのコードでは、type initializer は単にフィールドを初期化するためだけのものであることが理由だそうだ。

tkaftkaf 2009/10/18 12:14 beforefirst属性 → beforefieldinit属性 でしょうか?

saiya_moebiussaiya_moebius 2009/10/18 20:02 ご指摘ありがとうございます。

というわけで直しておきました(beforefirst)。・・・なんという一括置換ミス。

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20091017

2009-07-01

Net_Growl を使ってみた

05:06 | Net_Growl を使ってみたを含むブックマーク Net_Growl を使ってみたのブックマークコメント

ref: Net_Growl 2.7.0

ref: ネイティブMacアプリをPHPで操作しよう - @IT

関連: OS X に macports で入れた pear (@ PHP 5.2.10) が使えない現象とそれへの対処 - やこ〜ん SuperNova2

せっかく pear のテストのために Net_Growl pear パッケージを入れてみたので、試しに使ってみた。

Net_Growl を使うと、php から簡単に Growl をたたくことができる。php で書かれたバッチ処理のためのスクリプトやらも持っている身としては、Growl で通知できればちょっと嬉しいものである。

必要な準備

% sudo pear install net_growl-beta

Growl 側の設定

@IT の記事 では触れられていないが、Net_Growl は UDP によって Growl への通知を行う関係上、Growl 側で UDP による通知を受け付けるように設定する必要がある。

具体的には、システム設定などから Growl の設定画面を開いた上で、Network タブで、

  • Listen for incoming notifications
  • Allow remote application registration

の二つを有効にしておく必要がある。

f:id:saiya_moebius:20090701050856p:image

また、このままだと同一サブネット上のどこからでも、このコンピュータへと Growl の通知を投げつけることができてしまう。そのことが不気味であるなら、パスワードを設定しておくべきだろう。

書いてみたコード

<?php
require_once 'Net/Growl.php';

$growlApp = new Net_Growl_Application("AppName", array("NotifyA"), "Growl で設定したパスワード");
$growl = new Net_Growl($growlApp);
$growl->notify("NotifyA", "title", "body");
?>

これを Growl の動いているマシンで実行すると、Growl の通知がポップアップするであろう。

f:id:saiya_moebius:20090701051632p:image

コードについてメモ

  • new Net_Growl() の第一引数は参照を受け取るので、Net_Growl_Application インスタンスの格納されている変数などを指定せねばならない
  • notify() の第一引数に指定する通知の名称は、Net_Growl_Application の第二引数の array にある文字列のどれかでなくてはならない
  • Growl 側でパスワードを設定しなかった場合、Net_Growl_Application の第三引数は不要である

OS X に macports で入れた pear (@ PHP 5.2.10) が使えない現象とそれへの対処

| 04:43 |  OS X に macports で入れた pear (@ PHP 5.2.10) が使えない現象とそれへの対処を含むブックマーク  OS X に macports で入れた pear (@ PHP 5.2.10) が使えない現象とそれへの対処のブックマークコメント

ref: Memo

ref: PEAR Forum - Forum for PHP Extension and Application Repository

症状

pear や pecl のパッケージをインストールしようとすると、

% sudo pear install -a HTTP_Request
pear.php.net is using a unsupported protocal - This should never happen.
install failed

という風に、pear コマンドが動作してくれない症状に遭遇した。

環境

OS X 10.5.7 に、以下のような +pear variant 付きの php5 (5.2.10) を macports でインストールしてある。

% sudo port installed | grep php5
php5 @5.2.10_1+apache2+dbase+fastcgi+gmp+imap+ipc+macosx+mssql+mysql5+oracle+pcntl+pear+postgresql83+pspell+readline+snmp+sockets+sqlite+t1lib+tidy (active)
% php --version
PHP 5.2.10 (cli) (built: Jun 30 2009 17:44:39) 
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2009 Zend Technologies
% pear version
PEAR Version: 1.8.0
PHP Version: 5.2.10
Zend Engine Version: 2.2.0
Running on: Darwin mac-3.local 9.7.1 Darwin Kernel Version 9.7.1: Thu Apr 23 13:52:18 PDT 2009; root:xnu-1228.14.1~1/RELEASE_I386 i386

対処

フォーラム を元に、以下の作業を行った。

なお、冒頭の ref 先の blog では、PHP 5.2.9 環境のファイルを PHP 5.2.10 へとコピーしているが、筆者の環境では PHP 5.2.10 環境を構築していなかったので、フォーラムにペーストされていたファイルの中身を元に作業をしている。中身の信頼性には要注意である。

まず、以下のようにファイルをバックアップ。

% cd /opt/local/lib/php/.channels
% sudo mv pear.php.net.reg pear.php.net.reg.org
% sudo mv pecl.php.net.reg pecl.php.net.reg.org

次に、以下の中身のファイルを .channels ディレクトリに作成する。

pear.php.net.reg
a:6:{s:7:"attribs";a:4:{s:7:"version";s:3:"1.0";s:5:"xmlns";s:31:"http://pear.php.net/channel-1.0";s:9:"xmlns:xsi";s:41:"http://www.w3.org/2001/XMLSchema-instance";s:18:"xsi:schemaLocation";s:71:"http://pear.php.net/channel-1.0 http://pear.php.net/dtd/channel-1.0.xsd";}s:4:"name";s:12:"pear.php.net";s:14:"suggestedalias";s:4:"pear";s:7:"summary";s:40:"PHP Extension and Application Repository";s:7:"servers";a:2:{s:7:"primary";a:1:{s:4:"rest";a:1:{s:7:"baseurl";a:4:{i:0;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.0";}s:8:"_content";s:25:"http://pear.php.net/rest/";}i:1;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.1";}s:8:"_content";s:25:"http://pear.php.net/rest/";}i:2;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.2";}s:8:"_content";s:25:"http://pear.php.net/rest/";}i:3;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.3";}s:8:"_content";s:25:"http://pear.php.net/rest/";}}}}s:6:"mirror";a:2:{i:0;a:2:{s:7:"attribs";a:1:{s:4:"host";s:15:"us.pear.php.net";}s:4:"rest";a:1:{s:7:"baseurl";a:4:{i:0;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.0";}s:8:"_content";s:28:"http://us.pear.php.net/rest/";}i:1;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.1";}s:8:"_content";s:28:"http://us.pear.php.net/rest/";}i:2;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.2";}s:8:"_content";s:28:"http://us.pear.php.net/rest/";}i:3;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.3";}s:8:"_content";s:28:"http://us.pear.php.net/rest/";}}}}i:1;a:2:{s:7:"attribs";a:3:{s:4:"host";s:15:"de.pear.php.net";s:3:"ssl";s:3:"yes";s:4:"port";s:4:"3452";}s:4:"rest";a:1:{s:7:"baseurl";a:4:{i:0;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.0";}s:8:"_content";s:34:"https://de.pear.php.net:3452/rest/";}i:1;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.1";}s:8:"_content";s:34:"https://de.pear.php.net:3452/rest/";}i:2;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.2";}s:8:"_content";s:34:"https://de.pear.php.net:3452/rest/";}i:3;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.3";}s:8:"_content";s:34:"https://de.pear.php.net:3452/rest/";}}}}}}s:13:"_lastmodified";a:2:{s:4:"ETag";s:20:""2fe96-59a-31a3fc80"";s:13:"Last-Modified";s:29:"Tue, 02 Jun 2009 05:55:46 GMT";}}
pecl.php.net.reg
a:6:{s:4:"name";s:12:"pecl.php.net";s:14:"suggestedalias";s:4:"pecl";s:7:"summary";s:31:"PHP Extension Community Library";s:7:"servers";a:1:{s:7:"primary";a:2:{s:6:"xmlrpc";a:1:{s:8:"function";a:10:{i:0;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:9:"logintest";}i:1;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:26:"package.listLatestReleases";}i:2;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:15:"package.listAll";}i:3;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:12:"package.info";}i:4;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:22:"package.getDownloadURL";}i:5;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.1";}s:8:"_content";s:22:"package.getDownloadURL";}i:6;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:25:"package.getDepDownloadURL";}i:7;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.1";}s:8:"_content";s:25:"package.getDepDownloadURL";}i:8;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:14:"package.search";}i:9;a:2:{s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}s:8:"_content";s:15:"channel.listAll";}}}s:4:"rest";a:1:{s:7:"baseurl";a:2:{i:0;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.0";}s:8:"_content";s:25:"http://pecl.php.net/rest/";}i:1;a:2:{s:7:"attribs";a:1:{s:4:"type";s:7:"REST1.1";}s:8:"_content";s:25:"http://pecl.php.net/rest/";}}}}}s:15:"validatepackage";a:2:{s:8:"_content";s:19:"PEAR_Validator_PECL";s:7:"attribs";a:1:{s:7:"version";s:3:"1.0";}}s:13:"_lastmodified";s:31:"Thu, 03 Jan 2008 11:48:06 -0700";}

上記を行った上で、以下を実行。

% sudo pear channel-update pear.php.net
% sudo pear channel-update pecl.php.net

これらの作業により、無事 pear でパッケージをインストールすることができるようになる、はずである。

% sudo pear install net_growl-beta
downloading Net_Growl-0.7.0.tgz ...
Starting to download Net_Growl-0.7.0.tgz (5,176 bytes)
.....done: 5,176 bytes
install ok: channel://pear.php.net/Net_Growl-0.7.0

2009-05-14

MethodInfo.GetGenericArguments() と System.RuntimeType と配列の covariance

| 00:07 |  MethodInfo.GetGenericArguments() と System.RuntimeType と配列の covarianceを含むブックマーク  MethodInfo.GetGenericArguments() と System.RuntimeType と配列の covarianceのブックマークコメント

概要

配列の covariance のおかげでデバッグに手間が掛かった、という話。

キーワードについて

  • MethodInfo.GetGenericArguments メソッド
    • これは、メソッドの型引数を表す Type[ ] を返す。
  • System.RuntimeType は、System.Type の .net 上における実装。
    • System.Type は public abstract class
    • System.RuntimeType は internal class
    • この RuntimeType は、mono 環境に存在しない。
  • 配列の covariance とは、例えば A is B ならば A[ ] を B[ ] に暗黙に変換できるということ

ひどい目

SUtils(https://www.assembla.com/wiki/show/SUtils)の持っているリモートオブジェクト機構(SUtils.Remoting)にて、Generic なメソッドをサポートしていたのだが、これがなぜか mono 上のサーバーと .net 上のクライアントでのみ正常に動作しないというバグが出たのが発端。

このバグでは、mono 環境上のサーバープロセスにて、デシリアライザにて System.RuntimeType が発見できなかった旨の例外が発生していた。

しかしここで不思議なのは、SUtils.Remoting では、RuntimeType を通信に使っている型の定義や、そこから使われている型には含まれないように意図的に排除していたのである。にも関わらず RuntimeType の型情報がシリアライズされ、mono 環境にて検出されてしまったのである。

じつはこれは、MethodInfo.GetGenericArguments() が、Type[ ] ではなく RuntimeType[ ] を返していたのが原因であった。

イメージ図

class HogeMessage{
    // Generic なメソッドの型引数を覚えておく
    // Type や Type[ ] は安心してシリアライズ出来ると分かっている
    public Type[ ] TypeArguments;
}

HogeMessage msg = ...;
// GetGenericArguments() の戻り型は Type[ ]。
// でも、実際には RuntimeType[ ] インスタンスが戻る。
msg.TypeArguments = method.GetGenericArguments();

すなわち、GetGenericArguments() が返す Type[ ] (上の例で HogeMessage.TypeArguments)をそのままシリアライズした所、それは実際には RuntimeType[ ] であった故に、mono 環境でのデシリアライズで RuntimeType 何それ美味しいの、な状態になってコケてしまったのである。

問題の一般化

今回の事例は、.net と mono 環境の間でシリアライズされたデータを相互にやりとりしていて、かつ片方にしかない型がある、という一般的にはあまりないであろうシチュエーションであった。

しかしながらも、例えばメソッドなどが、シグニチャ上の型とは違う型の配列を返しうるということは、例えば以下のような問題も起こりうることを意味している。

class A{}
class B : A{}

A[ ] array = (A[ ] を返すメソッドの戻り値);
array[0] = new A();   // ArrayTypeMissmatchException

このように、配列の covariance が通る C#/CLI など*1において、GetGenericArguments() のように、シグニチャ上の型(Type[ ])とは違う型の配列(RuntimeType[ ])を戻す実装をすると、使う側にとって予期せぬバグを招くため、危険である。

なので、配列型を返すメンバなどを宣言した場合は、返すインスタンスはその宣言と同じ型になっているべきであろう。

また、GetGenericArguments() のように、正体不明な配列インスタンスを返すメソッドを呼ぶ場合、戻り値の配列はいったん列挙して、必要なら別のコンテナに移すようにした方が、より安全であろう。

(というか、個人的には配列の covariance なんざ使うべきではないとも思っている。そうであるが故に、今回の GetGenericArguments の件は想定外だったのだが・・・。)

*1Java もそうである

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20090514

2009-05-05

Ubuntu 8.04 (hardy heron) に mono 2.4 をインストール

06:27 | Ubuntu 8.04 (hardy heron) に mono 2.4 をインストールを含むブックマーク Ubuntu 8.04 (hardy heron) に mono 2.4 をインストールのブックマークコメント

我が家のメインサーバーである Ubuntu 8.04 (近々 OpenSUSE にリプレース予定) に mono 2.4 をインストールしてみたのでメモ。

Ubuntu 8.04 の公式パッケージなどでは mono 1.x になってしまうので、ソースからビルドした。

環境とインストール後の状態

% uname -a
Linux NEC-1 2.6.24-16-generic #1 SMP Thu Apr 10 12:47:45 UTC 2008 x86_64 GNU/Linux

% lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 8.04.2
Release:	8.04
Codename:	hardy
% mono -V
Mono JIT compiler version 2.4 (tarball 2009年  5月  5日 火曜日 05:18:44 JST)
Copyright (C) 2002-2008 Novell, Inc and Contributors. www.mono-project.com
	TLS:           __thread
	GC:            Included Boehm (with typed GC)
	SIGSEGV:       altstack
	Notifications: epoll
	Architecture:  amd64
	Disabled:      none
 % gmcs --version
Mono C# compiler version 2.4.0.0

やったこと

ほとんど、こちらのサイトを参考にひたすら configure, make, make install しただけである: invisiblefulmoon.net -&nbspこのウェブサイトは販売用です! -&nbspinvisiblefulmoon リソースおよび情報

上のサイトは Ubuntu 9.04 対象だが、Ubuntu 8.04 でも同様の手順でほぼ問題なくインストール完了した。

問題など

事前にインストール済みの mono を apt-get remove した

手でインストールした mono と共存しても大丈夫かもしれないが、共存させる意味もあまり感じられないので、apt-get install してあった mono は消した。

mono-tools の make で "An exception was thrown by the type initializer for System.Drawing.GDIPlus" となってコケる

以前書いたこのエントリと同じ話である: OS X に macports でインストールした mono で Graphics や Forms を使うための設定 - やこ〜ん SuperNova2

対処法としては、~/.mono/config に

<configuration>
        <dllmap dll="gdiplus.dll" target="/usr/local/lib/libgdiplus.so"/>
</configuration>

と書いておけばよい(当然ながら事前に libgdiplus のインストールが必要である)。

こうすることで、gdiplus.dll を発見できないためにコケているところを、gdiplus.dll の代わりに libgdiplus.so をロードさせることで解決できるようになる。

gnome-desktop-sharp が gtksourceview2-sharp.dll: no と主張する

gnome-desktop-sharp の configure にて、

% gacutil -l | grep source
gtksourceview-sharp, Version=1.0.0.2, Culture=neutral, PublicKeyToken=35e10195dab3c99f
gtksourceview2-sharp, Version=2.0.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f

という風に、gtksourceview2 がインストールされているはずであるにも関わらず、

   Optional assemblies included in the build:

      * gtkhtml-sharp.dll: yes
      * gtksourceview2-sharp.dll: no
      * nautilusburn-sharp.dll: yes
      * rsvg-sharp.dll: yes
      * vte-sharp.dll: yes
      * wnck-sharp.dll: yes

となってしまう問題がある。

gacutil -l では出てくるのに見つからないあたり、何か問題があるのだろうが、今のところ gtk / mono なアプリを触る予定が無い上、おなかが減ったのでとりあえず放置してある。

おそらく、gtksourceview を使えないことで MonoDevelop などで困るだけだと思うので、後日気が向いたらどうにかする予定。

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20090505

2009-05-03

System.Type.GUID プロパティと GuidAttribute

22:41 | System.Type.GUID プロパティと GuidAttributeを含むブックマーク System.Type.GUID プロパティと GuidAttributeのブックマークコメント

ref: Type.GUID プロパティ (System)

ref: GuidAttribute クラス (System.Runtime.InteropServices)

まとめ

  • GuidAttribute を型に付けることで、Type.GUID の値を指定できる
    • GuidAttribute を付けておけば、コンパイラやランタイムに依存せずに Type.Guid を取得できる
  • GuidAttribute が無い場合の Type.GUID の値は、ランタイムに依存
    • .net framework なら、型の完全限定名を元に GUID を振ってくれる
    • mono の場合、GuidAttribute が存在しない限り、Type.GUID は Null Guid*1 になる
  • Type.GUID は、現実的には COM Interop

そもそも Type.GUID は何のためにあるのか

MSDN の Type.GUID の解説には「GUID は、GuidAttribute 属性を使用して型に関連付けられています。」とあるので GuidAttribute についての記述を見てみると、タイプライブラリ インポーターが使う旨が読み取れる。

このことから、Type.GUID は COM Interop のためにあると考えられる。

任意の型について、GUID はどのようにして決定されるのか

Generic 型について

Generic な型については、Type.GUID は型引数が与えられているかどうかに関わらず同じ値を返す。

つまり、

typeof(List<>).GUID
typeof(List<string>).GUID
typeof(List<int>).GUID

は全て同じ Guid を返す。

よって Generic な型もそうでない型も Type.Guid に関する振る舞いは同じなので、以下では区別せずに扱う。

GuidAttribute がある場合

Type.Guid{ get; } は、GuidAttribute で指定した GUID をそのまま返す。

これについては、csc.exe、gmcs のどちらでコンパイルしても、.net framework、mono のどちらで実行しても正しく動作する。

GuidAttribute がない場合

GuidAttribute がない場合には、Type.GUID の値はランタイムで決定される。つまり、アセンブリを作ったコンパイラではなく、実行している環境に依存した値になる。

具体的には、

  • .net framework の場合: 型の完全限定名*2を元にした値
  • mono の場合: Null Guid

が得られる。

なお、前者については、GuidAttribute の無い適当な型を作って、型の完全限定名を変えながら実行してみると分かるが、たとえクラスの中身が別物だろうと完全限定名が一致すれば同じ Type.GUID になる。

また、後者については、Re: (Mono-dev) Type.GUID patch の議論にあるとおり、

  • MS.NET の Type.GUID の生成アルゴリズムが不明
  • そもそも COM Interop 以外に使わないはず

といった理由があるようだ。

Type.GUID は COM 相互運用のための値

個人的に Type.GUID が気になった理由は、自作シリアライザなどで、型の完全限定名を毎回付加するのがサイズ的に大きくて悲しいことへの対処に使おうと考えたからであった。

のだが、この状況を見るに、Type.GUID は、「Type に対応する GUID 値」でなく、(おそらく MS の人が意図しているであろう通り、) COM 相互運用のための値と割り切ったほうが無難なようだ。

*1:全ビットが 0 の GUID

*2:アセンブリ名やバージョンも含む

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20090503

2009-04-06

xterm-color の TermInfo がない @ Solaris

| 08:36 |  xterm-color の TermInfo がない @ Solarisを含むブックマーク  xterm-color の TermInfo がない @ Solarisのブックマークコメント

ref: 404 - エラー: 404

最近新しい環境に ssh でログインすることがあったのだが、その際に

  • vi, emacs, less などなどがまともに立ち上がらない
  • シェルによっては Backspace などが効かない

ということがあってハマったのでメモ。

結局、これは端末の種類に対応する terminfo がなかったために起きていたようだ。

Terminfo とは

先頭にある ref 先に詳しいが、要するに端末の種類(vt100, xterm, ...)に応じた制御文字のデータベースである。

less, vi, emacs, (bashでないある種の)シェルなどは、ここから得られる情報を使って制御文字を吐くことで端末の出力を制御する*1

ので、自分の使っている端末に応じた terminfo が当該環境に無いと、悲しいことが起こるのである。

悲しいことの例

今回問題になった環境(Solaris)では、less や emacs が以下のようなエラーメッセージを出してくれたおかげで解決できた次第。最初は zsh が何もエラーを出さずに、かつ挙動がおかしかったりしたのでハマった・・・。

less のエラーメッセージ:

WARNING: terminal is not fully functional

emacs のエラーメッセージ:

emacs: Terminal type xterm-color is not defined.
If that is not the actual type of terminal you have, use the Bourne shell command `TERM=... export TERM' (C-shell: `setenv TERM ...') to specify the correct type.
It may be necessary to do `unset TERMINFO' (C-shell: `unsetenv TERMINFO') as well.

対処

使っている端末の種類は $TERM 環境変数を読めば出てくる。筆者の場合、

$ echo $TERM
xterm-color

だった。

で、それに対応する Terminfo が存在するかを確認するには、locate コマンドを使ってファイルを探すのが早い、と先述の ref 先には書いてあった。のだが、いかんせん件の環境では locate が core を吐いて落ちる。どうやら、調べてみたところ、Solaris の terminfo は /usr/share/lib/terminfo に置いてあるそうである。

TERMINFO – terminfo データベースに追加された、デフォルトでない端末のパス名を指定します。terminfo データベース内のデフォルト端末については、この変数を設定する必要はありません。terminfo データベースの詳細は、『Solaris のシステム管理 (上級編)』を参照してください。

TERM – 現在使っている端末を指定します。エディタを実行するときは、TERM 変数で定義された名前と同じ名前のファイルが検索されます。その場合、まず最初に TERMINFO 変数で指定されるパスが検索され (TERMINFO が定義されている場合)、次にデフォルトディレクトリの /usr/share/lib/terminfo が検索されて、端末の特性が決定されます。TERM 変数で定義されたファイルが見つからない場合は、その端末はダム端末と認識されます。

http://docs.sun.com/app/docs/doc/816-3946/6ma6m5bp5?l=Ja&a=view:title

また、上にもあるとおり、デフォルトの terminfo の置き場所(/usr/share/lib/terminfo)の前に、$TERMINFO で設定したパスを探索してくれる。

terminfo の置き場は、先頭一文字のディレクトリをほって、その下にコンパイル済みの*2 terminfo ファイルを置いてやると良いようだ。

件の環境での例:

$ ls /usr/share/lib/terminfo
1  3  5  7  9  B  H  P  a  c  e  g  i  k  m  o  q  s  u  w  y
2  4  6  8  A  G  M  S  b  d  f  h  j  l  n  p  r  t  v  x  z
$ ls /usr/share/lib/terminfo/x
x1700     x1750     xitex     xpcterm   xtalk     xtermc    xterms
x1720     x820      xl83      xpcterms  xterm     xtermm

この辺の挙動についても、冒頭の ref 先の説明が分かりやすい*3

そこで、

$ ls ~/.terminfo
x
$ ls ~/.terminfo/x
xterm-color

という構造になるように terminfo を置いてから、

export TERMINFO=/home/myname/.terminfo

と設定してやると、emacs なり zsh なりも無事に使えるようになったのである。

*1:ncurses とかがそうなっている

*2:要するにバイナリな

*3:『Solaris のシステム管理 (上級編)』を検索してもそれらしき記述が無かった・・・

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20090406

2009-03-27

日記間違い

| 18:16 | 日記間違いを含むブックマーク 日記間違いのブックマークコメント

別の blog に書くはずの(あまりこちらに関係ない)記事をうっかり書いてしまった・・・。これはやはり対策が必要だ・・・。

RSS リーダーなどのアンテナに引っかかってしまった人*1には申し訳ない。

今後は書く前にアカウントをチェックするのと、そもそも日記を間違えられないような運用に変えていくとします。

*1:居るのだろうか

トラックバック - http://d.hatena.ne.jp/saiya_moebius/20090327