プログラム関連 >  メモ >  .NET(VB.NET)
最終更新日:2006/11/19

.NET(VB.NET)

基本的な注意事項

私的なメモなので、あまり信用しないでください。 実際の動きは自分で確認してください。 基本的に、VS(VB)は2003、.NET Frameworkは1.1が対象となっています。 それ以外のバージョンの話題も含まれるかもしれませんが、まあ気にしないでください。 メモというより独り言になっている部分もありますが、今更編集しなおすのも辛いので軽くスルーしてください。 また、同じような内容が繰り返し記述されている可能性もありますが、まとめる気力がないのでスルーしてください。

こんなメモよりもよっぽど有益な情報源

工事中・・・

メモIndex

現在ナッシングです。

■ String型の既定値

String型は初期化していない場合空文字が入ってるんじゃなくて、Nothing。 まあ、プリミティブじゃなくてオブジェクト型だから当たり前と言えば当たり前か。
→嘘。String型はプリミティブ。プリミティブかどうかは値型か参照型かは関係ない。 参照型だったから初期化していない場合の既定値、Nothingが入っているだけ。

■ WindowStateプロパティを使用してのフォーム最大化

WindowStateプロパティを使用してフォームを最大化させると、たとえ「元に戻る」コントロールボタンを無効にしていたとしても、 ウィンドウのタイトルバーをダブルクリックすると強制的に元のサイズにウィンドウが戻ってしまう。 但し、逆の動作(通常の大きさの時にタイトルバーをダブルクリックして最大化)は行われない。Windows.Formのバグか?

■ Date型について

Dateは参照型ではなくて値型(構造体)。System.DateTimeのエイリアス。 プリミティブ型なので、リテラルによる初期化を行えば定数の宣言も可能。

■ 変数へのNothing代入

参照型にNothingを代入した場合はそのままNothingになるが、値型にNothingを代入した場合(VB.NETでしかできないらしいが)、 既定値が設定される(値型なのでもちろんNothingにはならない)。 Integerとかだったら0、Dateだったら、# 01/01/0001 12:00:00AM #になる。

■ プリミティブ型について

プリミティブ型の定数を宣言できる。
値がリテラルの記述によって作成できる。123IはInteger型のリテラル。

■ 例外の処理

■ .NETのセキュリティ関連のドキュメント

■ WebRequest.Proxyプロパティについて

ProxyプロパティはデフォルトでGlobalProxySelection.Selectの設定を利用するらしく、この為、 特に何も設定しなくても自動で(IEで設定されている(?))Proxyを利用するみたい。 逆にプロキシを利用しない場合、明示的にGetEmptyWebProxyメソッドによる設定を行う必要がある。

■ プロキシの例外判定について

同じプロキシ設定を使用しても、WebRequest.ProxyとIE(IE6)では例外の判定の仕方が若干異なる (ここで言っている「例外」とは「プロキシの設定」ダイアログで設定されている例外のこと)。 IEではアスタリスクを展開した結果が対象URLと完全に一致すれば例外となるが、WebRequest.Proxyでは後方部分一致で一致すれば例外となる。

例:「例外」に設定しているドメイン名を「test.co.jp」とし、アクセスしようとしているドメイン名を「server.test.co.jp」とする。

・IE → 完全一致しないので、例外とはならない
・WebRequest.Proxy → 後方部分一致で一致するので、例外となる

上記の場合、例外に設定するドメイン名を「*.test.co.jp」や「*.co.jp」や「server.test.co.jp」などとすれば、 IEでも(WebRequest.Proxyでも)例外として扱われるようになる。 ちなみに、WebRequest.Proxyの方は本当に単純に後方部分一致で一致すれば例外となってしまい、 例えば上の例だと「st.co.jp」でも例外となってしまうので、その辺りは多少注意する必要があるかもしれない。

■ セキュリティ関係

■ スレッド、デリゲート、同期・非同期処理、イベント関連の記述がある雑誌記事

■ エラーの処理について

処理が続行できないエラーが発生した場合、ランタイムエラーではなくてアプリケーション的なエラーだとしても、 エクセプション(この場合はアプリケーションエクセプションが妥当か?)にエラー情報を格納してスローし、 最上位の関数でエラー発生時の動きを一括で管理したほうが良いような気がする(業務でやったことはまだないけど・・・)。 じゃないと、とてもじゃないけど全部のエラー箇所でエラー時の動きをコーディングするのは無理。 やることが一緒だったら冗長だし。 よく、パフォーマンスのためにランタイムエラー以外でエクセプションをスローするのはやめるべきって記述を見かけるけど、 少なくとも正常処理の続行が不可能なエラーは頻繁に起こることはないはずなので、エクセプションをスローしても特に問題はないはず。

■ DataSet,DataTableのディープコピー

DataSet,DataTableではディープコピー用のメソッドとしてCloneとCopyが用意されている。 Cloneはスキーマをコピーするだけでデータはコピーされない。 Copyはスキーマとデータの完全なコピーを行う。

■VB.NETでの論理演算子について

