佐々木屋

技術的なことから趣味まで色々書きます

型変換は何を使う?(その①)

型を覚えるときに必ずセットで出てくる型変換(キャスト)の話です。

実は、型変換はC#VB.NETでだいぶ違います。C#は変換方法がだいたい決まっているのですが、VB.NETでは変換方法がありすぎて、プログラマーによってまちまちなのです。そのため初学者はどれを使っていいのか迷ってしまいます。

今回はどの型変換をどの場面で使えば良いのかを考えてみたいと思います。なお、先に言っておくと実は明確なルールというものは無く、あくまで私個人の見解となりますので、一つの意見としてとらえて下さい。

できそこないのDirectCast

いきなりのVBディスリですが、このDirectCastがこの問題の半分くらいを占めていると言って過言ではありません。C#でこれに対応するのが()による型変換ですが、挙動が全く異なります。違いは以下の通りです。

項目 C#
VB.NET
構文 (型) 値 DirectCast(値, 型)
適用範囲 ・値の型と変換する型が継承関係の場合
・ボクシング
・列挙型とその基となる型
●数値型同士
●列挙型と数値型
・値の型と変換する型が継承関係の場合
・ボクシング
・列挙型とその基となる型
速度 高速に処理されるがas演算子よりは低速 高速に処理される
適用範囲の●印の2つが重要で、例えばInteger型をDecimal型に変換するようなことはDirectCastでは出来ないのです。

//これは可能
int a = 0;
decimal b = (decimal)a;
'コンパイルエラー
Dim a As Integer
Dim b As Decimal = DirectCast(a, Decimal)

これはとても使いにくいです。処理自体はC#の()と同様で非常に高速で処理されますので多用したいのが山々ですが、結局こういった制約があるので使う場面は限られてしまいます。

型変換失敗時にNullを返す

C#はas演算子が相当しますが、VB.NETではTryCastがほぼ同じ挙動となります。エラー時はnullを返しますので、必然的に参照型だけ使用可能となります。

項目 C#
VB.NET
構文 値 as 型 TryCast(値, 型)
適用範囲 ・参照型
●null許容型
・参照型
なぜかVB.NETはnull許容型は適用外。

C#の場合は()よりasの方が高速に処理されますので、参照型であれば優先的に使用します。逆にVB.NETの場合はDirectCastの方が高速に処理されるので、はっきりってTryCastを利用する場面はあまりありません。

VB.NET独自の型変換関数

VB6でお馴染みCIntやCStrと同等処理となります。C#の()とほぼ同じ挙動(一部異なる)となりますが、速度は遅く、また数値型とString型が相互変換可能ということより、VB.NETだけの独自の仕組みと考えた方がよさそうです。

Dim a As Integer
Dim b As Decimal = CType(a, Decimal)
'以下も同等(内部的にCTypeが呼ばれる)
Dim c As Decimal = CDec(a)

VB独自関数といいつつ、前述したDirectCastが使えない場面では使わざるを得ないのが現状です。但し、値型の変換はこのあとに出てくるParse(TryParse)を利用した方がよいでしょう。

デリゲートの使いどころ ③(マルチキャストデリゲート)

一つのデリゲートで複数のメソッドを連続で実行することをマルチキャストデリゲートと呼びます。これによってメソッドを任意のタイミングで追加して最後に実行するような処理も簡潔に書くことができます。

例えば以下のようなメソッドをデリゲートで呼ぶとします。

private delegate void TestDlgt();
private void Method1() {
    Console.WriteLine("hoge");
}
private void Method2() {
    Console.WriteLine("piyo");
}
private void Method3() {
    Console.WriteLine("fuga");
}
Private Delegate Sub TestDlgt()
Private Sub Method1()
    Console.WriteLine("hoge")
End Sub
Private Sub Method2()
    Console.WriteLine("piyo")
End Sub
Private Sub Method3()
    Console.WriteLine("fuga")
End Sub


メソッドの追加

C#は+演算子でメソッドを追加していきます。

TestDlgt dgt = new TestDlgt(Method1);
dgt += Method2;
dgt += Method3;
dgt();

VB.NETは少し面倒くさいです。System.DelegateクラスのCombineメソッドに連結するメソッドを渡して追加します。Combineメソッドの戻り値はSystem.Delegate型なので、DirectCastかCTypeで型変換が必要です。

