連載

改訂版
プロフェッショナルVB.NETプログラミング

Chapter 02  データ型の変化

株式会社ピーデー 川俣 晶
2004/03/04


 本記事は、(株)技術評論社が発行する書籍『VB6プログラマーのための入門 Visual Basic .NET 独習講座』の一部分を許可を得て転載したものです。同書籍に関する詳しい情報については、本記事の最後に掲載しています。

 値型と参照型のパフォーマンスの相違

 構造体はクラスとほぼ同じ機能を持つ(それぞれの詳細は、構造体宣言オブジェクト関連の変化継承とポリモーフィズムを参照)。では、どうしてクラスで代用することはできず、構造体という独立した機能が存在するのだろうか? また、クラスと構造体は何が異なり、どう使い分けるべきなのだろうか? 構造体とクラスの決定的な違いを示すサンプル・ソースを以下に示す。まず、クラスを用いた例から。

 1: Public Class Form1
 2:   Inherits System.Windows.Forms.Form
 3:
 4: …Windows フォーム デザイナで生成されたコード…
 5:
 6:   Public Class Test
 7:     Public v As Integer
 8:   End Class
 9:
10:   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
11:     Trace.WriteLine(DateTime.Now)
12:     Dim count As Integer
13:     count = 10000000
14:     Dim test(count) As Test
15:     Dim i As Integer
16:     For i = 0 To count - 1
17:       test(i) = New Test()
18:       test(i).v = i
19:     Next
20:     Dim sum As Long
21:     For i = 0 To count - 1
22:       sum = sum + test(i).v
23:     Next
24:     Trace.WriteLine(DateTime.Now)
25:   End Sub
26: End Class
リスト2-48 クラスを用いた場合の実行性能を調べるプログラム

 これを実行すると以下のようになる。

2002/03/31 13:29:48
2002/03/31 13:29:55
リスト2-49 リスト2-49の実行結果

 実行した環境は、Pentium 4/1.5GHz、メモリ512MBytesのマシンである。約7秒でこれを処理したことになる。これがメモリ256MBytesのマシンになると、激しくハードディスクにスワップが発生する。メモリ消費量も大きいサンプル・プログラムである。

 では、リスト2-48の6行目のClassキーワードをStructureキーワードに変更してみよう。

 1: Public Class Form1
 2:   Inherits System.Windows.Forms.Form
 3:
 4: …Windows フォーム デザイナで生成されたコード…
 5:
 6:   Public Structure Test
 7:     Public v As Integer
 8:   End Structure
 9:
10:   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
11:     Trace.WriteLine(DateTime.Now)
12:     Dim count As Integer
13:     count = 10000000
14:     Dim test(count) As Test
15:     Dim i As Integer
16:     For i = 0 To count - 1
17:       test(i) = New Test()
18:       test(i).v = i
19:     Next
20:     Dim sum As Long
21:     For i = 0 To count - 1
22:       sum = sum + test(i).v
23:     Next
24:     Trace.WriteLine(DateTime.Now)
25:   End Sub
26: End Class
リスト2-50 クラスの代わりに構造体を用いたプログラム

 これを実行すると以下のようになる。