VB.NETでは論理演算子としてAnd, Orの他にもAndAlso, OrElseが用意されている。 And, Orに関してはVB6までと動作が変わっていないのでまあ良いとして、問題はAndAlsoとOrElse。 これも論理演算子で使い方は同じような感じなんだけど、こっちの演算子についてはショートサーキットを行った評価が行われる。 つまり、評価が確定した時点でその後の評価はショートカットされて行われない。 よって、単純に考えると余計な評価をしない分スピードが速くなる。 評価の過程で行われる処理が必要となるならショートカットが行われてはまずいが、 単純に評価だけが目的であるならショートカットを使わない理由がない。 と言うか、デフォルト(まあデフォルトと言うかAndとOrなんだけど)でショートサーキットが行われないのはVB.NETだけらしい。 他の(C#とか)言語ではデフォルトでショートサーキットされる(と言うか、ショートサーキットしない論理演算子自体ないっぽい)。 VB.NETだけショートサーキットされないのは恐らくVB6との互換性を考えてのことだと思われるので、 VB.NETで論理評価を行う場合はデフォルトでAndAlso又はOrElseを使うのが効率よいと思う (またその場合、ショートサーキットされ易い順番で式を記述するとなお効果的。ソースの可読性の方が優先順位的には上だとは思うけど)。 まあ、いつもより4文字増えちゃうのがちょっと痛いけど・・・。 逆に、評価の過程で行われる処理が必要であるならAndまたはOrを使わなければならない・・・と言いたいところだけど、 評価と必要処理を(ある意味)暗黙的にまとめてやってしまうのは、それはそれでどうかと思う。 処理自体が必要なんだったら評価の中で暗黙的にやらないで事前に呼び出しておくとか、 Ifで処理を分けるとか明示的にやった方が良いと思う。 そう考えると・・・AndとOrは不要か?

■ 配列やDataTableなどを返すメソッド

配列やDataTableなどに値をセットして呼び出し元に返すメソッドでは、基本的にNothingはセットしないようにする。 データなしの場合でもNothingをセットするのではなく、データなしのインスタンスを作成してセットして返す。 こうすることで呼び出し側ではNothingの可能性を考慮する必要がなくなり、 データがあるかどうかは単純にLengthやRows.Countが0かどうかで判断できるようになる。 この方法はマイクロソフトも推奨している・・・らしい。 フィールド変数とかも考え方としては同じかな?  コンストラクタでNewしたオブジェクトをセットして、Nothingの可能性をなくす。

■ Date型の値を内部的に使用する文字列に変換する場合

Date型の値を内部的に使用する文字列に変換する場合、 単純にToStringメソッドで変換すると現在のカルチャに合わせた書式で文字列が生成される。 通常、ユーザーに表示する必要のない、内部的に使用する文字列はカルチャの設定に依存してはまずい。 カルチャの設定に依存せず、常に一貫した書式を生成するにはインバリアントカルチャを使用する。 例えば、こんな感じ

strYYYYMMDD = dteTest.ToString("yyyyMMdd", CultureInfo.InvariantCulture)

上記のように記述すれば、常にこちらが期待する日付文字列(西暦4桁+月2桁+日2桁のyyyyMMdd)が取得できる。 ちなみに、 上記のフォーマットだとインバリアントカルチャを指定しなくても常に期待する"yyyyMMdd"文字列が取得できそうな気もするが、 実際には違うらしい。 例えば、ロケールの設定でカレンダーの種類を「和暦」にしたりすると、"yyyy"は西暦ではなく、和暦の数値を表現するらしい。 特に日付のカルチャについては色々とプロパティが存在するので、この問題は和暦とかに限った話ではないはず。

この問題については以下の掲示板で質問されている。
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=8991&forum=7

参考サイト

このカルチャ依存の問題に関しては、 日付型だけではなく数値型でも関係あることはあるのだが、 一般書式指定子("G")が指定されている場合はあまり関係ないかもしれない・・・けど、実際には関係あるんだろうなぁ。 まあとにかく、日付型に関してはカルチャへの依存が厳しいので、常にカルチャを意識するようにする。

補足:
「Dr.GUI .NET #5: .NET Framework における文字列」によると、 VB.NET組み込みのメソッドはほとんどインバリアントカルチャで処理しているらしい。 ”ほとんど”ってのがちょっと気になるけど。 まあどっちにしろFrameworkのメソッドで処理してる分には関係ないんだけど。
「.NET Framework では通常、書式設定メソッドに CultureInfo.InvariantCulture を渡さない限り、 言語に基づく変換と書式設定が実行されます。一方、Visual Basic .NET の組み込みメソッドは、 既定ではインバリアント カルチャに設定されていることがほとんどなので、注意が必要です。」

■ 基本データ型の変換(文字列絡み)

基本データ型を変換する方法として、Parse、ToStringインスタンスメソッドの使用、 またはConvertクラスの該当クラスメソッドを使用する方法がある。 どっちを使えばいいんだってことになるが・・・Convertクラスのメソッドは、 該当クラスのParse、ToStringメソッドを呼んでいるだけ、らしい。 なので、やっていることは全く一緒なんだけど、だったらわざわざConvertクラスを使う必要もない。 Parse、ToStringメソッドを使用する。

■ CLR(SSCLI)のソース

以下のサイトで、なんかCLR(SSCLI)のソースが見れる。

上のサイトのほうが細かいレベルまで載っているっぽい。

でも、Rotorってなんだろうと思って調べてみたら、 「Microsoft シェアード ソース CLI 実装」のコードネームという事で、 内容はと言うとどうやらCLRのソースと言うよりも、有志(?)がソースを起こしてるっぽい・・・(ほんとかよ)。 いやでも、マイクロソフトで配布されてるんだから有志ってことはないか・・・なので、CLRの実際のソースと言うわけではないらしい。 参考にはなると思うけど、このソースがそのままCLRで使われてると言うわけではないと言うことか・・・。 まあでも、現実的にはこれを参考にするしかないし、参考しても全く問題ないレベルなんだろうけど。

■ 数値型(Int32など)のToStringメソッドの実装について

Int32のToStringメソッドは、書式指定子が一般書式指定子の"G"だと、場合によっては数字そのままでは返ってこないっぽい・・・らしい。 まあ、(桁指定とかしないで)普通に"G"だけだったら大丈夫っぽいんだけど・・・。 逆に、数字そのままの文字列が欲しいんだったら、"D"を指定するべきなのかなとも思ってしまう。 まあでも、現実的には"G"でも問題ないんだろうなぁ・・・たぶん。

→マイクロソフトのサイトからダウンロードできるSSCLIのソースをちらっと見たところでは、 桁指定なしの"G"は"D"と同じ処理をしてるっぽい。

■ ユーザーメソッド内でエラーが発生した場合の例外について

取りあえず、引数がおかしい場合は ArgumentException系 で決まり。 で、問題は引数以外がおかしい場合。 MSDNのヘルプでInvalidOperationException を見ると、 「InvalidOperationException は、引数が無効であること以外の原因でメソッドの呼び出しが失敗した場合に使用されます。」 とあるので、引数以外のエラーは、取りあえず InvalidOperationException をスローすればいいか。 例えば、フィールドの値が変だった時とか。 まあ、要はSelect Case分岐とかで予想していないCase Elseでは全部 InvalidOperationException を投げとけよと。ほんとかなぁ。 でもまあ、何かしら例外を投げとくのは合ってるだろ。

■ SQLのWHERE句に設定する文字列の扱いについて

基本的に、シングルクォートはシングルクォート2つにエスケープしなければいけない。 後、LIKEによる部分一致検索に使用する場合は、 シングルクォートに加えてLIKEで使用するキーワード(%とか)も置換して無効化しなければならない。

また、上記の操作に加えて、場合によってはセミコロンとかのSQL文字列として特殊な意味を持つ文字も無効化しないとまずい。 ただこれは使用しているDBや環境によって異なるので、それぞれ適切な処理を行う。

まあ何だかんだ言いつつ、直接SQL文字列を生成して投げるよりかはパラメータ化した方がセキュリティ的にもパフォーマンス的にも有利。 書くのがちょっとめんどくさくなるかもしれないけど。

■ DateTime.ParseExactメソッドで変換元情報を一部省略した場合の動作

DateTime.ParseExactメソッドでは指定したフォーマットに基づいて文字列をDateTimeに変換するが、 年月日時分秒の全てを指定しなければいけないわけではなく、一部省略して解析することが可能である。 例えば、年月のみ指定した場合、呼び出し方は以下のように記述する。

例:
DateTime.ParseExact("200501", "yyyyMM", CultureInfo.InvariantCulture)

こうすると、少なくとも年月に関しては2005年1月のDateTimeが作成されるのだが、省略された情報に関してはどうなるのか。 ヘルプを見ると 「s に時刻だけを指定して日付を指定しない場合は、現在の日付が使用されます。 s に日付だけを指定して時刻を指定しない場合は、深夜 (00:00:00) が使用されます。」 とあり、実際の動きもそのようだ。 ただ、日付の一部を指定した時などの動きは記述されていないので、前述した年月のみの指定はどうなるのか。 試したところでは、以下のような感じで初期化されるっぽい。
年:システム日付の年
月:1
日:1
時刻:00:00:00

ただ、ドキュメントに記述されているわけではないので、 この動作を期待してコーディングするのもまずいかも (まあ、上記の初期化値が一番合理的な感じもするので、他の値に変わる可能性は低い気もするけど)。 確実にやるんであれば、一部省略する情報は明示的に適切な値を付加した方が良いと思う。 例えば、年月を変換するならこんな感じ。

例:
DateTime.ParseExact("200501" & "01", "yyyyMMdd", CultureInfo.InvariantCulture)

ん〜でも杞憂かなぁ。

■ Decimalを対象とした四捨五入

.NETのFrameworkには「四捨五入」を行う丸めは用意されていない。 また、浮動小数点数で数値処理を行うと丸め誤差などが発生しやすいが、 Mathクラスに用意されている丸めに関連したメソッドはほとんどがDoubleを基本としている (まあ、金額と言うよりかは数値演算の為のクラスなので、そうなるだろうけど)。 金額系の計算でDouble(浮動小数点数)を使うのは(桁数が足りないとかの理由がない限り)基本的にありえない。 また、最終的な値をDoubleで扱っていないとしても、計算途中でもDoubleが介入する演算はできるだけ行いたくない。 よって、計算途中に浮動小数点数(Double)を介さない為には、Decimalクラスに用意されたメソッドを使えばよい。

四捨五入の基本的な流れとしては、以下のようになる。

1. 指定した精度へ対象数値をべき乗する。 小数点1桁に四捨五入(小数点第2位を四捨五入)するのなら、10 ^ 1 = 10倍する。

2. 対象数値の符号方向へ0.5をプラスする(五入する)。 正数だったら0.5、負数だったら-0.5。

3. 小数点以下を切り捨てる。

4. 1の反対の処理を行う。 小数点1桁に四捨五入(小数点第2位を四捨五入)するのなら、10で除算する。

上記処理を.NETの機能を使って表現すると、以下のようになる。


Public Function Round(ByVal decValue As Decimal, ByVal intDecimals As Integer) As Decimal
    Dim decShift As Decimal

    decShift = 10 ^ intDecimals

    Return (Decimal.Truncate((decValue * decShift) + (Math.Sign(decValue) * 0.5D))) / decShift

End Function

ポイントとしては、1,4の処理にDecimalを使い、2の処理にMathクラスのSignメソッドを使い、 3の処理にDecimalクラスのTruncateメソッドを使っている点。

上記の方法で、あまり大きな数値でないなら正確な四捨五入処理ができると思う・・・。 .NETのDecimalは表現できる範囲が広いので(29桁)、個人的には桁数が足りなくなることはないと思う(もちろん科学計算とかは除く)。

■ パフォーマンスに関するドキュメント

かなりのボリュームだが、具体的なパフォーマンスの向上方法が記述されているので、必読。 ただ、こういうのを読んでて思うのは、同じマイクロソフトの方針でも人によってかなり違うことがあるなぁと言うこと。

Webアプリケーション開発技術大全で推奨されている方法とは全く逆の方法が推奨されていたりして、 結局どっちを使えばいいのか悩む(DataReaderの使用とか、Server.Transferの使用とか)。 ターゲットにしている内容が違うってのもあるけどね。 まあ、どちらかと言うとパフォーマンスの向上の方が優先度低いとは思うけど。

■ 例外をThrowする、しない場合やFor、For Eachの速度比較など

上記ページに載っているサンプルで速度を比較すると、実際の速度の違いを体感できる。 For、For Eachの方は、試した環境ではドキュメント内の記述とほぼ同様の3倍程度の差が出た。 ただ、「プログラミング Microsoft Visual Basic 2005 言語編〈上〉」では 「配列を操作するとしたら、通常はFor Each...NextループよりもFor...Nextループのほうが高速ですが、 コレクションを操作するとしたら、通常はFor Each...Nextループのほうが高速です。」と記述されている(比較結果未確認)。

例外のThrowに関しては、比較できない差が出た。 例外をThrowする方は、Throwした分だけ(かなりの)時間がかかったが、 Throwしない方は全く時間がかからなかった(このサンプルコードでは)。

これを見てしまうと、はっきり言って例外の方の速度はやばいかも。 短期間に数万回の例外を送出させることはあまり考えられないように思えるが、コードによってはやっぱりありえる。 特に、このサンプルコードじゃないけど、ループの中で例外を処理の一環として明示的に発生させている場合、 すぐに同じような状態になる気がする。 また、自分で明示的にThrowしてなくてもFrameworkのメソッド内で例外を使用している場合もあるので、 そういうメソッドを読んでいる場合は予期せず同じ状態になってしまうと思う。

StringとStringBuilderは、許容できない速度差が発生してる。 サンプルでは数万回のループ内で単純に文字列追加処理を行っているだけだが、StringBuilderの方はほぼノータイムの速度だが、 Stringの結合だととんでもなく時間がかかる。 サンプルでは5万回のループだが、Stringは3分半もかかった(StringBuilderは1秒以下)。 特徴的なのは処理回数を重ねるにつれて右肩上がりに処理時間も増加していること。 最初の方は1000回ループに1秒かかっていないのだが、その後どんどん処理時間が増えていき、最後の1000回ループは8秒かかっていた。 この結果を見る限り、ループ内でStringによる結合を行うのは無謀。

ASP.NETの場合は処理しているユーザー数分同時に発生することになるので、事はもっと深刻になる。 1ユーザー1秒のコストでも、100ユーザーが同時処理を行うのであれば(単純計算で)100秒のコストになる。

■ (ほぼ)既知のアペンド数の文字列連結を行う場合の速度について

例えば、あるSQLを発行するメソッド内でそのSQLが固定の場合、SQL文字列を生成するのにどのような方法を使えば良いのか。 SQLなんて短くて数行、長くても通常数十行だが、どのような方法で文字列を作成するのが効率が良いのか。

ここでは、例として"1"から"20"まで文字列結合するメソッドを作成し、 そのメソッドをループで呼んで時間を計測してみた。
試した方法

結果

上記方法による結合は、100万回呼んでもノータイム(コンマ1秒以下)。 まあ、1ステートメント結合以外の方法は何となくわかるが、1ステートメント結合も同様にノータイムなのはちょっとだけ意外。 でもまあ、本当に単純にリテラルを結合してるだけなので、最適化された際に1つのリテラルにまとめられたりしてるんだろう(未確認)。

ある程度予想はできたけど、差が出たのが

の結合方法。 Stringによる結合1及び2はどちらの結果もほぼ変わらず(誤差1,2%程度)。 もしかしたら吐かれてるILも一緒なのかもしれない(未確認)。 StringとStringBuilderは、もちろん差が出る。

結合する回数が20回ぐらいまではStringの方が速い。 結合する回数が30回を超えてくるとStringBuilderの方が速い。

ただ、当たり前だけど結合する回数が少なければ総処理時間自体が 短いので、場合によっては好みの問題になる気もする。 5回結合だと、100万回コールでStringが0.5秒、StringBuilderが0.7秒とかそういうレベル。この差をどう見るかだけど。 結合回数を100回まで増やせば、Stringが24秒、StringBuilderが12秒ぐらいの差は出た。

★結論
結合回数が2,30回程度までであれば、Stringの方がStringBuilderよりも速い。 ただ、その差は大きくても数割程度なので、場合によっては無視できる差だと思う。 結合回数が30回を超えるような場合は、StringBuilderの方が有利。 この場合、結合回数が増えれば増えるほど、差が顕著になる。 これについては一つ上の説明を参照。 ただ、はっきりと言えるのは、上記以外の方法を使えばノータイムだと言うこと。 速度を最優先させるのであれば、上記以外の方法(定数など)を選択する以外ない。 ただ、どの方法も可読性はかなり落ちると思う。 可読性をある程度保ちつつ速度を優先するのであれば・・・クラス変数またはインスタンス変数かな。 どちらも初期化は1回だし、初期化の仕方は可読性の高い方法(&=結合とか)が採用できる。 定数っぽく使用するから、それ考えるとインスタンス変数よりクラス変数の方がコスト的には良いかも。 特にASP.NETだとWebアプリで初回だけのコストになるはずなので、よりパフォーマンスが出る、気がする。 まあ、まだ仕事では使ってないけど。

■ IL関連の情報

■ 逆コンパイル

以下のツールで逆コンパイル可能。

■ プロセスが使用しているスレッドの監視

Win2000のCDに付属しているProcess Viewerでプロセスが所有しているスレッドが見れるらしい。 リソースキットに入っているQuick SliceだとスレッドIDもわかるっぽい。

■ Static, Sharedのスコープ

Sharedは共有メソッド(メンバ)の定義と言うことであまり悩むことはないと思うが(Sharedメソッドは常に型レベル)、 メソッド内でStatic宣言した変数はどのようなスコープになるのか。 結論としては、インスタンスメソッド内のStatic変数はインスタンス内で静的となり、 Sharedメソッド内のStatic変数は型レベルで静的となる(らしい。未検証)

■ ソリューションやプロジェクトの名前付け

(例えばソリューションの場合は)_SOLなどのようにひと目で種類がわかる名前付けを行う

■ 半固定情報のキャッシュ

半固定情報と言う言葉自体変なのだが、 どんなことを言っているのかと言うと基本的には固定的な情報だが変更された時に自動で変更内容が反映されてくれると嬉しい情報。 例えば、クラスのメソッドでそのクラスのIDみたいな情報としてクラス名(に何か固定文字列を付加してとか)を返すような場合、 一番簡単であんまり問題もない方法は単純に返す文字列をハードコーディングする方法。 クラス名なんて変更されることなんてほぼないはずなのでハードコーディングでも基本的に問題はないんだけど、 そのソースをコピペしてクラス名変えて使おうとか考えてると、 そのハードコーディング(定数とかかませてる場合でも基本は一緒)した箇所も一緒に修正しないと、まずい。 それを考えると、次に思いつく方法がリフレクションとかを利用して動的に文字列を生成して返す方法だが、 この方法使うと単純にコピペしても正しい動きをしてくれるが、(本来不要な)文字列の動的生成を行っている為、オーバーヘッドが発生する。 どっちもあまりよろしくないので、両者を勘案すると、 静的変数を用意して、初回時にのみ動的生成した値を格納して以降はその静的変数を使いまわすってやりかたが汎用性があると思う。

仮想アプリケーションルートパスとかを使用している場合も同じことが言えると思う。 ただこの場合、初期化するタイミングはWebアプリケーション起動時とかになると思うが (と言うか、この場合はそもそもHttpRequest.ApplicationPathの呼び出しにそこまでオーバーヘッドかかってないかもしれないが。まあ例えの話)。

情報の位置付けとしては、定数とconfigの間くらいかな(それもどちらかと言うと定数寄りの)。

■ ネストされた型の完全限定名

ネストされている型の完全限定名は、入れ子された型名をプラスで連結する。

■ 構造体とクラスの使い分け

構造体とクラスはできることはあまり変わらないのだが、どのように使い分ければよいか。 値型、参照型という違いを除けば、後は使い勝手次第となるのだが、 できることにあまり違いがないと言ってもクラスにできて構造体にできない事はそれなりにある。 基本的に構造体でなければ駄目な場合(そもそもそういう状況自体あまりないと思うが)にのみ、構造体を使用する。 構造体でなくても構わないのであれば、クラスを使用する。 両者の違い及び影響を把握していない、または確たる理由がないのに、 ただ単に「構造体でもできるから構造体でいいや」的な考えで構造体を採用するのは止めたほうが良い。 構造体は継承不可、パラメータなし非共有コンストラクタの定義不可なことに注意すること。 パラメータなし非共有コンストラクタの定義が不可なせいでデータメンバの初期値は既定値でしか初期化できない。

上記ドキュメント(値型の使用法のガイドライン)の構造体採用基準の中で、「プリミティブ型と同様に機能する」って言うのがポイントな気がする。 この例ではInt32を例にとってるけど、要はそういうことか。 VB業務アプリとかでは変数をグループ化するのによく構造体使ってたと思うけど、 そういう類の実装に.NETの構造体を使うのはやっぱり違うみたい。 構造体の採用を考える場合は、「構造体=Int32」の法則に照らし合わせてみる。 Int32みたいな感じで使うのでなければ、迷わずClassを採用。 どうでもいいけど「静的ファイナル定数」ってなんじゃらほい。

後、上記のガイドラインを守ればそういう風な実装はないかもしれないけど、構造体のメンバに参照型を定義した場合、 メモリの確保のされ方ってどうなるんだろ。この場合も参照型の実体はヒープに確保されるの?

■ Withステートメントの謎

Withステートメントで指定するオブジェクトは、Withステートメントが実行(?)される時点でインスタンスができてないと何か駄目みたい。 Withステートメントの中で、Withステートメントで指定しているオブジェクトのインスタンスを作成しても、 メンバにアクセスしようとするとヌルリファレンスエクセプションが発生してしまう。 この辺りの詳しい動きは、ILみないと分からないかも。

■ Withステートメントの謎(IL確認結果)

Withステートメントの内部動作をILにて確認。まず、VB.NETのコード。 既にコードに結果が出てしまっているけど、Example4_1がWithを使用したコードで、Example4_2が同じ動作をWithなしで表現したコード。

    Sub Example4_1()

        Dim al As ArrayList

        With al
            al = New ArrayList
            al.Clear()

            .Clear()
        End With

    End Sub

    Sub Example4_2()

        Dim al As ArrayList
        Dim alTemp As ArrayList

        alTemp = al

        al = New ArrayList
        al.Clear()

        alTemp.Clear()

        alTemp = Nothing

    End Sub

上記コードをコンパイルしたILコード

.method public static void  Example4_1() cil managed
{
  // コード サイズ       23 (0x17)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Collections.ArrayList al,
           [1] class [mscorlib]System.Collections.ArrayList VB$t_ref$L0)
//000099:         With al
  IL_0000:  ldloc.0
  IL_0001:  stloc.1
//000100:             al = New ArrayList
  IL_0002:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0007:  stloc.0
//000101:             al.Clear()
  IL_0008:  ldloc.0
  IL_0009:  callvirt   instance void [mscorlib]System.Collections.ArrayList::Clear()
//000102: 
//000103:             .Clear()
  IL_000e:  ldloc.1
  IL_000f:  callvirt   instance void [mscorlib]System.Collections.ArrayList::Clear()
//000104:         End With
  IL_0014:  ldnull
  IL_0015:  stloc.1
  IL_0016:  ret
} // end of method Module1::Example4_1