Dim dgt1 As TestDlgt = New TestDlgt(AddressOf Method1)
dgt1 = DirectCast([Delegate].Combine(dgt1, New TestDlgt(AddressOf Method2)), TestDlgt)
dgt1 = DirectCast([Delegate].Combine(dgt1, New TestDlgt(AddressOf Method3)), TestDlgt)
dgt1()
hoge
piyo
fuga



C#でもCombineメソッドで追加できますが、面倒なのであまり利用されません。

TestDlgt dgt = new TestDlgt(Method1);
dgt = (TestDlgt)Delegate.Combine(dgt, new TestDlgt(Method2));
dgt = (TestDlgt)Delegate.Combine(dgt, new TestDlgt(Method3));
dgt();


削除

C#は-演算子で削除します。

dgt -= Method2;
dgt();

VB.NETはSystem.DelegateクラスのRemoveメソッドに削除するメソッドを渡します。

dgt1 = DirectCast([Delegate].Remove(dgt1, New TestDlgt(AddressOf Method2)), TestDlgt)
dgt1()
hoge
fuga

C#でもRemoveメソッドは利用可能です。

dgt = (TestDlgt)Delegate.Remove(dgt, new TestDlgt(Method2));
dgt();



削除は追加した数だけ必要ですが、過剰に削除しても特にエラーになるようなことはありません。

不要な制御フラグ

制御フラグでの条件分岐はよくある手法の一つですが、これが多くなりすぎると非常に可読性が悪くなります。

課題①

例えば以下のコードを見てみましょう。

int x = 1;
int y = 2;

bool flg = true;
while (flg) {
    if (x * y > 10) flg = false;
    ++x;
    ++y;
}
Dim x As Integer = 1
Dim y As Integer = 2

Dim flg As Boolean = True
While flg
    If (x * y > 10) Then flg = False
    x += 1
    y += 1
End While

変数flgがtrueの間だけ繰り返し処理が実行されますが、制御変数であるflgは本当に必要でしょうか。

リファクタリング

条件で繰り返し処理を抜けるだけに変数を使用するなら、抜ける命令をすれば良いのです。

while (true) {
    if (x * y > 10) break;
    ++x;
    ++y;
}
While True
    If (x * y > 10) Then Exit While
    x += 1
    y += 1
End While


課題②

次のコードはどうでしょうか。

int x = 1;
int y = 2;

bool flg = x * y > 10 ? true : false;
if (flg) {
    //何かの処理
}
Dim x As Integer = 1
Dim y As Integer = 2

Dim flg As Boolean = If(x * y > 10, True, False)
If flg Then
    '何かの処理
End If

計算結果が変数flgに格納されて、その後条件判断に使用されています。

リファクタリング

あまりも長い計算や、処理が複雑で判別に行数を要するような処理は別ですが、特に短く済むような場合はいちいち変数に○×を持つのは無駄ですし、そのまま書いた方が可読性が良くなります。

if (x * y > 10) {
    //何かの処理
}
If x * y > 10 Then
    '何かの処理
End If



癖で何でも変数を作って入れてしまう人もいますが、その変数を作る処理と実際使われる回数であったり場面であったりを考えた時、正直無い方がすっきりすることが多いです。これは変数に限らず関数化でも同じことが言えます。

適切なメンバーを作ることが、見やすい保守しやすいコードの第一歩です。

シャドウとオーバーライドの違い

VB.NETにはオーバーライドに似たような機能でシャドウがあります。但し、オーバーライドとシャドウは機能としては似ているかもしれませんが、使用される意味としては全くの別物です。初学者は混同しがちなので注意して下さい。

読んで字のごとくですが、シャドウされたメソッドは隠されます。隠されるというとアクセス出来ないと思うかもしれませんが、決してそういうわけではありません。まず、以下の例を考えます。

Public Class BaseClass
    Public Overridable Sub ShowMessage()
        Console.WriteLine("基底クラス")
    End Sub
End Class

Public Class SubClass1
    Inherits BaseClass
    Public Overrides Sub ShowMessage()
        Console.WriteLine("派生クラス1")
    End Sub
End Class

Public Class SubClass2
    Inherits BaseClass
    Public Shadows Sub ShowMessage()
        Console.WriteLine("派生クラス2")
    End Sub
End Class

まぁ、単純な継承したクラスです。但し、SubClass1は基底クラスのメソッドをオーバーライドしていますが、SubClass2はシャドウしています。シャドウはShadowsキーワードをプロシージャ名の前につけます。これはVB.NET特有のものです。

挙動にどのような影響があるのか、いつものごとく呼び出してみます。

