2013年7月2日

Application.Exit() / Application.Restart() の注意事項 #1
フォームクローズのキャンセル

System.Windows.Forms 名前空間の Application.Exit() は、MSDNに記載してあるとおり、開いているフォームのいずれかの FormClosing イベントでクローズをキャンセル(FormClosingEventArgs.Cancel = true)した場合、アプリケーションは終了されない。
Application.Restart() に至っては、キャンセル時は、アプリケーションは終了されず、新たなアプリケーションが立ち上がる。まあ、MSDNに「主に ClickOnceアプリで使え」とあるので、Formアプリはあまり考慮されてないのかもしれない・・・。
※Application.Restart() の処理内容は簡単で、Formアプリの場合は Application.Exit() と同じ処理をした後、Process.Start()で自身の exe を起動するだけである。起動時のパラメーターも元のプロセスと同じ内容が使用される。(ClickOnceアプリや URL クリックで起動したアプリの場合は少々異なる。)

問題となるケース

フォームクローズをキャンセルする例として、下記のようにモードレスウィンドウの閉じる処理を、Hide() で行う場合がある。
この場合に、Application.Exit()/Restart() を使うと、終了しなかったり、アプリケーションが2つ立ち上がったりする。

public class ModelessForm : Form
{
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        e.Cancel = true;
        Hide();
        base.OnFormClosing(e);
    }
// 以下省略
}

解決手段

おそらく、最も簡単で合理的な解決手段は、FormClosingEventArgs.CloseReason でクローズ理由を判定することである。
下記では、ユーザーによるUI操作で閉じられる場合のみ、キャンセルするようにしている。
そうすれば、Application.Exit()/Restart() の問題は起こらなくなる。(ユーザーのUI操作で、イベント内に記述したApplication.Exit()/Restart()が呼ばれても、CloseReason は UserClosing にはならない。)

public class ModelessForm : Form
{
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        if( e.CloseReason == CloseReason.UserClosing ){
            e.Cancel = true;
            Hide();
        }
        base.OnFormClosing(e);
    }
// 以下省略
}

他の解決手段として、Application.Exit()/Restart() を自分で実装することもできる。
Application.OpenForms すべてに対して、Close() -> Dispose() するなど。
※再起動処理の挙動は、キャンセル無視の強制終了で新プロセス起動、または、キャンセル時は二重起動せず例外発生、など。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

2013年6月25日

VB.NET のダブルコーテーション(二重引用符)

VB.NET のダブルコーテーション(二重引用符)は、 全角「“”"」 半角「"」すべて半角として解釈される。
※Visual Studio のエディタは全角ダブルコーテーションを自動で半角にするため、下記を試す場合はテキストエディタ+vbcで