.method public static void  Example4_2() cil managed
{
  // コード サイズ       23 (0x17)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Collections.ArrayList al,
           [1] class [mscorlib]System.Collections.ArrayList alTemp)
//000113:         alTemp = al
  IL_0000:  ldloc.0
  IL_0001:  stloc.1
//000114: 
//000115:         al = New ArrayList
  IL_0002:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0007:  stloc.0
//000116:         al.Clear()
  IL_0008:  ldloc.0
  IL_0009:  callvirt   instance void [mscorlib]System.Collections.ArrayList::Clear()
//000117: 
//000118:         alTemp.Clear()
  IL_000e:  ldloc.1
  IL_000f:  callvirt   instance void [mscorlib]System.Collections.ArrayList::Clear()
//000119: 
//000120:         alTemp = Nothing
  IL_0014:  ldnull
  IL_0015:  stloc.1
  IL_0016:  ret
} // end of method Module1::Example4_2

まあ、ILを見てもらえば分かると思うけど・・・Example4_1とExample4_2は全く同じILコードになっている。 With使う方のILのlocals部を見てもらえば分かると思うけど、With用の内部ローカル変数が定義されている。 ポイントはここかな。ILを追ってもらえばわかるけど、Withに入った時点で、まずこの内部ローカル変数に本体の参照をセットしてる。 なので、Withに入る前に本体の参照がNothingだと、 Withの中でWithローカル変数にアクセスした時にNullReferenceExceptionが発生してしまう。仕組みが分かれば当たり前の結果だね。 Example4_2では自分で一時ローカル変数を用意して使ってるけど、With使った時はこれと同じ事を内部的にやってるだけの話。 蛇足だけど、Withステートメント終了した時点でちゃんと内部一時ローカル変数にNothingをセットしてるみたい。行儀が良いね。