Dim test1 As New SubClass1()
Dim test3 As New SubClass2()

test1.ShowMessage()
test3.ShowMessage()
派生クラス1
派生クラス2

あれ?何も変わりませんね。

では、基底クラスを型にして派生クラスを呼び出すように変更してみます。

Dim test2 As BaseClass = New SubClass1()
Dim test4 As BaseClass = New SubClass2()

test2.ShowMessage()
test4.ShowMessage()
派生クラス1
基底クラス

挙動が変わりました。

オーバーライドした場合は基底クラスを型にして派生クラスを呼び出しても変化しません(派生クラスのメソッドが呼ばれる)が、シャドウした場合は派生クラスのメソッドは隠されて基底クラスのメソッドが呼び出されます。つまり、シャドウは継承後メソッドをオーバーライドのように置き換えるわけではないということが分かります。

なお、シャドウは基底クラスのメソッドを仮想メソッドにしなくても使用可能です(基底クラスメソッド自体を書き換えるわけではない)。


C#の場合はnewキーワードを指定すると同じ動作になります。

public class BaseClass {
    public virtual void ShowMessage() {
        Console.WriteLine("基底クラス");
    }
}

public class SubClass1 : BaseClass {
    public override void ShowMessage() {
        Console.WriteLine("派生クラス1");
    }
}

public class SubClass2 : BaseClass {
    public new void ShowMessage() {
        Console.WriteLine("派生クラス2");
    }
}

SubClass1 test1 = new SubClass1();
BaseClass test2 = new SubClass1();
SubClass2 test3 = new SubClass2();
BaseClass test4 = new SubClass2();

test1.ShowMessage();
test2.ShowMessage();
test3.ShowMessage();
test4.ShowMessage();
派生クラス1
派生クラス1
派生クラス2
基底クラス

イテレーターの基本

メソッドの戻り値やgetterプロパティでreturnではなくyield returnした構文のことをイテレーター(メソッド)と呼びます。イテレーターの概念はRubyPythonC#とほぼ同様です。なお、yieldはイールドと呼びます。

イテレーターの概念は現在のオブジェクト指向プログラミングで無くてはならないLINQラムダ式の概念につながっていきますので、是非勉強してみて下さい。


以下のように簡単な配列を返す処理を考えてみます。

private List<string> Naiyo() {
    List<string> res = new List<string>();
    res.Add("hoge");
    res.Add("piyo");
    res.Add("fuga");
    return res;
}

foreach (var n in Naiyo()) {
    Console.WriteLine(n);
}
Private Function Naiyo() As List(Of String)
    Dim res As New List(Of String)
    res.Add("hoge")
    res.Add("piyo")
    res.Add("fuga")
    Return res
End Function

For Each n As String In Naiyo()
    Console.WriteLine(n)
Next
hoge
piyo
fuga



さて、これをイテレーターにするとどうなるでしょうか。(呼び出し方法は変わりありませんので、これ以降は省略します。)

イテレーターの条件の一つとして、IEnumerableインタフェースを返すオブジェクトである必要があります。C#はreturn の代わりにyield returnを使用します。

private IEnumerable<string> Naiyo() {
    yield return "hoge";
    yield return "piyo";
    yield return "fuga";
}

VB.NETIteratorキーワードをつけて、Returnの代わりにYieldを使用します。

Private Iterator Function Naiyo() As IEnumerable(Of String)
    Yield "hoge"
    Yield "piyo"
    Yield "fuga"
End Function
hoge
piyo
fuga

見事に同じ結果になってしまいました。但し、前者は配列をそのまま返しているのですが、後者はforeachで配列の数だけNaiyoメソッドが呼ばれています。
ブレークポイントを設定して処理を一つ一つ確認してみて下さい。


イテレーターが果たす役割は現時点ではよく分からないと思いますが、今回は書き方と挙動を理解して下さい。

オプション引数①

メソッドの引数を省略、既定値を設定できる機能をオプション引数と呼びます。

オプション引数はC++VB.NETでは昔からありましたが、C#では.NET Framework4.0から初めて導入されました。

オプション引数

C#は引数に既定値を設定するとオプション引数となります。

private void test(int a, int b, int c = 4) {
    Console.WriteLine(a + b + c);
}

test(1, 2, 3); //6
test(1, 2); //7



VB.NETではOptionalキーワードをつけます。

Private Sub test(ByVal a As Integer, ByVal b As Integer, Optional ByVal c As Integer = 4)
    Console.WriteLine(a + b + c)