'文字列リテラルの開始と終了も全角使用可能(全角半角の混在も可能)
'文字列リテラル内の全角ダブルコーテーションはエスケープが必要
Dim strings = {”a”, “b“, "c", "””", "““", """"}
Console.WriteLine(String.Join(",", strings)) '結果「a,b,c,",","」

'文字リテラルも同様
Dim chars = {”a”c, “b“c, "c"c, "””"c, "““"c, """"c}
Console.WriteLine(New String(chars)) '結果「abc"""」

そのため、文字列リテラルでは、全角ダブルコーテーションが表現できない。
非.NET の VB から続く残念仕様・・・
全角ダブルコーテーションが必要な場合は、定数として定義しておくのがベター。

Public Class SpecialChars
    Public Const LeftDoubleQuote = ChrW(&H201C)  '“
    Public Const RightDoubleQuote = ChrW(&H201D) '”
    Public Const FullwidthQuote = ChrW(&HFF02)   '"
End Class

2013年6月19日

Dim と var

意味は異なるが、型推論の導入により、C#.NET と VB.NET で同じような構文で書けるようになった。
ただ、Dim の方が複数宣言できるので便利。元々、複数宣言可能だったのが、型推論でさらに便利になったというか。
対してvarは複数宣言不可。
理由は、「var val1=1, val2=1.0;」などの場合に型が1つに推測できないから?
複数の型が推測される場合は複数行に展開してくれたら便利だよね・・・

  • Dim (VB.NET)

'[Option Infer On]設定
Dim num = 1 'Integer型
Dim text = "1" 'String型

'複数宣言可能
Dim val1 = 1, val2 = 2, val3 = "1"
  • var (C#.NET)

var num = 1; //int型
var text = "1" //string型

//複数宣言 → コンパイル不可
var val1 = 1, val2 = 2;

Dim の変数宣言+New

Dim に型推論機能が追加されて、変数宣言+Newの省略記法が増えた。
どちらを使うかは好みの問題(生成されるMSILは同じ)だが、クラスのメンバ変数の場合は「As New」しか使えない。
※「As New」は、メンバ変数宣言でも使える省略記法であり、C# より便利な点。

Private Sub Test
    '今までの方法「As New」
    Dim hoge1 As New Hoge()
    '型推論「= New」 ※[Option Infer On]設定
    Dim hoge2 = New Hoge()
End Sub

'メンバ変数で使用可能 (実質、型が省略できる)
Private _hoge1 As New Hoge()

'コンパイル不可(メンバ変数は型推論不可)
Private _hoge2 = New Hoge()

2013年6月2日

キャストの比較 #2 オーバーフロー

キャストまたは算術演算(+、-、*、/) でオーバーフローが発生した際の挙動は、2種類ある。
  1. OverflowException をスロー
  2. 結果の最上位ビットを破棄

どちらの挙動にするかは、プロジェクト単位の設定、または C# なら checkedunchecked 構文が使える。
逆に言えば、VB.NET はプロジェクト単位でしか、オーバーフロー時の挙動を制御できない。
また、プロジェクト単位の設定オプションは下表になるが、注意すべきは C# と VB.NET で設定するチェックボックスの意味が逆という点。
デフォルト設定は両方チェック OFF、つまり、C# はオーバーフロー時の例外を発生させない、VB.NET は発生させる、と逆の挙動になっている。
オーバーフロー時の挙動設定オプション (Visual Studio 2010) デフォルト設定
C#.NET プロジェクトのプロパティ → [ビルド]タブ → [詳細設定]ボタン
→ [演算のオーバーフローおよびアンダーフローのチェック]チェックボックス
チェック OFF
VB.NET プロジェクトのプロパティ → [コンパイル]タブ → [詳細設定コンパイル オプション]ボタン
→ [演算のオーバーフローのチェックを解除]チェックボックス
チェック OFF

定数値のオーバーフロー

定数値のキャスト・算術演算でオーバーフローが発生する場合、コンパイルの挙動が C# と VB.NET で異なる。
オーバーフローチェック無し オーバーフローチェック有り
C#.NET チェック有無に関わらず、コンパイルエラー。
unchecked 構文を使うとコンパイルできる。
VB.NET キャストはコンパイル可能。
算術演算はコンパイルエラー。
キャスト・算術演算ともにコンパイルエラー。
  • C# の例

//定数値のキャストでオーバーフロー → 要 unchecked
int val1 = (int)2147483648L;
//定数値の算術演算でオーバーフロー → 要 unchecked
int val2 = 2147483647 + 1;
  • VB.NET の例

'定数値のキャストでオーバーフロー → オーバーフローチェック解除でコンパイル可
Dim val1 As Integer = CType(2147483648L, Integer);
'定数値の算術演算でオーバーフロー → 常にコンパイル不可
Dim val2 As Integer = 2147483647 + 1;

MSIL比較

オーバーフローチェック有無による MSIL の相違点は、「.ovf」の有無。
例:数値型から int にキャストする場合
オーバーフローチェック無し conv.i4
オーバーフローチェック有り conv.ovf.i4

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

2013年5月30日

キャストの比較 #1 対比表

C#.NET VB.NET
(Type)value DirectCast(value, Type) 基本のキャスト。value の型と Type に継承関係がある場合にキャスト可能。
継承関係ではないが、列挙型とその基になる型の場合、相互にキャスト可能。
C#のみ、下記が可能。
  • 数値型の間で相互変換
  • 列挙型と数値型の間で相互変換(規則は数値型同士の変換と同じ)
  • operator による独自の実装
× CType(value, Type) VB 系の CInt や CStr を型指定して変換できるようにしたもの。
列挙型と数値型の変換も可能。(規則は数値型同士の変換と同じ)
C# の (Type)value と同じく Operator による独自の実装が可能。
ただし、C# の (Type)value の結果と異なることがあり(☆1)、数値型⇔文字列型の変換も可能なことから別物と考えるべき。
value as Type TryCast(value, Type) キャスト失敗時に例外発生ではなく、null を返してくれるキャスト。
そのため、Type は null を代入できる参照型となるが、C# のみ、null 許容型も指定可能。

☆1 C# の (Type)value と VB の CType(value, Type) の結果が異なるパターン
 ※全パターン探ってないため、他にもあるかも…
浮動小数点→整数
変換例は★1
(Type)value 0 方向への丸め(切り捨て)。
C や Java のキャストと同じ。
Math.Truncate(value) でも同じ結果だが、型は変えてくれない。
CType(value, Type) 偶数方向へ丸める四捨五入(銀行丸め)。
Convert.ToInt32(value) と同じ。
Math.Round(value) でも同じ結果だが、型は変えてくれない。

★1 浮動小数点→整数の変換例
value(Type)valueCTypeConvert.ToInt32
-2.0-2-2-2
-1.6-1-2-2
-1.5-1-2-2
-1.4-1-1-1
-1.0-1-1-1
-0.60-1-1
-0.5000
-0.4000
0000
0.4000
0.5000
0.6011
1.0111
1.4111
1.5122
1.6122
2.0222

VB.NET の DirectCast、CType の使い分け

CType より DirectCast の方がパフォーマンスがいいため、DirectCast 可能な場合は、DirectCast を使う。
※ボックス・アンボックス化や、列挙型とその基になる型の相互変換など
String へ変換したい場合は ToString()、String から変換したい場合はそのクラス・構造体の Parse()、TryParse() が大抵使えるので CType は避けるべき。
浮動小数点→整数は、Math のメソッドで丸め後に CType が明示的で分かり易いかも。
整数→整数、整数→浮動小数点は、丸めを気にしなくていいので、素直に CType を使う。
Operator で独自実装している場合は、当然 CType を使用。

VB.NET で C# のキャスト (Double→Integer)

  • Math.Truncate を使う方法
※ Single や Decimal の場合は、引数の型を変えるだけでOK。

Public Shared Function CastToInt(ByVal val As Double) As Integer
    Return CType(Math.Truncate(val), Integer)
End Function

  • MSIL を使う方法
C# で作成した「double 引数を int でキャストして返す」メソッドの MSIL を基に作成。
C# のキャストと同じ結果・挙動になるが、この方法を採るくらいだったら、C# の DLL にキャストメソッドを実装して呼んだ方がまし…

Public Shared Function CastToIntIl(ByVal val As Double) As Integer
    Return DirectCast(_castToIntIlInternal.Value.Invoke(Nothing, New Object() {val}), Integer)
End Function
Private Shared _castToIntIlInternal As New Lazy(Of DynamicMethod)(
    Function()
        Dim instance As New DynamicMethod("CastToIntIlInternal", GetType(Integer), {GetType(Double)}, True)
        Dim il = instance.GetILGenerator()
        With il
            .Emit(OpCodes.Ldarg_0)
            .Emit(OpCodes.Conv_I4) 'キャスト処理(オーバーフローチェック無し)に相当
            .Emit(OpCodes.Ret)
        End With
        Return instance
    End Function
)

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0