■ 同一オブジェクトを別オブジェクトにセットする時の注意点

当たり前だが、.NETは完全にオブジェクト指向なので、オブジェクトを中心に話が進められる。 オブジェクトは参照型なので、何も意識しないで値型のように設定していると、 同じオブジェクトを複数の別オブジェクトに設定してしまう可能性がある (もちろんここではオブジェクトのコピーが設定されていると想定しているのが前提)。 これは基本的にどんな場面でも言えることなのだが、一応例を挙げると、DropDownListにはListItemをAddすることができるが、 この時複数のDropDownListに同じListItemをそのままAddしていると、同じ項目がDropDownListに設定されるが、 「本当に同じオブジェクト」をそれぞれのDropDownListから参照していることになる。 つまるところどういう事かと言うと、この状態でいずれかのDropDownListのSelectedIndexを変更すると、 全てのDropDownListのSelectedIndexが同じように変更される。 変更されると言うか、変更されたオブジェクトを参照しているので、変更されたようにみえる。 何でこんな事になってしまうかと言うと、DropDownListのプロパティとしてではなく、 ListItemのプロパティとしてSelectedプロパティを持っているから。 一つのオブジェクトにしか設定していなければ問題ないが、複数のオブジェクトに上記の設定を行ってしまうと問題がでる。 同じ画面内でも問題だが、アプリケーション全体で共通のオブジェクトをキャッシュしていて、 それを各画面で使用しているような状況ではもっと深刻。 この問題、仕組みは理解していても気づかないうちにやってしまっている場合があるので、 出来る限りそういうコードが記述されないような状況にもっていくのも(どちらかと言うと後ろ向きな考えだが)必要だと思われる。 例えば、アプリケーション共通のデータソース(上述した例でいうところのListItemのコレクションとか)は直接使用できないようにして、 設定用の共通関数を用意するとか。そういう対策をしておけば、 少なくとも共通データに関しては上述した心配をしないで済む(し、もし上述した問題が発生したとしても共通関数の修正のみで済む)。