2002/03/31 13:30:14
2002/03/31 13:30:15
リスト2-51 リスト2-50の実行結果

 見てのとおり、1秒もかかっていない。また、メモリ256MBytesのマシンで実行しても、スワップは発生しない。つまりメモリ消費量も小さい。さらに、17行目は実は不要であり、これを取り除けばもっと速くなる。これは、たった1カ所のキーワードがClassかStructureかの相違で発生した大きな差である。つまり、機能は似ているとしても、クラスと構造体を適切に使い分けることは性能に大きく影響するわけである。

 技術的に見ると、クラスと構造体の差は、それが参照型であるか値型であるかの違いである。参照型とは、情報が独立したメモリ領域に確保され、そのメモリ領域の場所を指し示す参照情報を経由して参照されるデータ型である。これに対し値型とは、情報を保存するメモリ領域が、直接、値型を含むメモリ内に確保される方法を示す。ちなみに、クラスは参照型、整数型などの基本的なデータ型は値型である。つまり、上記の例であれば、クラス使用時はTestクラスのインスタンスとなる10000000個の小さなメモリ領域と、それを参照するためのアドレスを含む配列が確保されたのに対して、構造体の場合は構造体の内容そのものが配列の中に確保されるので、実際には1個の大きなメモリ領域を確保している。メモリ確保という処理が、1回で済むか、10000001回繰り返すかが、大きな性能差を生んでいるわけである。

 しかし、構造体の利用が常にクラス利用より速いわけではない。大きな情報の受け渡しの場合、クラスを使用すると、単にデータの場所を伝えるだけで済み、極めて迅速である。それに対して、構造体を用いた場合は、構造体の内容をすべてコピーすることになるので、処理が遅くなる。一般的に、サイズの大きいデータを頻繁に受け渡す場合は、クラスを用いた方が処理速度は速くなる

 構造体とクラスの相違は、パフォーマンスの相違のほかに、値型と参照型の振る舞いの相違もある。このように、構造体とクラスは、目的や状況に応じて使い分けが必要とされる。

 値型と参照型の振る舞いの相違

 VB.NETにおける値型と参照型の振る舞いの相違としては、値型と参照型のパフォーマンスの相違があるが、ほかにも勘違いするととんでもないバグを生み出してしまいかねない振る舞いの相違がある。リスト2-52は、クラスと構造体を同じように操作するサンプル・プログラムである。

 1: Public Class C
 2:   Public n As Integer
 3: End Class
 4:
 5: Public Structure S
 6:   Public n As Integer
 7: End Structure
 8:
 9: Public Class Form1
10:   Inherits System.Windows.Forms.Form
11:
12: …Windows フォーム デザイナで生成されたコード…
13:
14:   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
15:     Dim c1 As New C(), c2 As New C()
16:     Dim s1 As S, s2 As S
17:
18:     c1.n = 123
19:     c2 = c1
20:     c1.n = 456
21:     Console.WriteLine(c1.n)
22:     Console.WriteLine(c2.n)
23:
24:     s1.n = 123
25:     s2 = s1
26:     s1.n = 456
27:     Console.WriteLine(s1.n)
28:     Console.WriteLine(s2.n)
29:   End Sub
30: End Class
リスト2-52 クラスと構造体を同じように操作するプログラム

 これを実行すると以下のような結果になる。