End Sub

test(1, 2, 3) '6
test(1, 2) '7


オプション引数の制限

オプション引数は後ろの引数のみが有効です。上記の例であればaをオプションにしてbとcを通常、といったことは出来ません。以下のようにすると、「省略可能なパラメーターはすべての必須パラメーターの後で指定する必要があります」といったエラーが表示されます。

private void test(int a = 4, int b, int c) {
    Console.WriteLine(a + b + c);
}



C#のみ、可変長引数だけはオプション引数の後ろでも設定可能です。

private void test(int a, int b = 4, params int[] c) {
    Console.WriteLine(a + b + c.Length);
}

test(1, 2, 3, 4, 5, 6);


名前付き引数

引数の名前から指定することが可能です。よって、オプション引数を含んでいても、引数の順番は自由に書くことが出来ますし、省略も可能となります。

private void test(int a, int b, int c = 4) {
    Console.WriteLine(a + b + c);
}

test(c: 1,a: 2,b: 3);
Private Sub test(ByVal a As Integer, ByVal b As Integer, Optional ByVal c As Integer)
    Console.WriteLine(a + b + c)
End Sub

test(c:=1, a:=2, b:=3)



しかし、名前付きオプション引数は拡張・保守の観点から非常にリスキーなのでおすすめしません。

null許容型(null判定)

null許容型を利用する上で重要なnull判定方法をいくつか説明します。

int? a = null;
Dim a As Integer? = Nothing

等価(Is)による条件判断

if文による等価評価(Is)を行います。

if (a == null) {
    Console.WriteLine("nullだ");
}
else {
    Console.WriteLine("nullではない");
}
If a Is Nothing Then
    Console.WriteLine("nullだ")
Else
    Console.WriteLine("nullじゃないよ")
End If


Nullable型

null許容型はNullable型でもあるので、HasValueプロパティが利用できます。

if (a.HasValue) {
    Console.WriteLine("nullではない");
}
else {
    Console.WriteLine("nullだ");
}
If a.HasValue Then
    Console.WriteLine("nullじゃないよ")
Else
    Console.WriteLine("nullだ")
End If


三項演算子

if文を一行にまとめた形です。

Dim a As Integer? = Nothing
Console.WriteLine(a == null ? "nullだよ" : "nullじゃないよ");
Console.WriteLine(If(a Is Nothing, "nullだ", "nullじゃないよ"))

短い振る舞いであれば問題ありませんが、長くなると可読性が著しく落ちます。また、通常のif文と異なり、真偽どちらの場合も書かなければならないというのも欠点です。

null条件演算子

メンバー変数のあとに「?」を付けてプロパティを指定することで、nullではない場合のみそのプロパティが実行されます。これにより、意味のない0を返したりすることを防ぐことが可能です。

Console.WriteLine(a?.ToString().Length); //nullの場合は空が返る
Console.WriteLine(a?.ToString().Length) 'nullの場合は空が返る

また、nullだとエラーをスローしてしまうようなプロパティやメソッドも、この演算子を使用することでnull判定をする必要がなくなりますので、コード自体がすっきりします。

int[] a = null;

//これだとaがnullの場合System.NullReferenceExceptionが発生
Console.WriteLine(a.Length);

//null条件演算子の場合はnull以外の場合のみLengthプロパティが実行される
Console.WriteLine(a?.Length);
Dim a As Integer() = Nothing

'これだとaがnullの場合System.NullReferenceExceptionが発生
Console.WriteLine(a.Length)

'null条件演算子の場合はnull以外の場合のみLengthプロパティが実行される
Console.WriteLine(a?.Length)


null合体演算子

三項演算子より簡潔にnull判定が行えるのがnull合体演算子です。C#のみ利用可能です。
null合体演算子の左辺がnullの場合右辺が評価され、nullではない場合はそのまま左辺が評価されます。但し、三項演算子と異なり、右辺の型と左辺の型を合わせる(厳密にオーバーロードされた型)必要があります。

Console.WriteLine(a ?? 111);            
<aがnullの場合>
111

<aが例えば数値5が入っている場合>
5

null合体演算子はnull条件演算子と併せて使うことで、上記の問題を回避することも可能です。但し、書き方によって可読性が悪くなる場合がありますので注意が必要です。

Console.WriteLine(a?.ToString() ?? "nullだ");
<aがnullの場合>
nullだ

<aが例えば数値5が入っている場合>
5