■ キャストの使い分け

型が明らかに分かっている場合はDirectCastで問題ないが、明らかでない場合はCTypeを使うのが無難。 例えば、数値系のキャスト(特にリテラル)は型文字を使用して型を指定する事も可能だが、 どちらかと言うとそこまでする必要はない気がする。 そういう場合、柔軟性やメンテを考えると、CTypeかなぁ。 ちなみに、「プログラミング Visual Basic 2005 言語編」にはDirectCastとCTypeのパフォーマンス差はごくわずかと記述されている(動作未確認)。

■ SPREAD for .NET Web Forms Ed.のデザイナが立ち上がらない時・・・

取りあえず、GACを確認。 デザイナのアセンブリが登録されてない場合は、インストール時のアセンブリ登録に失敗している可能性がある。

C:\Program Files\Common Files\FarPoint Technologies\SpreadNETWebv2\Bin の下にインストール時にアセンブリをGACに登録する為のバッチがあるので、バッチの内容を確認。 ファイルのパス指定がダブルクォートで囲まれてなかったりすると、エラーになっちゃうかも。 実際にバッチを起動するのが手っ取り早い。 ダブルクォートで囲んでうまく登録できたんだったら、 ついでにunregのバッチもアンインストール時に備えてダブルクォートで囲んでおくと安心かも。

■ Integer.ParseとかLong.Parse

Integer.ParseとかLong.Parseは実数をパースしようとすると例外が発生するので注意。

■ 定数の管理方法(一例)

VSのマクロのやつだと Constants.vsWindowKindMacroExplorer みたいな感じで管理されてる・・・。

定数のまとまりは「Constants」クラスで、種別の種類は「xxxKind」で、種別の各項目を「xxxKind」の後に記述してるみたい。

Constantsクラスの下にクラス定数メンバとして宣言されてるみたい。 まあ確かに、定数クラスを用意するのはわかり易いと思う。

■ マスタデータを取得する共通クラスとかで使うDataSet

マスタデータを取得する共通クラスとかを作成した場合、 取得したデータの入れ物は普通のDataSetよりも型付DataSet使った方が良いかな?  各画面内での処理はともかく、共通であればそれぐらいの労力をかける見返りがあるはず。

■ VB.NETでデザイン画面でコントロール名を変更した場合

デザイン画面でコントロール名を変更した場合、コードにそのコントロールのイベント処理を入れていると、 Handles句が削除されるので、注意。

■ キャストガイドライン

「異なるドメインから値をキャストしないようにします」 「Int32をStringにキャストすることは、これらが異なるドメインにあるため無意味です」だそうで。 無意味ってこれまた凄い断言だな。 VBで言うところのキャストは「C〜」キーワードだと思うけど、 CType以外の型変換キーワードは思いっきり異なるドメインの値も対象にしている気がするが。 まあ、要は可能だけど無意味です、もといお勧めしませんぐらいの感じだろうけど。 数値文字間の正統な変換方法は、ToString及びParseメソッドを使えと言うことだろう(たぶん)。

■ Active Repostsでrpxファイルとvbファイルの紐付けがおかしくなった時

基本的にrpxとvbファイルの紐付けはプロジェクトファイル内に定義されているので、 紐付けがおかしい場合はプロジェクトファイルの定義を確認してみる。

■ 変数のスコープ

変数のスコープは、その変数の宣言がそのスコープのブロックのどこで宣言されているかに関わらず、 そのスコープに入った時点でそのスコープ内で宣言されている全変数が有効になる。 この動きは、デバッグ実行してローカルウィンドウやウォッチウィンドウで確認できる。 また、上記の動きの結果、恐ろしい(?)事に変数が宣言されている箇所を通過する前にその変数を使用することが可能となる。 ただ、当たり前だがソースコードとしてそのようなコードを記述することは出来ない(怒られてコンパイルできない)。 この動きはデバッグモード固有の動きかもしれないが、詳細については未確認。と言うか、リリースモードだと上記の動きの 確認のしようがないので、どうにもならない。 ILを比較すればもしかしたら何かわかるかもしれないけど・・・。

■ Forループの中とかで宣言した変数の初期化について

Forループの中とかで宣言された変数って、Forループ抜けてもスコープを完全には抜けない?  Forループ抜けた後にまた宣言の箇所に戻ってくると、 スコープ(変数)が有効になると同時に以前格納されていた値が初期値として格納されている・・・。 スコープ自体はもちろんForループの中でしか有効じゃないんだけど、 変数のオブジェクトがスコープ抜けても破棄されていないっぽい。 んでもって、次にそのスコープが有効になった時、以前使用したオブジェクトが復活するっぽい。 結果、変数の宣言と同時に初期化してないと(変数の宣言だけを行っていると)、 変数の値が以前の値のまま(初期化されないまま)使用されてしまう。 変数の宣言と同時に明示的に初期化しなくても基本的には既定値で初期化された状態になるので、 宣言と同時に既定値で明示的に初期化してもしなくても結果は同じなのだが、 宣言を行っている箇所がループ内の場合、2回目以降の処理で話が変わってくる。 初回はどちらの場合も既定値で初期化されているが、2回目以降になると、 明示的に初期化されている場合はもちろん初期化された状態になるが、 明示的に初期化していない場合は「以前のスコープで格納されていた値が格納された状態で使用可能になる」。 もしかしたらデバッグモードだけの現象かもしれないが(未検証)、例えそうだとしても上記の動きは回避するべき。 ループの中で宣言した変数は、 使用する前に必ず明示的に初期化する必要がある(とりあえず宣言と同時に初期化するのがわかりやすいかと思う)。

■ VBの型変換とFCLの型変換

まあ、VBの型変換使わなければ関係ない話なんだけど・・・VBの型変換とFCLの型変換は同じ結果になるとは限らない。

■ CTypeを使った配列の型変換

CTypeは配列の型変換も行うことができる。 但し、Option StrictがOnの場合、(当たり前だが)明示的な変換を行う必要があり、ToArrayメソッドの引数に変換先の型を指定する必要がある。

例:Stringが格納されたArrayListからString配列に変換

CType(alString.ToArray(Type.GetType("System.String")), String())

■ 固有カルチャを意識したコーディングについて

普通(?)、国際化対応などを意識しない限り、カルチャを考慮したコーディングはしないと思われる。 ニュートラルカルチャ(いわゆる、言語)については何らかの問題で変わってしまう事なんて考えられないが、 固有カルチャ(いわゆる、数値、通貨、時刻、日付)についてはコントロールパネルの設定で簡単に変えられる、変える可能性がある、 何かの問題で知らないうちに変わってしまう可能性があるため、 固有カルチャに関しては最初から考慮してコーディングした方が何かあった時の事を考えるとベター。 稼動端末がそのシステムの為にのみ用意される(且つ、他のシステムが入っていない)と言う事であれば、 最悪自分のシステムが動くような設定にコントロールパネルで変更してもらうと言う事でも対応できるかもしれないが、 もしそれで対応できるとしても最後の手段だろう。 通常は同一端末で他のシステムも何かしら動いているだろうから、どちらのシステムも固有カルチャの設定に依存していて、 且つ正常動作する設定が異なるのであれば、事実上共存できないことになってしまう。いや、マジで。 この辺りの問題については、何も考えてないと実際にシステムが稼動し始めてから出てくる可能性があるので、注意。

■ Optionalキーワードの使用

Optionalキーワードを使用することでメソッド呼び出し時の引数指定の省略が可能となるが、 これはよっぽど特殊な場合でない限り使わない方が混乱が起きにくいと思う。 それなりの頻度で指定する必要が出てくる引数をOptional指定してしまうと、 気づかないうちに引数を省略した状態で使用してしまって無用な混乱を招く恐れがある。

■ クラスのファイルへの定義

基本は1クラス1ファイルで、ファイル名=クラス名。 定数とかの定義用のクラスを用意するとしてもファイル名と同じ名称のクラスを用意して、必要であればその中に入れ子で各クラスを定義する。 ファイルの直下に並列して複数のクラスが定義されていると、ファイルとクラスの関係が分からなくなってくる。

■ .NETアプリケーションの起動プロセスについて

よく、システムで一度でも.NETアプリケーションを起動すれば次回以降の.NETアプリケーションの起動は早くなるみたいな記述を見かけるけど、 具体的にどうしてそうなるのかよく分からない。 同じアプリ内であれば分かるんだけど、アプリケーション(プロセス)を終了しても.NETのランタイムはメモリ上に残っているのか?  アセンブリはドメインにロードされるから、ドメインがアンロードされれば全て破棄されるのではないのか?  ドメイン中立だったとしてもこの場合は関係ないよね?  プロセスが終了すればドメイン中立だったとしてもアンロードされるはず(?)だし・・・。 いったい全体、何がどうなって早くなるのか?

■ ファイル名(プロジェクト名)の付け方

ファイル名やプロジェクト名は、特に理由がない限り連番による名称管理はしない方が無難。 本当に単純に増加させていくだけであるのなら問題ないが、 何かの規則に沿って番号を振る順番が決まっていたりする場合は問題がある。 そのような場合、ファイルの削除や追加が発生した時に、他のファイルの番号をずらす必要が出てくる。 単純にファイル名を変更するだけでもそれなりの問題が出てくるが、VSSを使用している場合もっとやっかいなことになってくる。 出来ないと言う事ではないが、めんどうなのは確かなので無理に意味のない作業を増やす必要はない。 具体的な名称変更方法についてはマイクロソフトの資料を参照・・・と言いたいところだけど、 まともな状態にするのであればこの資料に書いてある手順を実行するだけでは駄目なので注意。 この方法で変更できるのは「表面的な」名称だけ。 普通、クラス名=ファイル名とかなっているはずなので、 クラス名やaspxファイルのCodebehind、Inherits設定なども全て手動で正しく変更しなくてはならない。

■ 変数の有効期間とスコープ

最初に言葉の定義を確認しておくと、変数の有効期間とは「変数が値を保持している期間」で、 変数のスコープとは「名前に修飾子を付けずに要素を参照できる全コードの範囲」である。 両者については同じようなものとして捉えられ易いが、実際には異なるので注意が必要となる。

で、今まで変数って言うのは宣言した位置からブロックを抜けるまでが有効な範囲だと思っていたけど、で、 ある意味これはこれでそうなんだけど、内部的に変数の領域を確保するタイミングは違うらしい。 つまり、上記の例で考えれば、宣言されたタイミング(位置)で初めて変数の領域が確保され、 ブロックを抜けた時点でスコープからも外れる感じがするけど、 プロシージャの中で宣言されている変数(ローカル変数)はどのブロックのレベルで宣言されているか、 どの位置で宣言されているかに関わらず、「プロシージャの中に入ったタイミング」で全ての変数の領域確保、 及びその型の既定値による値の初期化が行われる。

で、スコープの方は「ブロック内で変数を宣言した場合、その変数はそのブロック内でのみ使用できます」とヘルプに記述されているので、 プログラムで実際に使用できるのは、該当ブロック内でのみ。 この「ブロック内」と言うのもちょっとやっかいで、 ここで言っている「ブロック内」とは本当に単純にそのブロックの中で使用できると言う事を言っている。 つまり、該当ブロック内で変数を宣言している位置とその変数をブロック内で使用できる位置は全く関係がないと言う事を暗黙的に示唆している。 つまり、極端な話ブロックの最後で宣言した変数をブロックの最初で使用するのも全然構わないと言う事。 ただ、一応内部的にはそのような仕様らしいのだが、宣言されている位置よりも前の時点で使えるようになると分かりづらいので(?)、 コンパイラによっては(VB.NETなど)コンパイルエラーとなる。 宣言より前で使えるコンパイラもあるらしい(?未確認)。

つまり、一番低い(広い)レベルは「有効期間」であり、それがそれよりも狭い「スコープ」によって隠蔽され、 更にそれよりも狭い「コンパイラによる制限」によって隠蔽されている。 もちろん実際のプログラムで従わなければいけないのは、最終的な「コンパイラによる制限」のレベル。

コンパイラによる制限のレベルが一番身近で、ある意味一般的な制限に見えるため、 「有効期間」「スコープ」もその有効範囲については「コンパイラによる制限」のレベルと同じように考えてしまうが、 実際にはそれぞれ違うので注意が必要となる。

ちなみに、「スコープ」レベルの内部動作については、VSのデバッグ用の[ローカル]ウィンドウによっても確認することができる。 ローカルウィンドウは「現在のスコープに含まれるすべてのローカル変数」が対象なので、 プロシージャの開始位置にブレークをはって止めてからローカルウィンドウを確認すると、 その時点でスコープ対象となっている全ての変数が既定値で初期化された状態で存在していることが確認できる。 ちなみにこの状態でも「コンパイラによる制限」は健在なので、 該当変数の宣言を過ぎるまでは値の変更は行えない(・・・って書いたけど、VS2005だと宣言位置より前でも変更できるな・・・。 VS2003で確認した時は駄目だった気がするんだけど・・・動作が変わったのかもしれない)。 同じようにブロック変数についても、ブロックに入った時点でそのブロック内の全ての変数がスコープ対象となることが確認できる。

「有効期間」についての確認はたぶんIL見ないと分からないと思う(IL確認結果は後述)。

スコープと内部的な有効期間が違うので、場合によってはコンパイラから変かなと思われる指摘を受けたりするが、 上記の仕様によるせいだと思われる。

上記ドキュメントの中のメモに「スコープがブロック内に制限されている変数でも、有効期間はプロシージャ全体の有効期間と同じです」 と言う記述を見つけることができる。要はそういうこと。

また、例えば以下のようなコードの場合、

Sub Example()

    Do
        Dim objNoInitialized As Object
        Dim objInitialized As New Object

    Loop While False

End Sub

スコープを無視すれば本質的には以下のコードと同等という事か?(要IL確認)

Sub Example()
    Dim objNoInitialized As Object
    Dim objInitialized As Object

    Do
        objInitialized = New Object

    Loop While False

End Sub

一緒だと仮定して・・・最初のコードだとループの中で毎回新たにオブジェクトが定義(作成)されるイメージを受けるけど、 実際には新たなオブジェクトなんて作成されていないことになる(作っていなければ)。 上記の例だとobjNoInitialized変数が実質見初期化でループ内で使いまわされる事になるので、 場合によっては問題が発生すると思われる(定義した直後は値がNothingなどの既定値だと仮定してコーディングしているとか)。 なので、例えその型の既定値が設定されていれば良いとしても、 常に変数宣言と初期化(宣言時に入れる値がないんだったら既定値)を同時に行うようにコーディングした方が余計な心配が減る。 この事象は特にループ内で使用する変数に影響するが、いちいちループかどうか判断して初期化してるとミスが出ると思われるので(コピペとか)、 記述する場所は特に考えずに常に初期化するほうが問題ないかと思われる。

まあ、この辺りは前述した「Visual Basic におけるスコープ」のメモにも書いてあるんだけど。

■ 変数の有効期間とスコープ(IL確認結果)

上記の内容を確認するため、実際に出力されたILを確認してみた。

まず、「スコープ」の検証。 以下は検証用のVB.NETのコード

    Sub Example1_1()

        Do
            Dim objNonInitialized As Object
            Dim objInitialized As New Object

        Loop

    End Sub

    Sub Example1_2()
        Dim objNonInitialized As Object
        Dim objInitialized As Object

        Do
            objInitialized = New Object

        Loop

    End Sub

次に、上記のコードをコンパイルして出力されたILのコード(ILDASM使用)

.method public static void  Example1_1() cil managed
{
  // コード サイズ       17 (0x11)
  .maxstack  1
  .locals init ([0] object objInitialized,
           [1] object objNonInitialized)
//000007:     Sub Example1_1()
  IL_0000:  nop
//000008: 
//000009:         Do
  IL_0001:  nop
//000010:             Dim objNonInitialized As Object
//000011:             Dim objInitialized As New Object
  IL_0002:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0007:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000c:  stloc.0
//000012: 
//000013:         Loop
  IL_000d:  br.s       IL_0002
//000014: 
//000015:     End Sub
  IL_000f:  nop
  IL_0010:  ret
} // end of method Module1::Example1_1



.method public static void  Example1_2() cil managed
{
  // コード サイズ       17 (0x11)
  .maxstack  1
  .locals init ([0] object objInitialized,
           [1] object objNonInitialized)
//000017:     Sub Example1_2()
  IL_0000:  nop
//000018:         Dim objNonInitialized As Object
//000019:         Dim objInitialized As Object
//000020: 
//000021:         Do
  IL_0001:  nop
//000022:             objInitialized = New Object
  IL_0002:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0007:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000c:  stloc.0
//000023: 
//000024:         Loop
  IL_000d:  br.s       IL_0002
//000025: 
//000026:     End Sub
  IL_000f:  nop
  IL_0010:  ret
} // end of method Module1::Example1_2

ILの見方がよく分からない人のために軽く説明をしておくと(って俺もよく分かってないけど)、 「//」はILのコメント文。今回はデバッグビルドなので色々とコメントも出力されてます。 「IL_〜」の部分が実際に処理を行っているコードなんだけど、 ILはスタックを中心にした処理になるので、スタックに値を入れたりスタックから値を出して使ったりって命令が書かれてます。 一応、リリースビルドだと以下のILコードが出力されます。

.method public static void  Example1_1() cil managed
{
  // コード サイズ       13 (0xd)
  .maxstack  1
  .locals init ([0] object objInitialized,
           [1] object objNonInitialized)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  br.s       IL_0000
} // end of method Module1::Example1_1



.method public static void  Example1_2() cil managed
{
  // コード サイズ       13 (0xd)
  .maxstack  1
  .locals init ([0] object objInitialized,
           [1] object objNonInitialized)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  br.s       IL_0000
} // end of method Module1::Example1_2

デバッグビルドの方が色々とコメントが付いてて説明しやすいかと思ったけど・・・リリースビルドの方がシンプルだからこっちでいいか。 で、上のリリースビルドのILを見てもらうと、少なくともこれだけは分かるはずだ。 「内容が全く同じ」って事が。(ILの)コードサイズすら同じだよ。 とまあ、これで「Example1_1」と「Example1_2」をコンパイルした結果が同じっていうのは分かったけど、 一応やってることの詳細を説明すると以下のようになる。

まず、「.maxstack 1」は、このメソッドで使用するスタックのサイズを決定している(らしい)。 まあ、今回はあまり関係ない。で、次。「.locals init」なんだけど、これが所謂「ローカル変数の領域確保及び初期化」命令です。 VBのコードだと、一応Doループの中で宣言したりとかしてるんだけど、ILからしてみれば宣言している場所は全く関係ないみたいですね。 メソッド内のローカル変数は(宣言位置に関わらず)全てこの最初の「.locals init」命令で領域確保と初期化が行われます。 「有効期間はプロシージャ全体の有効期間と同じです」と言うのはILがこういうことになってるからなんだね。 次からが実際の処理・・・と言っても、たったの4行しかないけど。あまりにも短いので説明する必要ないとは思うけど・・・ 「IL_0000」はObjectのインスタンスを作成してます。「IL_0005」は・・・よく分からない(おい)。 まあ、内部的に必要な処理なんでしょう。 「IL_000a」でローカル変数[0](=objInitialized変数)に生成したObjectインスタンスをセットしてます。 「IL_000b」で条件分岐を行ってます。この場合無限ループなので、単純に「IL_0000」にジャンプするだけですね。 これで、終了。 (VB.NETを視点とした時の)要点だけまとめると、「ローカル変数は宣言位置に関わらず全てメソッド先頭で領域確保及び初期化が行われる」です。 ILだけ見るとスコープも何もあったもんじゃないね・・・。まだ先は長いので、次。

以下のコードは、「ブロック内で宣言した変数はブロックを抜けた時点でスコープを抜けるから、 該当ブロックの後ろで同じ変数名を使っても大丈夫だよね」をやろうとしたところ。 何かうまくいきそうな気もするけど、実際にはコンパイルエラーになります(ので、以下の例ではコメントアウトしちゃってます)。 ここでの問題点は2つ。1つ目は「スコープが及ぶ範囲は宣言している位置によらず、所属しているブロック以下が全てスコープとなる」です。 なので、メソッド終了前に宣言されている「objInitialized変数」のスコープは、 この変数が所属しているブロック、つまりメソッド全てがスコープとなります。 問題点2つ目、「変数のスコープは名前に修飾子を付けずに要素を参照できる全コードの範囲」なこと。 ローカル変数とインスタンス変数、クラス変数などであれば、同じ変数名を使用したとしても「修飾子」による特定が可能だが、 ローカル変数同士では修飾子もクソもないので、 同じ変数名が使用された場合スコープが被っている箇所ではどの変数を指しているかの特定ができない、 ので、スコープが被ることは許されない(と推測)。

    Sub Example1_3()

        Do
            Dim objNonInitialized As Object
            Dim objInitialized As New Object

        Loop

        'Dim objInitialized As New Object

    End Sub

次、同じローカル変数名でもスコープが被っていない時のコード

    Sub Example1_4()

        Do
            Dim objNonInitialized As Object
            Dim objInitialized As New Object

        Loop While False

        Do
            Dim objNonInitialized As Object
            Dim objInitialized As New Object

        Loop While False

    End Sub

この場合は同じローカル変数名を複数箇所で宣言していても、スコープがかぶっていないので問題ない。 実際の変数領域も別々に用意される。以下は上記コードをコンパイルしたILコード。

.method public static void  Example1_4() cil managed
{
  // コード サイズ       23 (0x17)
  .maxstack  1
  .locals init ([0] object objInitialized,
           [1] object objNonInitialized,
           [2] object V_2,
           [3] object V_3)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0010:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_0015:  stloc.2
  IL_0016:  ret
} // end of method Module1::Example1_4

変数名は変わってしまっているが、locals部で4つのローカル変数が定義されている事がわかる。

次、「有効期間」の検証。ここまでの例を見てもらえばもう分かるとは思うが、 以下のようなコードを書いて「intInLoopJはjのForループの中で宣言しているので、 ここを通るたびに領域確保しなおされて、既定値(=0)が再セットされてるはずだよね」 とか思ってると、大間違いという事になる。 jのForループどころか、一旦jのForループを抜けたとしてもintInLoopJは有効な状態となっている。 このメソッドを終了するまでが有効期間となる。 既定値で初期化されていることを前提とするならば、変数の宣言と同時に必ず既定値で初期化する。

    Sub Example2_1()

        For i As Integer = 0 To 1
            For j As Integer = 0 To 1
                Dim intInLoopJ As Integer

                Debug.Print("i={0}, j={1}, intInLoopJ={2}", i, j, intInLoopJ)

                intInLoopJ += 1
            Next
        Next

    End Sub

実行結果

i=0, j=0, intInLoopJ=0
i=0, j=1, intInLoopJ=1
i=1, j=0, intInLoopJ=2
i=1, j=1, intInLoopJ=3

最後、スコープの範囲の確認を、ILレベルじゃなくてVSで確認してみる。 以下のコードをステップ実行する。

    Sub Example3()

        Do
            Dim intBlockValA As Integer
            intBlockValA = 111

            Dim intBlockValB As Integer
            intBlockValB = 222

        Loop

    End Sub

まず、ブロック内に進入する前で止める。ローカル変数ウォッチにはまだ何も表示されていない(対象変数のスコープ外)。

ブロックに進入した直後。intBlockValB変数の宣言前にも関わらず、ローカルウォッチにはどちらの変数も表示されている(対象変数のスコープ内)。

この状態で値の変更が可能。でも、確かVS2003では宣言箇所を通過するまでは駄目だった気もするけど・・・ちなみに、これはVS2005での動き。

VB.NETのコードだけ見て考えるとちょっと理解しづらい動きだけど、ILではどういうコードになっているのかを考えれば、理解できると思う。

■ 変数宣言時の初期化方法について

VB.NETでは変数宣言時の初期化方法は2種類ある。 「Dim 変数 As クラス = New クラス」と「Dim 変数 As New クラス」だが、内部的な動作は同じなのか、違うのか。 以下は検証用のVB.NETコード

    Sub Example5_1()
        Dim obj As Object
        obj = New Object
    End Sub

    Sub Example5_2()
        Dim obj As New Object
    End Sub

    Sub Example5_3()
        Dim obj As Object = New Object
    End Sub

ILコード

.method public static void  Example5_1() cil managed
{
  // コード サイズ       12 (0xc)
  .maxstack  1
  .locals init ([0] object obj)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  ret
} // end of method Module1::Example5_1



.method public static void  Example5_2() cil managed
{
  // コード サイズ       12 (0xc)
  .maxstack  1
  .locals init ([0] object obj)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  ret
} // end of method Module1::Example5_2



.method public static void  Example5_3() cil managed
{
  // コード サイズ       12 (0xc)
  .maxstack  1
  .locals init ([0] object obj)
  IL_0000:  newobj     instance void [mscorlib]System.Object::.ctor()
  IL_0005:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
  IL_000a:  stloc.0
  IL_000b:  ret
} // end of method Module1::Example5_3

結果としては、全部全く同じILとなった。 まあ、ILのローカル変数宣言は全部メソッド開始時に行われることを考えれば当たり前と言えば当たり前の結果か。 そんな感じなので、どの初期化方法を使うかは好みの問題。どれ使っても一緒。

▲画面上へ