1: 456
2: 456
3: 456
4: 123
リスト2-53 リスト2-52の実行結果

 見てのとおり、クラスの変数を操作したときと、構造体の変数を操作したときでは結果が同じにならない。その理由は、値型と参照型の相違にある。まず、ソース18〜20行目の処理は、クラスのインスタンスを対象としたものである。クラスは参照型である。そのため、19行目の代入文は、参照の代入として処理される。その結果、20行目を実行するとき、変数c1とc2は同じインスタンスを参照していることになる。つまり、c1.nとc2.nは同じ変数を指し示している。その結果、c1.nへの代入は、c2.nの値も変化させる。一方、24〜26行目は値型の構造体を扱っている。値型は、代入するときに、値の内容そのものをコピーする。そのため、25行目の代入は、構造体の内部にある情報の複製を作ることになる。その結果、26行目の代入でs1.nは書き変わるが、s2.nはそれとは独立しているので値は変化しない。

 もう1点注意する必要があるのが、15〜16行目の宣言である。参照型は参照するインスタンスを明示的にNewキーワードで生成する必要がある。しかし、値型はNewキーワードを使わなくても、そのまま使用できることが分かる分かるだろう。

 VB.NETでは、構造体のほか、Integerなどの数値型も値型である。それに対して、クラスのほか、文字列などは参照型である。文字列代入によるメモリ消費量の違いで述べる文字列の振る舞いの違いで、文字列を代入しても使用メモリ容量がそれほど増えていないのは、文字列が参照型であり、代入時には参照しか代入されないことによる。

 日付と時刻の変更

 日付と時刻の扱いは、Microsoft系BASICの歴史に限っても激変を経験している。初期のBASICでは、日付と時刻はそれぞれ文字列として、別個の情報として扱われていた。VB 6のDate$やTime$はこの時代の名残りといえる。しかし、VB 6では日付と時刻は倍精度浮動小数点型の数値として格納され、しかも、日付と時刻が一体の情報として扱われるようになっている。

 日付と時刻が一体になって処理されるのは、ある時点から、別のある時点までの時間間隔を計算するときなどに便利である。また、国際化をにらんで、時差を処理しようとすると、時刻の補正だけでは不十分で、時刻と日付を連動して処理しなければならない。そのような観点で、日付と時刻を一体で処理することは意義があると思われる。

 さて、どうしてこのようなVB 6の仕様の話を書いたかといえば、もしVB 6の日付時刻の仕様を誤解したままVB.NETに移行すると、混乱が拡大してしまう可能性があるからだ。

 まず、日付や時刻を取得する機能を使ったVB 6のサンプル・プログラムを作成してみた。

 1: Private Sub Form_Load()
 2:   Debug.Print Date
 3:   Debug.Print Time
 4:   Debug.Print Date$
 5:   Debug.Print Time$
 6:   Debug.Print Now
 7:   Debug.Print Timer
 8:   Debug.Print Format(Date, "yyyy/MM/dd hh:mm:ss")
 9:   Debug.Print Format(Time, "yyyy/MM/dd hh:mm:ss")
