[2019/03/08] 名前付きパイプを使ったプロセス間通信について (No.88)
[2019/02/06] 日付型データの比較について (No.78)
[2019/02/01] 日付型データの使い方(月末、年末の日付取得) (No.77)
[2018/12/26] コレクションクラス(SortedList , Hashtable)でのクラスデータの使い方 (No.73)
[2018/11/05] 関数の引数がクラスオブジェクトの場合の注意点について (No.67)
-
×
[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。
-
名前付きパイプは、パイプサーバーと複数のパイプクライアントとの間でのプロセス間通信ができます。 名前付きパイプは、メッセージ単位で通信するモードと、バイトストリームとして読み書きするモードのいずれかが選べます。
どちらのモードでも、名前付きパイプを生成し、接続した後は、サーバー側・クライアント側とも通常のファイル入出力を使うことで通信が行われます。
名前付きパイプの例として以下の2通りの方法を示します。■1回の通信毎に名前付きパイプを生成する方法
サーバー側では、名前付きパイプ処理を行うスレッドを立ち上げて、そのスレッドの中で通信処理を行います。
フォームに通信開始ボタンと、受信メッセージ等の表示用にリッチテキストボックスを設置します。
スレッド処理では NamedPipeServerStream によりサーバー用名前付きパイプクラスを生成します。
その後 WaitForConnection メソッドで接続を待ち StreamReader によりパイプからの受信文字列を取得します。 取得文字列をリッチテキストボックスにデリゲートで表示し、すぐに StreamReader とサーバー用名前付きパイプクラスを閉じます。
サーバー側プログラム
Imports System.Threading Imports System.IO Imports System.IO.Pipes Public Class frmNameServer '別スレッドからメッセージを処理するためデリゲートを利用 Delegate Sub SetRichTextBox1Delegate(ByVal Value As String) Private RichTextBox1Delegate As New SetRichTextBox1Delegate(AddressOf _ AppendTextRichTextBox1) 'リッチテキストボックスにメッセージを表示する Private Sub AppendTextRichTextBox1(ByVal message As String) '文字列を最後尾に追加 RichTextBox1.AppendText(message) 'カレット位置を末尾に移動 RichTextBox1.SelectionStart = RichTextBox1.TextLength 'カレット位置までスクロール RichTextBox1.ScrollToCaret() End Sub '別スレッドからボタンの許可設定処理するためデリゲートを利用 Delegate Sub SetButton1EnabledDelegate(ByVal bln As Boolean) Private SetButton1Delegate As New SetButton1EnabledDelegate(AddressOf _ SetButton1Enabled) '[Buttin1]のEnabled設定 Private Sub SetButton1Enabled(ByVal blnEnabled As Boolean) Me.Button1.Enabled = blnEnabled End Sub '[Start NamePipe]ボタンクリックイベント Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles Button1.Click '名前付きパイプによる通信スレッド開始 Dim server As New Thread(AddressOf ServerThread) server.Start() AppendTextRichTextBox1("名前付きパイプ通信待機中..." & vbNewLine) '[Start NamePipe]ボタン不可設定 Me.Button1.Enabled = False End Sub '名前付きパイプによる通信スレッド Private Sub ServerThread() 'サーバー用のパイプを双方向でクライアント数は2個で生成 Dim pipeServer As New NamedPipeServerStream("PIPE_TEST", PipeDirection.InOut, 2) Try 'パイプスレッド取得 Dim threadId As Integer = Thread.CurrentThread.ManagedThreadId Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ生成完了(スレッドID:" & threadId & ")" & vbNewLine}) 'パイプの接続を待つ pipeServer.WaitForConnection() Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ接続完了" & vbNewLine}) 'ストリーム読込を生成 Dim pipeStreamReader As New StreamReader(pipeServer) Try 'パイプからの文字列受信(入力) Dim strRead As String = pipeStreamReader.ReadLine() '受信文字列の表示 Me.Invoke(RichTextBox1Delegate, _ New Object() {"受信文字列:" & strRead & vbNewLine}) Catch ex As IOException MsgBox(ex.Message) Finally pipeStreamReader.Close() End Try Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ通信の終了" & vbNewLine}) '[Button1]の許可設定 Me.Invoke(SetButton1Delegate, New Object() {True}) Catch ex As Exception MsgBox(ex.Message) Finally pipeServer.Close() End Try End Sub End Class
クライアント側プログラムは送信ボタンとメッセージ入力用にテキストボックスを設置します。
送信ボタンを押下した時に NamedPipeClientStream によりクライアント用名前付きパイプを生成します。
そのクラスの Connect メソッドで接続後ストリームライタで1回のみ文字列送信を行います。
送信後、ストリームライタとクライアント用名前付きパイプを閉じます。
クライアント側プログラム
Imports System.IO Imports System.IO.Pipes Imports System.Security.Principal Public Class frmNameClient '[Start Named Pipe]ボタンクリック時イベント Private Sub ButtonSend_Click(sender As Object, e As EventArgs) _ Handles ButtonSend.Click Dim pipeStream As NamedPipeClientStream = Nothing Dim pipeSw As StreamWriter = Nothing Try '[Send Message]ボタンを不可設定 Me.ButtonSend.Enabled = False '名前付きパイプ・クライアント生成 pipeStream = New NamedPipeClientStream(".", "PIPE_TEST", _ PipeDirection.InOut, _ PipeOptions.None, _ TokenImpersonationLevel.Impersonation) '名前付きパイプ接続 pipeStream.Connect(1000) 'ストリームライタ pipeSw = New StreamWriter(pipeStream) pipeSw.AutoFlush = True '文字列送信 pipeSw.WriteLine(Me.TextBox1.Text.Trim) Catch ex As Exception MsgBox(ex.Message) Finally 'ストリームライタを閉じる If Not pipeSw Is Nothing Then pipeSw.Close() End If '名前付きパイプを閉じる If Not pipeStream Is Nothing Then pipeStream.Close() End If '[Send Message]ボタンを許可設定 Me.ButtonSend.Enabled = True End Try End Sub End Class
プログラムの実行は以下の様になります。
■名前付きパイプを生成後、複数回の通信を行う方法
1回の通信毎に名前付きパイプを生成するのでは効率が悪いですし、連続での通信ができませんので サーバー側のスレッド部分を複数回の通信が出来る様に変更します。
変更した部分は、スレッド内でストリーム読込を生成をした後で、ループを形成しパイプからの文字列受信を繰り返す様にしています。 受信した文字列がNULLの場合にストリームライタとクライアント用名前付きパイプを閉じます。
サーバー側プログラム
Imports System.Threading Imports System.IO Imports System.IO.Pipes Public Class frmNameServer '別スレッドからメッセージを処理するためデリゲートを利用 Delegate Sub SetRichTextBox1Delegate(ByVal Value As String) Private RichTextBox1Delegate As New SetRichTextBox1Delegate(AddressOf _ AppendTextRichTextBox1) 'リッチテキストボックスにメッセージを表示する Private Sub AppendTextRichTextBox1(ByVal message As String) RichTextBox1.AppendText(message) RichTextBox1.SelectionStart = RichTextBox1.TextLength End Sub '別スレッドからボタンの許可設定処理するためデリゲートを利用 Delegate Sub SetButton1EnabledDelegate(ByVal bln As Boolean) Private SetButton1Delegate As New SetButton1EnabledDelegate(AddressOf _ SetButton1Enabled) '[Buttin1]のEnabled設定 Private Sub SetButton1Enabled(ByVal blnEnabled As Boolean) Me.Button1.Enabled = blnEnabled End Sub '[Start NamePipe]ボタンクリックイベント Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click '名前付きパイプによる通信スレッド開始 Dim server As New Thread(AddressOf ServerThread) server.Start() AppendTextRichTextBox1("名前付きパイプ通信待機中・・・" & vbNewLine) '[Start NamePipe]ボタン不可設定 Me.Button1.Enabled = False End Sub '名前付きパイプによる通信スレッド Private Sub ServerThread() 'サーバー用のパイプを双方向でクライアント数は2個で生成 Dim pipeServer As New NamedPipeServerStream("PIPE_TEST", PipeDirection.InOut, 2) Try 'パイプスレッド取得 Dim threadId As Integer = Thread.CurrentThread.ManagedThreadId Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ生成完了(スレッドID :" & threadId & ")" & vbNewLine}) 'パイプの接続を待つ pipeServer.WaitForConnection() Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ接続完了" & vbNewLine}) 'ストリーム読込を生成 Dim pipeStreamReader As New StreamReader(pipeServer) Try While True 'パイプからの文字列受信(入力) Dim strRead As String = pipeStreamReader.ReadLine() 'ストリームの末尾に到達した場合はNullなので、通信を終了する 'If strRead Is Nothing Then If strRead = "" Then Exit While End If '受信文字列の表示 Me.Invoke(RichTextBox1Delegate, _ New Object() {"受信文字列:" & strRead & vbNewLine}) End While Catch ex As IOException MsgBox(ex.Message) Finally pipeStreamReader.Close() End Try Me.Invoke(RichTextBox1Delegate, _ New Object() {"パイプ通信の終了" & vbNewLine}) '[Button1]の許可設定 Me.Invoke(SetButton1Delegate, New Object() {True}) Catch ex As Exception MsgBox(ex.Message) Finally pipeServer.Close() End Try End Sub End Class
クライアント側プログラムではパイプ開始ボタンとメッセージ送信ボタンを設置します。
パイプ開始ボタン押下で名前付きパイプ・クライアント生成と接続、及びストリームライタ生成を行います。
この生成されるクラスは、メッセージ送信ボタン押下時にも使用されるため、関数の外で静的変数として宣言します。
メッセージ送信ボタン押下時にはストリームライタでメッセージを送信します。 メッセージが空白の場合はストリームライタ、名前付きパイプ・クライアントを閉じます。
クライアント側プログラム
Imports System.IO.Pipes Imports System.IO Imports System.Security.Principal Public Class frmNameClient '名前付きパイプ・クライアント Private pipeStream As NamedPipeClientStream = Nothing 'パイプ用ストリームライタ Private pipeSw As StreamWriter = Nothing 'フォームロード時イベント Private Sub frmNameClient_Load(sender As Object, e As EventArgs) Handles Me.Load '[Start Named Pipe]ボタンを許可設定 Me.Button1.Enabled = True '[Send Message]ボタンを不可設定 Me.Button2.Enabled = False End Sub '[Start Named Pipe]ボタンクリック時イベント Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Try '名前付きパイプ・クライアント生成 pipeStream = New NamedPipeClientStream(".", _ "PIPE_TEST", _ PipeDirection.InOut, _ PipeOptions.None, _ TokenImpersonationLevel.Impersonation) '名前付きパイプ接続 pipeStream.Connect(1000) 'ストリームライタ生成 pipeSw = New StreamWriter(pipeStream) pipeSw.AutoFlush = True '[Start Named Pipe]ボタンを不可設定 Me.Button1.Enabled = False '[Send Message]ボタンを許可設定 Me.Button2.Enabled = True Catch ex As Exception MsgBox(ex.Message) End Try End Sub '[Send Message]ボタンクリック時イベント Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button2.Click Try '送信文字列取得 Dim strTX As String = Me.TextBox1.Text.Trim pipeSw.WriteLine(strTX) If strTX = "" Then 'ストリームライタを閉じる pipeSw.Close() '名前付きパイプを閉じる pipeStream.Close() '[Start Named Pipe]ボタンを許可設定 Me.Button1.Enabled = True '[Send Message]ボタンを不可設定 Me.Button2.Enabled = False End If Catch ex As Exception MsgBox(ex.Message) End Try End Sub 'フォームクローズ時イベント Private Sub frmNameClient_FormClosed(sender As Object, _ e As FormClosedEventArgs) Handles Me.FormClosed 'ストリームライタを閉じる If Not pipeSw Is Nothing Then pipeSw.Close() End If '名前付きパイプを閉じる If Not pipeStream Is Nothing Then pipeStream.Close() End If End Sub End Class
クライアント側でフォームを閉じることでパイプの接続が閉じるため、サーバー側に通信の終了が通知できます。
プログラムの実行は以下の様になります。関連する記事
⇒Remoting の IPC を使ったプロセス間通信について
⇒Remoting の IPC を使ったプロセス間通信についてその2(HTTPチャネル)
⇒名前付きパイプを使ったプロセス間通信についてその2(複数クライアントとの通信)
⇒名前付きパイプを使ったプロセス間通信についてその3(クライアントとの双方向通信)
PR -
今回は日付型データの比較を取り扱います。日付型データはその変数名を使って直接 If 文等で比較することが出来ます。 数値データの様に日付型データを比較演算子で結んで値の大小等が比較できます。
以下の例は2個の日付型データに 2019/02/01 と 2019/02/02 の異なる日付を設定し、等しいかどうかの判定を If 文で行っています。簡単な日付型(Date)データの比較
Dim dateWK1 As Date = New DateTime(2019, 2, 1) '2019/02/01 Dim dateWK2 As Date = New DateTime(2019, 2, 2) '2019/02/02 '2個の日時の比較 If dateWK1 = dateWK2 Then Console.WriteLine("dateWK1 と dateWK2 は等しい") Else Console.WriteLine("dateWK1 と dateWK2 は異なる") End If Console.WriteLine("dateWK1:" & dateWK1.ToString("yyyy/MM/dd HH:mm:ss")) Console.WriteLine("dateWK2:" & dateWK2.ToString("yyyy/MM/dd HH:mm:ss"))
実行結果は当然、2個の日付型データが異なっていることが分かります。
dateWK1 と dateWK2 は異なる dateWK1:2019/02/01 00:00:00 dateWK2:2019/02/02 00:00:00
上の例では問題無く判定が行われましたが、以下の例を見て下さい。 上の例の様に、2個の日付型データは変数を Date として宣言し、 最初の変数に Now() 関数で現在日時を設定し、さらにその後10秒待った後で、2個目の変数に現在日時を設定しています。 その後、2個の変数を比較すると、やはり異なっているとの判定がされます。
2個の変数の中身を表示してやれば、最初の時刻から10秒経過した時刻が設定されていることが分かります。
これは当然と言えばそうなのですが、変数が Date 型だからと勘違いして、日付部分しか比較しないのかと考えると ミスが発生します。お恥ずかしい話、私自身も以前にはこれでバグを発生させたことがありました。日付型(Date)データの比較
'現在日時を取得 Dim dateWK1 As Date = Now() '10秒待つ System.Threading.Thread.Sleep(10 * 1000) '現在日時を取得 Dim dateWK2 As Date = Now() '2個の日時の比較 If dateWK1 = dateWK2 Then Console.WriteLine("dateWK1 と dateWK2 は等しい") Else Console.WriteLine("dateWK1 と dateWK2 は異なる") End If Console.WriteLine("dateWK1:" & dateWK1.ToString("yyyy/MM/dd HH:mm:ss")) Console.WriteLine("dateWK2:" & dateWK2.ToString("yyyy/MM/dd HH:mm:ss")) '日付型データの比較関数 Console.WriteLine("Date.Compare(dateWK1, dateWK2):" & Date.Compare(dateWK1, dateWK2))
上記の処理を実行すると以下の様な表示が、「出力」ウインドウに表示されます。
dateWK1 と dateWK2 は異なる dateWK1:2019/02/06 18:16:49 dateWK2:2019/02/06 18:16:59 Date.Compare(dateWK1, dateWK2):-1
Date 型にはプロパティとして Date というものを持っているのでこれを使うと 純然たる日付のみのデータが取得できますので、日付のみの比較が正しく行えます。 以下のソースは If 文の部分を変更して実行してみました。日付型(Date)データの日付のみの比較
'現在日時を取得 Dim dateWK1 As Date = Now() '10秒待つ System.Threading.Thread.Sleep(10 * 1000) '現在日時を取得 Dim dateWK2 As Date = Now() '2個の日時の比較 If dateWK1.Date = dateWK2.Date Then Console.WriteLine("dateWK1 と dateWK2 は等しい") Else Console.WriteLine("dateWK1 と dateWK2 は異なる") End If
上記の処理を実行すると以下の様な表示が、「出力」ウインドウに表示されます。
dateWK1 と dateWK2 は等しい
さらに日付型の Compare メソッドを使って比較処理ができます。
Date.Compare( para1, para2 ) ... 「-1, 0, 1」のInteger型の値を返します。 結果の値の意味は以下の通りです。 ・[-1] : para1 < para2 ・[ 0] : para1 = para2 ・[ 1] : para1 > para2
日付型の Compare メソッドでの比較
Dim date0 As New DateTime(2019, 8, 27, 10, 30, 0) Dim date1 As New DateTime(2019, 8, 26, 10, 10, 0) Dim date2 As New DateTime(2019, 8, 27, 10, 15, 0) Dim date3 As New DateTime(2019, 8, 28, 10, 20, 0) ' 日付のみで比較 Console.WriteLine("Compare(date1.Date, date0.Date) = {0}", Date.Compare(date1.Date, date0.Date)) Console.WriteLine("Compare(date2.Date, date0.Date) = {0}", Date.Compare(date2.Date, date0.Date)) Console.WriteLine("Compare(date3.Date, date0.Date) = {0}", Date.Compare(date3.Date, date0.Date)) ' 日付の時刻を含み比較 Console.WriteLine("Compare(date1, date0) = {0}", Date.Compare(date1, date0)) Console.WriteLine("Compare(date2, date0) = {0}", Date.Compare(date2, date0)) Console.WriteLine("Compare(date3, date0) = {0}", Date.Compare(date3, date0))
上記の処理を実行すると以下の様な表示が、「出力」ウインドウに表示されます。
Compare(date1.Date, date0.Date) = -1 Compare(date2.Date, date0.Date) = 0 Compare(date3.Date, date0.Date) = 1 Compare(date1, date0) = -1 Compare(date2, date0) = -1 Compare(date3, date0) = 1
DateTime型の CompareTo メソッドを使っても同様の結果が得られます。
DateTime型の CompareTo メソッドでの比較
Dim date0 As New DateTime(2019, 8, 27, 10, 30, 0) Dim date1 As New DateTime(2019, 8, 26, 10, 10, 0) Dim date2 As New DateTime(2019, 8, 27, 10, 15, 0) Dim date3 As New DateTime(2019, 8, 28, 10, 20, 0) ' さらにDateTime型のCompareToを利用 Console.WriteLine("date1.Date.CompareTo(date0.Date) = {0}", date1.Date.CompareTo(date0.Date)) Console.WriteLine("date2.Date.CompareTo(date0.Date) = {0}", date2.Date.CompareTo(date0.Date)) Console.WriteLine("date3.Date.CompareTo(date0.Date) = {0}", date3.Date.CompareTo(date0.Date))
関連する記事
⇒日付型データの使い方 :[Date,DateTime]
⇒日付型データの使い方(月末、年末の日付取得)
-
日付を取り扱う処理は多いもので、請求処理や月締処理などで対象月の月末の日付を計算する場合があります。 また、年度の替わりが年末である場合には年末の日付を計算する必要があります。
月末で問題なのは2月で、うるう年の月末は29日のため、そのことを考える必要があります。
月末の取得は、私がよく使っているのですが指定された年月での月の最初日から翌月の最初日を計算し、 その結果の1日前を計算しています。 また、DateTime の DaysInMonth メソッドを使って該当年月の日数を使うことでも可能です。
それでは、指定された年月日に対応する月末の日付を取得する方法と、年末の日付を取得する方法を以下に示します。月末、年末の日付取得
'月末の取得 Dim dateWK As DateTime dateWK = Now() Console.WriteLine("現在:" & dateWK.ToString("yyyy/MM/dd")) dateWK = New Date(dateWK.Year, dateWK.Month, 1) '指定年月での最初日 dateWK = dateWK.AddMonths(1).AddDays(-1) '次月の1日前を計算 Console.WriteLine("月末の取得1:" & dateWK.ToString("yyyy/MM/dd")) '[DateTime.DaysInMonth]メソッドによる方法 dateWK = Me.DateTimePicker1.Value dateWK = New Date(dateWK.Year, dateWK.Month, DateTime.DaysInMonth(dateWK.Year, dateWK.Month)) Console.WriteLine("月末の取得2:" & dateWK.ToString("yyyy/MM/dd")) '年末の取得(直接12月31日指定) dateWK = Me.DateTimePicker1.Value dateWK = New Date(dateWK.Year, 12, 31) '指定年での最終日(直接12月31日指定) Console.WriteLine("年末の取得の取得1:" & dateWK.ToString("yyyy/MM/dd")) '年末の取得(指定年の翌年の元旦の1日前の計算) dateWK = Me.DateTimePicker1.Value dateWK = New Date(dateWK.Year, 1, 1) '指定年での最初日(元旦) dateWK = dateWK.AddYears(1).AddDays(-1) '次年の1日前を計算 Console.WriteLine("年末の取得の取得1:" & dateWK.ToString("yyyy/MM/dd"))
上記の処理を実行すると以下の様な表示が、「出力」ウインドウに表示されます。
現在:2019/02/01 月末の取得1:2019/02/28 月末の取得2:2019/02/28 年末の取得の取得1:2019/12/31 年末の取得の取得1:2019/12/31
ところで、年の加算を行った場合にうるう年を跨いだ場合にどうなるのかが少し気になりましたので、 以下の様なソースで実行してみました。1年後の計算(AddYearsメソッド)
'1年後の計算(AddYearsメソッド) dateWK = New Date(2019, 2, 28) '指定年月:2019/02/28 dateWK = dateWK.AddYears(1) Console.WriteLine("うるう年では無い年の1年後:" & dateWK.ToString("yyyy/MM/dd")) dateWK = New Date(2020, 2, 28) '指定年月:2020/02/28 dateWK = dateWK.AddYears(1) Console.WriteLine("うるう年の場合の年の1年後:" & dateWK.ToString("yyyy/MM/dd")) dateWK = New Date(2020, 2, 29) '指定年月:2020/02/29 dateWK = dateWK.AddYears(1) Console.WriteLine("うるう日からの1年後:" & dateWK.ToString("yyyy/MM/dd")) dateWK = New Date(2019, 3, 1) '指定年月:2019/03/01 dateWK = dateWK.AddYears(1) Console.WriteLine("うるう日を挟む場合の1年後:" & dateWK.ToString("yyyy/MM/dd"))
上記の処理を実行すると以下の様な表示が、「出力」ウインドウに表示されます。
2019/02/28 を指定した場合、2019年はうるう年では無いので、1年後の計算は当然 2020/02/28 になります。
2020/02/28 を指定した場合、2020年はうるう年なので 2020/02/29 があるので1日ずれるかと思いきや、1年後の計算は当然 2021/02/28 になります。 これはうるう日を特別に計算しているのか、もしくはただ単に年の数値を1加算しているのかはわかりません。
2020/02/29 を指定した場合でも結果は 1年後の計算は当然 2021/02/28 になります。やはり、うるう日を加味されている様です。
このことは、ある指定日からの1年間を計算する場合に問題になるかもしれません。 1年は365日とするのかそれともうるう日を含めるのかはその時々の仕様によると思います。うるう年では無い年の1年後:2020/02/28 うるう年の場合の年の1年後:2021/02/28 うるう日からの1年後:2021/02/28 うるう日を挟む場合の1年後:2020/03/01
関連する記事
⇒日付型データの使い方 :[Date,DateTime]
⇒日付型データの比較について
⇒文字列から数値型への変換(parse - tryparse)
⇒オブジェクト型から数値型への変換(TryParse)
-
プログラム上でKEYを持つリスト構造を使う場合に利用するのがコレクションクラスです。 良く使うコレクションクラスには以下の様に3種類のものがあります。
クラス名 KEY操作 Index操作 備考 ArrayList × ○ Indexでの操作しかできないコレクション基本クラス
取り扱うデータ型は Object 型Hashtable ○ × 連想配列的なKEYを持つコレクションクラス
KEY及び、取り扱うデータ型は Object 型SortedList ○ ○ ArrayList と Hashtable の性質を併せ持ったコレクションクラス
Indexは指定されたKEY順にソートされた結果が割り振られます
単なる Object 型の配列として使う場合は ArrayList でもいいのですが、 データ処理をする場合には何がしかのキーがあって、それに紐づくデータがあるものです。 そのためキーが扱える Hashtable SortedList を利用します。
これらのコレクションクラスはデータとして扱えるのは、 Object 型なので結果 データ型としてOKなものは何でもになります。 いろんなサイトで、データとして文字列が扱われているものはあるのですが、今回は少しだけ複雑にするため、 ユーザクラスをデータにもつものを示したいと思います。
以下のソースを見て下さい。ユーザクラスとしてTest を宣言しています。
このクラスを SortedList のデータとして追加等を行います。
このプログラムはモジュールファイルに Sub Main を作成しその中に処理を記述しています。 これをコンパイルする場合はプロジェクト・プロパティでスタートアップオブジェクトとして MdlMainSortList を指定して下さい。
クラスデータをリストする SortedList を宣言した後、 テストクラスのデータを4件生成しながら SortedList に追加を行います。 キーとしては4件とも同一の書式で文字列として処理しています。
その後確認の為に SortedList.Values から順次データを取得して、内容を表示しています。 (この時取得されるのはクラス型なので、いわゆる参照型です。)
さて、内部のデータの変更を行う為にキーを指定しデータを取得します。 ここでクラスのパブリック変数に直接値を入れることで内部データの変更が行われます。 (クラスの参照を SortedList はデータとして持っているだけで、その参照により実体データにアクセスします)
再度 SortedList の中身を全て確認するのですが、今回はIndexを使ってデータを取得しています。SortedListでのクラスデータの使い方
Module MdlMainSortList 'テストクラス Private Class Test Public Key As String 'KEYとなる文字列 Public Name As String '名称の文字列 Public Data As Integer 'あるデータとしてのInteger型データ Public Flag As Boolean 'フラグ 'コンストラクタ Sub New(ByVal aKey As String, ByVal aName As String, ByVal aData As String) Me.Key = aKey Me.Name = aName Me.Data = aData Me.Flag = False End Sub End Class <STAThread()> _ Sub Main() 'クラスデータをリストするSortedList Dim SortTest As New SortedList Dim clsTest As Test '敢えてKEYの順番通りでは無い様に SortedList に追加 clsTest = New Test("KEY004", "NAME004", 444) SortTest.Add("KEY004", clsTest) clsTest = New Test("KEY001", "NAME001", 100) SortTest.Add("KEY001", clsTest) clsTest = New Test("KEY003", "NAME003", 333) SortTest.Add("KEY003", clsTest) clsTest = New Test("KEY002", "NAME002", 222) SortTest.Add("KEY002", clsTest) Debug.Print("**************************") 'SortedList のKEY順番に取り出す For Each clsTest In SortTest.Values '内容を確認 Debug.Print(clsTest.Key & " : " & clsTest.Name & " : " & clsTest.Data.ToString) Next '1個のデータをKEYで内容を取り出し、そのクラスの中身を変更する clsTest = SortTest.Item("KEY002") '取り出した clsTest はクラスデータの参照なので、以下の処理でクラスの中身を変更 clsTest.Name = "NAMEaaa" clsTest.Data = 555 clsTest.Flag = True Debug.Print("**************************") 'KEY順番に指標で取り出す For i As Integer = 0 To SortTest.Count - 1 'クラスの参照を取り出す clsTest = CType(SortTest.GetByIndex(i), Test) '内容を確認 Debug.Print(clsTest.Key & " : " & clsTest.Name & " : " & clsTest.Data.ToString & " : " & clsTest.Flag.ToString) Next End Sub End Module
結果の出力を開発環境の「出力」ウインドウに表示していますが、以下の様になります。
("KEY002"のデータが変更されていることが分かります。)
次に、 SortedList を Hashtable に変えてみた例を示します。 特に SortedList の場合と変わりはありませんが、 Indexを使ってのデータへのアクセスは出来ないので、キーを使うことになります。
ここでキーについての注意ですが、私はキーとしては文字列で設定する様にしています。 例えば扱うデータのキーとして「得意先コード(数値で5桁)」「商品コード(数値で8桁)」の2個があった場合は、 それぞれのコードを先頭ゼロ付きで桁数固定で文字列変換し連結したものをキーとしています。
もし「商品コード」等が文字列(漢字を含まないとする)の場合でしたら、 先頭に空白を補って桁数を揃えて連結します。そうしないと比較がうまくいかないからです。Hashtableでのクラスデータの使い方
Module MdlMainHashList 'テストクラス Private Class Test Public Key As String 'KEYとなる文字列 Public Name As String '名称の文字列 Public Data As Integer 'あるデータとしてのInteger型データ Public Flag As Boolean 'フラグ 'コンストラクタ Sub New(ByVal aKey As String, ByVal aName As String, ByVal aData As String) Me.Key = aKey Me.Name = aName Me.Data = aData Me.Flag = False End Sub End Class <STAThread()> _ Sub Main() 'クラスデータをリストするHashtable Dim HashTest As New Hashtable Dim clsTest As Test '敢えてKEYの順番通りでは無い様に SortedList に追加 clsTest = New Test("KEY004", "NAME004", 444) HashTest.Add("KEY004", clsTest) clsTest = New Test("KEY001", "NAME001", 100) HashTest.Add("KEY001", clsTest) clsTest = New Test("KEY003", "NAME003", 333) HashTest.Add("KEY003", clsTest) clsTest = New Test("KEY002", "NAME002", 222) HashTest.Add("KEY002", clsTest) 'KEY順番に指標で取り出す Debug.Print("**************************") For Each clsTest In HashTest.Values '内容を確認 Debug.Print(clsTest.Key & " : " & clsTest.Name & " : " & clsTest.Data.ToString) Next '1このデータをKEYで内容を取り出し、そのクラスの中身を変更する clsTest = HashTest.Item("KEY002") '取り出した clsTest はクラスデータの参照なので、以下の処理でクラスの中身を変更 clsTest.Name = "NAMEaaa" clsTest.Data = 555 clsTest.Flag = True 'KEY順番に指標で取り出す Debug.Print("**************************") For Each clsTest In HashTest.Values '内容を確認 Debug.Print(clsTest.Key & " : " & clsTest.Name & " : " & clsTest.Data.ToString) Next End Sub End Module
-
関数の引数が値渡しのクラス変数である場合は注意が必要です。 関数内でクラスの変数を介して、クラス内のデータの書き変えが行われた場合、呼び出した側でそれを参照した時に書き変った値を扱います。
これは、クラス変数は参照型として扱われ、関数にはクラス変数の参照そのものの値が渡されて、 その参照値を使ってクラスの実体(インスタンス)にアクセスを行う為、関数呼び出し側と、関数内で同じインスタンスを処理対象とする為です。
実際のプログラムを見れば一目瞭然なのですが以下の様になります。関数の引数が値型のクラス変数である場合
Module mdlClassPrm ' テスト用クラス Class clsPrmTest ' 内部変数を1個持つ Public intData1 As Integer End Class ' 引数がテストクラスの値渡し Sub PrmTestByVal(ByVal clsP As clsPrmTest) ' クラス内部の変数を1加算 clsP.intData1 += 1 End Sub ' 引数がテストクラスの参照渡し Sub PrmTestByRef(ByRef clsP As clsPrmTest) ' クラス内部の変数を1加算 clsP.intData1 += 1 End Sub Public Sub Main() ' テストクラス生成 Dim clsPrm As New clsPrmTest ' テストクラス変数初期化 clsPrm.intData1 = 0 Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) ' 引数がテストクラスの値渡しを呼出す Call PrmTestByVal(clsPrm) Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) ' 引数がテストクラスの参照渡しを呼出す Call PrmTestByRef(clsPrm) Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) End Sub End Module
クラス clsPrmTest は内部にパブリックな1個の変数を持つだけの簡単なものです。 関数 PrmTestByVal は引数に ByVal (値渡し)としての引数を持ち、内部の処理はクラス内の変数を+1しています。
このプログラムを実行すると、 PrmTestByVal を実行後の表示が「clsPrm.intData1=1」と1加算されたものになります。 関数内でアクセスされた変数と、関数の呼出し側でアクセスされた変数が同じものを扱っています。
引数が ByVal 指定なので値型変数(Integer, Long 等)の様に値そのものが渡されるので、 クラスの場合もクラスそのものが値として渡されると勘違いしがちです。
しかし、関数に渡される値は、クラスの参照型データ、つまりインスタンスを参照している参照データが渡されて、それを元にインスタンスにアクセスされてしまうからです。 参照値はクラスインスタンスのアドレスが渡されると考えた方が分かりやすいかもしれません。
PrmTestByVal の値渡し引数を参照渡しにした PrmTestByRef を宣言しましたが、 この場合、引数で渡されるのは、クラスインスタンスの参照データの参照が渡されます。
参照の参照を使ってクラスインスタンスにアクセスすると、インスタンスの実体にアクセスできるので、PrmTestByVal と同様の動作となります。 この動きがどうも解せないのですが、参照の参照でも、その内部の変数にアクセスが可能な様です。 (原理的に上手く説明ができませんが...)
この参照の参照を理解する為に、以下の様にプログラムを変更します。
参照データの引数を関数内で書き変える例
Module mdlClassPrm ' テスト用クラス Class clsPrmTest Public intData1 As String End Class ' 引数がテストクラスの値渡し Sub PrmTestByVal(ByVal clsP As clsPrmTest) clsP.intData1 += 1 ' 内部で新しくインスタンス生成 Dim clsPnew As New clsPrmTest clsPnew.intData1 = 100 clsP = clsPnew End Sub ' 引数がテストクラスの参照渡し Sub PrmTestByRef(ByRef clsP As clsPrmTest) clsP.intData1 += 1 ' 内部で新しくインスタンス生成 Dim clsPnew As New clsPrmTest clsPnew.intData1 = 200 clsP = clsPnew End Sub Public Sub Main() ' テストクラス生成 Dim clsPrm As New clsPrmTest ' テストクラス変数初期化 clsPrm.intData1 = 0 Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) ' 引数がテストクラスの値渡しを呼出す Call PrmTestByVal(clsPrm) Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) ' 引数がテストクラスの参照渡しを呼出す Call PrmTestByRef(clsPrm) Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1) End Sub End Module
これを実行すると、以下の様な表示になります。
clsPrm.intData1=0 clsPrm.intData1=1 clsPrm.intData1=200
PrmTestByRef の中では新しくクラスのインスタンスを生成し、参照渡し引数に代入していますので 変数の中身が新しいインスタンスへの参照に置き換わってしまいます。 そのため、関数呼び出し側に戻って表示を行うと、関数内で行った初期値の「200」となります。
では、メイン関数で最初に生成されたクラスのインスタンスはどうなったのでしょうか?
インスタンス自体はVB.NETが管理するメモリ上に存在するのですが、どこからも利用できない状態になります。
このプログラムでは直ぐに実行が終わるので、問題はありません。 もし直ぐに実行が終わらなくても、まあ、心配しなくても、どこからも参照されなくなったインスタンスは、そのうちシステムの方で片づけてくれます。
(この仕組みを「ガベージコレクション」いわゆる「ゴミ集め」というそうです。)
だからと言って、どんどん参照を書き変えていくことをしない方が良いと思います。関連する記事
⇒関数の戻り値がクラス(オブジェクト)の場合について
⇒クラスにイベントを実装する方法について(Event, RaiseEvent, WithEvents)