10:   Debug.Print Format(0#, "yyyy/MM/dd hh:mm:ss")
11: End Sub
リスト2-54 日付や時刻を取得する機能を使ったプログラム

 これを実行すると以下のようになる。

1: 2002/04/12
2: 17:13:12
3: 2002-04-12
4: 17:13:12
5: 2002/04/12 17:13:12
6: 61992.13
7: 2002/04/12 00:00:00
8: 1899/12/30 17:13:12
9: 1899/12/30 00:00:00
リスト2-55 リスト2-54の実行結果

 ソースの2〜3行目と、その結果は誰も異論はないと思う。4〜5行目は現在の日付と時刻を返す関数の文字列バージョンである。当然、結果として見える内容が似ていても、データ型は異なっている。なお、実行結果の3行目で、日付の区切りがスラッシュではなくハイフンになっているのが目に付くが、これはDate$の動作がCalendarプロパティの設定に影響されないという特徴によるものだ。特に理由がない限り、Date$ではなくDateを使うべきだろう。

 6行目のNowは現在の日付時刻を、7行目のTimerは午前0時からの経過時間をそれぞれ秒数で返す。8〜10行目は少し難題だ。これを正しく説明できれば、VB 6のDate型を正しく理解していると胸を張る資格がある。実はDate値は、日付だけでなく時刻も含んでいる。一方のTimeの値は、時刻だけでなく日付を含んでいる。

 Dateの場合、時刻は00:00:00であり、その日の午前0時である。Timeの場合日付は1899年12月30日となっているが、これはVB 6の日付を扱う起点となる日付である。起点はというと、値がゼロになる位置を表示させれば分かる。VB 6の日付時刻は倍精度浮動小数点数値で扱われるので、0#の値を渡せばよい。具体的には、ソースの10行目に書いたとおりである。実行結果を見て分かるとおり「1899/12/30 00:00:00」が起点となる。

 さて、これとほぼ同じプログラムをVB.NETで記述してみよう。

 1: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
 2:   Trace.WriteLine(Microsoft.VisualBasic.Today)
 3:   Trace.WriteLine(Microsoft.VisualBasic.TimeOfDay)
 4:   Trace.WriteLine(DateString)
 5:   Trace.WriteLine(TimeString)
 6:   Trace.WriteLine(Now)
 7:   Trace.WriteLine(Microsoft.VisualBasic.Timer)
 8:   Trace.WriteLine(Format(Microsoft.VisualBasic.Today, "yyyy/MM/dd hh:mm:ss"))
 9:   Trace.WriteLine(Format(Microsoft.VisualBasic.TimeOfDay, "yyyy/MM/dd hh:mm:ss"))
10:   Trace.WriteLine(Format(New Date(), "yyyy/MM/dd hh:mm:ss"))
11: End Sub
リスト2-56 リスト2-54をVB.NETで書き換えたプログラム

 これを実行すると以下のようになる。

1: 2002/04/12 0:00:00
2: 0001/01/01 17:14:02
3: 2002-04-12
4: 17:14:02
5: 2002/04/12 17:14:02
6: 62042.7941777
7: 2002/04/12 12:00:00
8: 0001/01/01 05:14:02
9: 0001/01/01 12:00:00
リスト2-57 リスト2-56の実行結果

 見てのとおり、一致しない結果がぞろぞろと出てくる。ソースもかなり違う。

 まず、ソースの2行目を見ていただきたい。VB 6のDate関数は、Todayプロパティに変わる。この例では、名前の紛らわしさが発生しているため、Microsoft.VisualBasicを先頭に付けた完全修飾名で指定している。以下も同じである。ソースの3行目は、VB 6のTime関数がTimeOfDayプロパティに変わることを示している。この2つの出力結果は結果の1〜2行目であるが、見てのとおり、日付だけ、時刻だけの出力になっていない。これは、通貨、日付、構造体の変化で説明したとおり、日付時刻から文字列に変換するデフォルトのルールが変わったためである。

 さて、ソースの3〜4行目では、Date$がDateStringに、Time$がTimeStringにそれぞれ変わっている。これは名前の変更と考えればよいだろう。

 ソースの6行目は特に説明は不要だろう。結果の6行目は、ソースの7行目の実行結果である。VB 6に比べて小数点以下の数値が多く出力されているが、これは日付時刻型が保持する精度が上がったことによるものである。とはいえ、データ型として表現できる精度があがることと、システムが持っているタイマーの精度があがることは別問題である。最後の桁まで有意の値を取得できるわけではない。

 ソースの10行目と、結果の9行目を見て分かるとおり、VB.NETでは日付時刻の起点は「0001/01/01 12:00:00」である。ここで注意が必要なのは、結果の1行目の時刻部が00:00:00であるのに、結果の7行目の時刻部は12:00:00になっていることだ。これは時間を変換する際に、12時間制と24時間制の差が出たもので、実際の値は同じプロパティから取得しており、同じものである。

 日付時刻の書式化

 VB 6には、書式を整形するためのFormat関数が存在するが、もちろんこれはVB.NETにも継承されている。しかし、両者の間に完全な互換性があるわけではない。VB 6の書式指定文字はVB 6だけの機能であったが、VB.NETの書式指定は.NET Framework上で他言語と共有されるのである。そのため、挙動が変わる書式指定文字、サポートされない書式指定文字、新規追加される書式指定文字などがある。これらは文字列中に書き込まれているため、コンパイル段階ではチェックできず、見落としやすいので注意が必要である。

 では、具体的に両者の相違を見てみよう。まずは、VB 6で書いたサンプル・プログラム(リスト2-58)をご覧いただきたい。

 1: Private Sub Form_Load()
 2:   Dim d1 As Date, d2 As Date
 3:   d1 = #2/3/2001 4:05:06 AM#
 4:   d2 = #2/3/2001 2:05:06 PM#
 5:   Debug.Print "日付時刻のFormat"
 6:   Debug.Print Format(d1, "dddddd")
 7:   Debug.Print Format(d1, "ddddd")
 8:   Debug.Print Format(d1, "dddd")
 9:   Debug.Print Format(d1, "ww")
10:   Debug.Print Format(d1, "w")
11:   Debug.Print Format(d1, "m")
12:   Debug.Print Format(d1, "q")
13:   Debug.Print Format(d1, "Nn")
14:   Debug.Print Format(d1, "N")
15:   Debug.Print Format(d1, "Hh")
16:   Debug.Print Format(d1, "H")
17:   Debug.Print Format(d1, "ttttt")
18:   Debug.Print Format(d1, "AM/PM")
19:   Debug.Print Format(d2, "a/p")
20: End Sub
リスト2-58 日付時刻に対してFormat関数を使用したプログラム

 これを実行すると以下のようになる。

 1: 日付時刻のFormat
 2: 2001年2月3日
 3: 2001/02/03
 4: Saturday
 5: 5
 6: 7
 7: 2
 8: 1
 9: 05
10: 5
11: 04
12: 4
13: 4:05:06
14: AM
15: p
リスト2-59 リスト2-58の実行結果

 実行結果から、次の書式指定文字について確認できる。

書式指定文字 意味
dddddd 長い日付
ddddd 短い日付
dddd 曜日の長い名前
ww 年の何番目の週か
w 週の何番目の日か
m 先行するゼロの付かない月
q 四半期
Nn 先行するゼロを付ける分
N 先行するゼロを付けない分
Hh 先行するゼロを付ける時
H 先行するゼロを付けない時
ttttt 長い時刻(デフォルト)
AM/PM フルネームの午前または午後(大文字)
a/p 1文字の午前または午後(小文字)
▼表2-60 リスト2-58で確認したVB 6の書式指定文字

 では、これと同じことをVB.NETで記述してみよう。リスト2-61が実際のサンプル・ソースである。

 1: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
 2:   Dim d1 As Date, d2 As Date
 3:   d1 = #2/3/2001 4:05:06 AM#
 4:   d2 = #2/3/2001 2:05:06 PM#
 5:   Trace.WriteLine("日付時刻のFormat")
 6:   Trace.WriteLine(Format(d1, "dddddd"))
 7:   Trace.WriteLine(Format(d1, "ddddd"))
 8:   Trace.WriteLine(Format(d1, "dddd"))
 9:   Trace.WriteLine(Format(d1, "ww"))
10:   ' Trace.WriteLine(Format(d1, "w")) 'エラーになる
11:   Trace.WriteLine(Format(d1, "m"))
12:   ' Trace.WriteLine(Format(d1, "q")) 'エラーになる
13:   Trace.WriteLine(Format(d1, "Nn"))
14:   ' Trace.WriteLine(Format(d1, "N")) 'エラーになる
15:   Trace.WriteLine(Format(d1, "Hh"))
16:   ' Trace.WriteLine(Format(d1, "H")) 'エラーになる
17:   Trace.WriteLine(Format(d1, "ttttt"))
18:   Trace.WriteLine(Format(d1, "AM/PM"))
19:   Trace.WriteLine(Format(d2, "a/p"))
20:   Trace.WriteLine("VB.NET固有のFormat")
21:   Trace.WriteLine(DatePart(DateInterval.Quarter, d1))
22:   Trace.WriteLine(Format(d1, "M"))
23:   Trace.WriteLine(Format(d1, "mm"))
24:   Trace.WriteLine(Format(d2, "HH"))
25:   Trace.WriteLine(Format(d2, "hh"))
26:   Trace.WriteLine(Format(d1, "%t"))
27:   Trace.WriteLine(Format(d2, "%t"))
28:   Trace.WriteLine(Format(d1, "tt"))
29:   Trace.WriteLine(Format(d2, "tt"))
30: End Sub
リスト2-61 リスト2-58をVB.NETで書き換えたプログラム

 これを実行すると以下のようになる。

 1: 日付時刻のFormat
 2: 土曜日
 3: 土曜日
 4: 土曜日
 5: ww
 6: 2月3日
 7: Nn
 8: 44
 9: 午前
10: A2/P2
11: a/p
12: VB.NET固有のFormat
13: 1
14: 2月3日
15: 05
16: 14
17: 02
18:
19:
20: 午前
21: 午後
リスト2-62 リスト2-62の実行結果

 まずはソース・コード(リスト2-61)を見ていただきたい。実行時にエラーになる行はコメントに変更した。つまり、w、q、N、Hはエラーになり、そのままでは使用できない。一方、ソース・コードの22〜29行目、出力結果の13〜21行目は、VB.NETで利用できるようになった新しい書式である。

 しかし、エラーになっていない書式指定文字であっても、動作が変わっているものがあることに注意しなければならない。「dddddd」(dが6個)と「ddddd」(dが5個)は、「dddd」(dが4個)と同じ動作に変更されている。なお、長い日付はVB.NETでは「D」、短い日付は「d」である。また、きちんと日本語対応して「土曜日」が返ってきているのも要注意だ。

 次に「ww」「Nn」「a/p」は、それぞれ記述した内容がそのまま表示されていることが分かるだろう。これらは、特別な意味を持った書式指定文字と見なされていない。また、「m」は月と日付を表示するように動作が変わっている。「Hh」は、「H」と「h」の2つが記述されたと判断され、時の値を2回出力した結果、4時が“44”になっている。「ttttt」は後述の「tt」と同じ意味と見なされて、午前または午後に変換するものとして処理されている。「AM/PM」はなかなかに悲惨で、「M」が月を示す文字と解釈され、2月であるため“A2/P2”という実行結果になっている。

 さて、VB.NETでの追加分を見てみよう。まずリスト2-61の21行目は、四半期を得るためのコードだが、これのみFormat関数を使っていない。これは日付を構成する部分を取り出すDatePart関数を経由してアクセスすべきものに変わったためだ。DatePart関数の結果は整数なので、整数として書式を整形すればFormat関数で扱えるのはいうまでもない。DatePart関数は、第1引数で取り出すべき部分を指定し、第2引数で対象となる日付を指定する。

 22行目の「M」は、「m」と同じく月と日に変換する。23行目の「mm」は「先行するゼロの付く分」となっている。23行目の「HH」は「24時間表示にした先行するゼロの付く時」、24行目の「hh」は「12時間表示の先行するゼロの付く時」、28〜29行目の「tt」は「午前または午後」となる。さて、残された問題は、26〜27行目の「%t」だが、これは午前と午後の先頭の1文字に変換するという機能がある。アメリカでは“AM”と“PM”なので、“A”または“P”に変換されるが、日本では午前と午後なので、どちらも結果が“午”になり、使う意味がない。

 ここで紹介した書式指定文字は、.NET Frameworkで使用できる文字の一部でしかない。完全な一覧表はマニュアルを参照するとよいだろう。


 INDEX
  [連載] 改訂版 プロフェッショナルVB.NETプログラミング
  Chapter 02 データ型の変化
    1.プリミティブ型のエイリアス/ VariantとObject/新しい整数型と整数型が表現できる数値範囲の拡大
    2.通貨、日付、構造体の変化/配列添え字の下限/配列のサイズ変更
    3.変数の初期化/配列の初期化/配列の次元数の変更/配列を作成するさまざまな方法/配列の共変性
    4.固定長文字列/構造体宣言/値型と参照型のパフォーマンスの相違
  5.値型と参照型の振る舞いの相違/日付と時刻の変更/日付時刻の書式化
    6.列挙型とその値/文字列代入によるメモリ消費量の違い/デフォルトのデータ型

「改訂版 プロフェッショナルVB.NETプログラミング 」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH