Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第17回 コレクション2

2020/7/26

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象です。
VB2005 Visual Basic 2005 対象です。
VB.NET 2003 Visual Basic.NET 2003 対象外ですがほとんどの説明があてはまるので参考になります。
VB.NET 2002 Visual Basic.NET (2002) 対象外ですがほとんどの説明があてはまるので参考になります。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.Stack

Stack(読み方:スタック)とQueue(読み方:キュー)は、値の出し入れの仕方に特徴があるコレクションです。

自分でStackとQueueを使うことはほとんどないと思いますが、StackとQueueの考え方はいろいろなところででてきて製品やソフトウェアに組み込まれていますので、考え方だけは知っておきましょう。

メソッドやプロパティの使い方の方は実際に必要になったときに調べる程度で問題ないはずです。

Dictionaryが辞書にたとえるなら、Stackは干草の山です。

Stackのイメージ

■画像:もっと山のように積んである干草の画像が欲しかったのですが…。

Stackは「後に追加したものを先に取り出す」というところに特徴があります。これをLIFO = Last In First Out = 後入れ先出し と呼びます。つまり、ListやDictionaryのように好きな項目を取り出せるのではなく、順番に項目を取り出していく必要があります。

Stackに対して項目を追加するということは、山の上に干草を載せることを意味します。干草を取り出すには上から順番に取り出すことになります。

 

下記のアニメーションは一般的にはスタックのイメージです。一番最後に入った C が一番最初に出て行くという「後入れ先出し」がシンプルに表現されています。

Stackのイメージ

 

発展 発展学習  -  このアニメーションはVisual Basicで作成しています

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

Visual BasicでWPFプロジェクトを作成すると、XAML(ザムル)という記述方法を使ってアニメーションを定義できます。XAMLはHTMLに似た雰囲気の言語で.NETのオブジェクトを記述するものです。WPFプロジェクトではC#でもXAMLを使用するので。同じアニメーションが作れます。

たとえば、四角形Aの移動は次の通りです。かなりべたに座標と時間を指定しています。

<Border Canvas.Left="0" Canvas.Top="50" Width="120" Height="40" Background="#FF51A0FF" BorderBrush="Black" BorderThickness="1">
  <StackPanel>
    <Rectangle Height="4"/>
    <TextBlock Text="A" FontSize="24" TextAlignment="Center" VerticalAlignment="Center" />
  </StackPanel>
  <Border.Triggers>
    <EventTrigger RoutedEvent="Window.Loaded">
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" To="310" Duration="0:0:0.5" BeginTime="0:0:5.0"/> <!-- 0 to 0.5 -->
          <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" To="300" Duration="0:0:0.5" BeginTime="0:0:5.7" /> <!-- 0.7 to 1.2 -->
          <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" To="50" Duration="0:0:0.5" BeginTime="0:0:12.5" /> <!-- 7.5 to 8.0 -->
          <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" To="570" Duration="0:0:0.5" BeginTime="0:0:13.2" /> <!-- 8.2 to 8.7 -->
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Border.Triggers>
</Border>

 

 

Stackへの項目の追加にはPushメソッド(読み方:プッシュ)を、項目を取り出すにはPopメソッド(読み方:ポップ)を使用します。Popで取り出した項目はStackからは削除されます。

値を取り出さずに先頭の項目を取得するPeekメソッド(読み方:ピーク)もあります。

次の例はStackの使用例です。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim animals As New Stack(Of String)

animals.Push("アメンボ")
animals.Push("イノシシ")
animals.Push("ウマ")

Dim result1 As String = animals.Pop()
Debug.WriteLine(result1) 'ウマ

Dim result2 As String = animals.Pop()
Debug.WriteLine(result2) 'イノシシ

■リストD-1: → Debug.WriteLineが表示される場所

Stackは作業中の履歴のような管理に向いています。たとえば、VBではメソッドを次々と呼び出していくとき、メソッドの呼び出しが終了したら呼び出しもとのメソッドに帰っていく必要があります。だから、メソッドがどのように呼び出されているかはスタックの考え方で管理されています。

 

2.演習 お絵かきアプリ

2-1.概観

簡単なお絵かきアプリを作ってStackを実践してみましょう。ここで作るプログラムはマウスでなぞった位置に線を描画するものです。そして、「元に戻す」ボタンを押すと、最後に書いた線は消えます。

この「元に戻す」機能 がポイントで、これを実現するためにStackを使います。

 

Windowsフォームアプリケーションを作成して、プロジェクト名は StackDrawer としてください。

フォームにはボタン(Button)とピクチャーボックス(PictureBox)を貼り付けて、プロパティは下記の通りにしてください。

コントロール プロパティ 備考
フォーム Text お絵かき  
Button (Name) btnUndo  
  Text <  
  Font MS UI Gothic、サイズ 16  
PictureBox (Name) PictureBox1 名前はデフォルトのままです
  BackColor White  
  BorderStyle Fixed3D  
  Anchor Top,Bottom,Left,Right フォームの大きさを変更するとPictureBoxの大きさも変わるようになります。

配置やその他のデザインはお好みでアレンジしてください。私は次のように配置しました。

お絵かきアプリデザイン画面

 

2-2.考え方

このプログラムでは、マウスでPictureBoxをなぞるときに発生するMouseMoveイベントを利用して、マウスが移動したすべての座標を記録します。

マウスが移動した座標を線で結ぶと、人間にはマウスでなぞったところが線で描画されたように見えます。

まず、単純にこの理屈をプログラムして考え方を確認してみましょう。

次のようにします。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Public Class Form1

    Dim Stroke As List(Of Point)

    Private Sub PictureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseDown
        Stroke = New List(Of Point)
    End Sub

    Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove

        '何かしらマウスのボタンが押されている場合
        If Control.MouseButtons <> MouseButtons.None Then
            Stroke.Add(e.Location) 'マウスの位置を最新のストロークに追加
            PictureBox1.Invalidate() 'PictureBox1の再描画を促す
        End If

    End Sub

    Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint

        If Stroke IsNot Nothing Then
            'ストロークに含まれるすべてのPointを線で結んだ図形を生成
            Dim path As New Drawing2D.GraphicsPath(Stroke.ToArray, Enumerable.Repeat(Of Byte)(1, Stroke.Count).ToArray)
            e.Graphics.DrawPath(Pens.Red, path) '生成した図形を描画
        End If

    End Sub

End Class

マウスが移動したすべての点を記録するための変数が、Stroke です。点の数がいくつになるかわからないので、拡張しやすい List を使って List(Of Point) で宣言しています。

PictureBox上でマウスのボタンを押したタイミングで、点の記録を開始します。これがMouseDownイベントです。この時点では点はまだ0個です。

PictureBoxのMouseMoveイベントはマウスが移動したときに発生するので、ここで Strokeに座標を追加します。マウスをちょっと移動するだけでこのイベントは何回も何十回も発生することになります。(が、コンピューターにとって何百個、何千個の座標を記録するくらい簡単な作業なので遠慮することはありません。)

マウスのボタンが押されているときだけ座標を記録するので、Control.MouseButtonsプロパティを使ってマウスのボタンが押されているかを調べています。

新しい座標を記録したらすぐに描画を行いたいのでPictureBoxのInvalidateメソッドを呼び出します。Windowsでは、いつどのタイミングで描画を実行するかはWindowsが制御しています。InvalidateメソッドはWindowsに対して、再描画命令が必要であることを通知します。これを受けてWindowsからPictureBox1に直ちに再描画命令が届きます。再描画命令が届くとPaintイベントが発生します。

Paintイベント中身は少し小難しいものになっています。今回のテーマとは関係ないので理解しなくてもよいのですが、簡単に説明しておきます。

Windowsフォームの描画機能は GDI+ (ジーディーアイプラス)と呼ばれます。GDI+で今回のような自由な形の線を描画するには、まず複数の座標から構成される図形を定義する必要があります。この図形の定義は GraphicsPath オブジェクト (読み方:GraphicsPath = グラフィックスパス)です。Newを使って、GraphicsPathオブジェクトのインスタンスを作成するときに記録されているすべての座標を第1引数に渡しています。ただし、ここではListではなく、配列を渡す必要があるので、ToArrayメソッドを使ってListを配列に変換しています。第2引数には、この図形はこれらの座標をすべて直線でつないだものであるという情報を与えています。これは、すべての要素が1であるByte型の配列で、要素の数は座標の数と一致している必要があります。Enemerable.Repeatメソッドはこのように同じ値が連続する配列を生成する機能です。

GraphicsPathオブジェクトが生成されればあとは、DrawPathメソッドでそれを実際に描画することができます。描画するときに、線の色や太さなどを指定する必要があります。ここでは、あらかじめ用意されている Pens.Red を使って幅 1 の赤いペンを使って線を描画させています。

 

このプログラムを実行すると、マウスでなぞった位置に赤い細い線を描画することができます。

マウスでなぞった位置に線が描画される

でも、2本目の線を書こうとすると最初に書いた線は消えてしまいます。

1つのストロークを記録する機能しかプログラムされていないからです。

 

2-3.複数のストローク

複数のストロークを描画できるようにするには、複数のストロークの情報を記録しておく必要があります。

このプログラムではストロークの実体は座標(Point)のListで、次のように定義されています。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019


Dim Stroke As List(Of Point)

これを複数にしたいので、イメージとしては次のような感じですが、これだと有限個になってしまうし、いろいろなところでものすごく手間がかかります。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim Stroke1 As List(Of Point)
Dim Stroke2 As List(Of Point)
Dim Stroke3 As List(Of Point)
Dim Stroke4 As List(Of Point)
Dim Stroke5 As List(Of Point)
Dim Stroke6 As List(Of Point)
Dim Stroke7 As List(Of Point)
Dim Stroke8 As List(Of Point)
Dim Stroke9 As List(Of Point)
・・・

 

コレクションを使うと似たようなものをまとめることができるので、こういうときに便利です。

後で「元に戻す」機能を作るときに使いやすいように使用するコレクションは Stack にしてみます。次のようになります。(このあとサンプルでは、さらに New も追加しています。)

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019


Dim Strokes As Stack(Of List(Of Point))

コレクションのコレクションになったので、Of が2つ出てきて少しややこしいことになってしまいました・・・。

変数名も Stroke から Strokes に複数表現できるように変えました。

こうなると、MouseMoveイベントで座標を追加するところでも、最新のストロークに対して座標を追加するように変更が必要で、Paintイベントで描画するところではすべてのストロークを描画するようにループに変更する必要があるなど、いくつか修正が発生します。

修正すると次のようになります。

変更点のうち、ポイントになる部分を黄色で強調しました。(すべての変更点が強調されているわけではありません。)

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Public Class Form1

    Dim Strokes As New Stack(Of List(Of Point))

    Private Sub PictureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseDown
        Strokes.Push(New List(Of Point))
    End Sub

    Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove

        '何かしらマウスのボタンが押されている場合
        If Control.MouseButtons <> MouseButtons.None Then
            Strokes.Peek.Add(e.Location) 'マウスの位置を最新のストロークに追加
            PictureBox1.Invalidate() 'PictureBox1の再描画を促す
        End If

    End Sub

    Dim drawPen As New Pen(Color.FromArgb(140, Color.Red), 12)

    Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint

        For Each stroke As List(Of Point) In Strokes
            'ストロークに含まれるすべてのPointを線で結んだ図形を生成
            Dim path As New Drawing2D.GraphicsPath((stroke.ToArray, Enumerable.Repeat(Of Byte))(1, stroke.Count).ToArray)
            e.Graphics.DrawPath(drawPen, path) '生成した図形を描画
        Next

    End Sub

End Class

すべてのストロークを記録する変数 Strokes を Stack として定義したのでMouseDown時に新しいストロークを追加するのに Push メソッドを使用しています。

MouseMoveイベントでは、複数ある(可能性がある)ストロークの最新のものに座標を追加したいので、先頭のストロークを取得する Peek メソッドを使用しています。

ストロークを描画するPaintイベントでは複数のストロークを処理する必要があるので For Each ~ Next を使って、ループするようにしています。

ついで、描画に使うペンを幅12 で不透明度140の赤いペンに変更しました。これは drawPenという変数で定義しています。不透明度はアルファ値と呼び、0が完全な透明、255が完全な不透明です。

これで複数の線が書けます。少し透明にしたので線が重なる部分は若干色が濃くなります。

複数の線を描画

 

2-4.元に戻す

それでは、元に戻すボタンをクリックしたら、1つずつストロークが消えていくようにしてみましょう。

ここまでできていれば簡単です。変数StrokesはStack型なので、最新のストロークを取り出すには Pop メソッドを呼び出すだけです。

プログラムによっては Pop で取り出したものに対し何か処理する場合もありますが、今回は捨てるだけなので、単純にPopを呼べばいいわけです。

元に戻すボタン btnUndo の Click イベントはたったこれだけです。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Private Sub btnUndo_Click(sender As Object, e As EventArgs) Handles btnUndo.Click
    Strokes.Pop() '最新のストロークを捨てる
    PictureBox1.Invalidate() 'PictureBox1の再描画を促す
End Sub

これで、このお絵かきアプリケーションに元に戻す機能が追加されます。

 

2-5.調整

とりあえずそれらしいアプリケーションができてよかったのですが、ストロークがない状態で元に戻すボタンをクリックするとエラーになりますし、PictureBox上でマウスをダブルクリックするとエラーになります。

簡易的に作ったプログラムなので、いろいろなケースを考慮していません。

これを改善するために、btnUndoのEnabledプロパティの初期値をFalseにし、ストロークが追加されたらTrue、ストロークがなくなったらFalseになるように改造します。

また、座標が2つ以上ないストロークに対しては描画処理を実行しないようにします。

プログラム全体は次のようになります。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Public Class Form1

    Dim Strokes As New Stack(Of List(Of Point))

    Private Sub PictureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseDown
        Strokes.Push(New List(Of Point))
    End Sub

    Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove

        '何かしらマウスのボタンが押されている場合
        If Control.MouseButtons <> MouseButtons.None Then
            Strokes.Peek.Add(e.Location) 'マウスの位置を最新のストロークに追加
            PictureBox1.Invalidate() 'PictureBox1の再描画を促す
            btnUndo.Enabled = True '元に戻すボタンを有効化
        End If

    End Sub

    Dim drawPen As New Pen(Color.FromArgb(140, Color.Red), 12)

    Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint

        For Each stroke As List(Of Point) In Strokes

            If stroke.Count <= 1 Then
                '座標が1つ以下のストロークは無視します。
                Continue For
            End If

            'ストロークに含まれるすべてのPointを線で結んだ図形を生成
            Dim path As New Drawing2D.GraphicsPath(stroke.ToArray, Enumerable.Repeat(Of Byte)(1, stroke.Count).ToArray)
            e.Graphics.DrawPath(drawPen, path) '生成した図形を描画
        Next

    End Sub

    Private Sub
btnUndo_Click(sender As Object, e As EventArgs) Handles btnUndo.Click
        Strokes.Pop() '最新のストロークを捨てる
        PictureBox1.Invalidate() 'PictureBox1の再描画を促す
       
        If
strokes.Count = 0 Then
            ''すべてのストロークが捨てられた場合、元に戻すボタンを無効化
            btnUndo.Enabled = False
        End If
       
    End Sub

End Class

これで、コメントや空の行を入れても合計 49行。たった49行でこれだけの機能が作れるのですから、たいしたものです。

 

発展 発展学習  -  画像として保存

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

最後に、描画した内容を画像として保存するプログラムを紹介します。保存用のボタンを1つ貼り付けて名前を btnSave としてください。

実行するにはPresentationCore と WindowsBase への参照設定が必要です。(.NET Frameworkの場合プロジェクトを右クリックして [追加] - [参照]で 一覧から PresentationCore と WindowsBase を『チェックして』OKクリック。)

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
    Using dialog As New SaveFileDialog

        '▼保存先ファイルの選択
        dialog.Filter = "png(*.png)|*.png|gif(*.gif)|*.gif|bmp(*.bmp)|*.bmp"

        If dialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then
            Return
        End If

        '▼メモリ上に描画して、画像データを生成
        Dim image As New Bitmap(PictureBox1.Size.Width, PictureBox1.Size.Height)
        Dim g As Graphics = Graphics.FromImage(image)

        g.Clear(Color.White) '背景を白で塗りつぶす

        'PictureBoxと同じ内容を描画するために、Paintイベントをだましてここで生成した g に描画させる。
        PictureBox1_Paint(Nothing, New PaintEventArgs(g, Nothing))

        '▼ファイルの形式にあわせてエンコードしてファイルに書き込む
        Dim encoder As Windows.Media.Imaging.BitmapEncoder = Nothing

        Select Case IO.Path.GetExtension(dialog.FileName).ToUpper
            Case ".GIF"
                encoder = New Windows.Media.Imaging.GifBitmapEncoder()
            Case ".PNG"
                encoder = New Windows.Media.Imaging.PngBitmapEncoder()
            Case ".BMP"
                encoder = New Windows.Media.Imaging.BmpBitmapEncoder()
        End Select

        Using gdiStream As New IO.MemoryStream
            image.Save(gdiStream, Imaging.ImageFormat.Bmp)

            Using mainStream As New IO.FileStream(dialog.FileName, IO.FileMode.Create)
                encoder.Frames.Add(Windows.Media.Imaging.BitmapFrame.Create(gdiStream))
                encoder.Save(mainStream)
            End Using
        End Using

    End Using
End Sub

 

 

 

3.Queue

Queueは「先に追加したものを先に取り出す」というところに特徴があります。これをFIFO = First In First Out = 先入れ先出し と呼びます。

Stackを干草の山にたとえるならQueueは行列です。人気ラーメン屋の行列やお役所の窓口の行列のイメージです。こちらは、先に並んだ人から順番に処理されていきます。

 

Queueに項目を追加するにはEnqueueメソッド(読み方:エンキュー)を使用します。項目を取り出すにはDequeueメソッド(読み方:デキュー)を使用します。Dequeueで取り出した項目はQueueからは削除されます。

値を取り出さずに先頭の項目を取得するPeekメソッド(読み方:ピーク)がある点はStackと同じです。

次の例はQueueの使用例です。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim animals As New Queue(Of String)

animals.Enqueue("アメンボ")
animals.Enqueue("イノシシ")
animals.Enqueue("ウマ")

Dim result1 As String = animals.Dequeue
Debug.WriteLine(result1) 'アメンボ

Dim result2 As String = animals.Dequeue
Debug.WriteLine(result2) 'イノシシ

■リストD-2:

リアルタイムな処理が必要なシステムでは、処理の要求が発生したときに逐一処理を実行していくのではなく、一度キューに保存して、順番に処理を実行していくという手法を取ることがあります。こういう作りにしておくと処理を行う側は作業をたんたんとこなせばよく、要求を出す側は好きなタイミングで並列で要求を出すことができるというメリットがあります。

ただ、ほとんどの場合このような構造は自分でプログラムするものではないので、プログラマーはキューの意味さえわかっていればだいたい十分です。

発展 発展学習  -  Visual Basic コンパイラの特別仕様

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

既定のプロパティがないコレクションの場合、配列のように animals(1) という具合にかっこをつけて数字を指定すると、その位置にある項目を取得することができます。StackやQueueにも当てはまります。

このとき、実際には値を取り出すために後で説明するElementAtOrDefaultというメソッドが使用されます。

これはVisual Basicのコンパイラの特別な仕様で、どうも「IEnumerableインターフェースを実装するクラスが、既定のプロパティ(やインデクサ)を持たない場合、直接 ( ) をつけるとElementAtOrDefaultメソッドの呼び出すとみなす。」ということのようです。このことが正式にドキュメントに書いてあるのを見たことがありません。

VBのコンパイラの仕様なので、C#で同じように書くとエラーになります。もっとも、ElementAtOrDefaultメソッドを自分で呼び出す必要があるかないかだけの違いです。

 

 

4.いろいろなコレクション

 

前回と今回出てきたList, Dictionary, Stack, Queueを簡単にまとめてみます。

  List Dictionary Stack Queue
考え方 一覧 辞書。
キーと値。
干草の山。
後入れ先出し。
窓口の行列。
先入れ先出し。
項目の追加 col.Add(項目) col.Add(キー, 項目)
col(キー) = 項目
col.Push(項目) col.Enqueue(項目)
先頭の項目を取得(※1) value = col(0) (該当なし) value = col.Peek value = col.Peek
先頭の項目を取り出して削除する (該当なし) (該当なし) value = col.Pop value = col.Dequeue
n番目の項目を取得(※2) value = col(n) (該当なし) (該当なし) (該当なし)
キーを指定して項目を取得 不可 value = col(キー) 不可 不可
同じ項目を複数追加 OK キーの重複はNG
値の重複はOK
OK OK

※1:コレクション共通の拡張機能(Firstメソッド)を使用して先頭の項目を取り出すことは可能です。

※2:コレクション共通の拡張機能(ElementAt)を使用してn番目の項目を取り出すことは可能です。

 

これら以外にもさまざまなコレクションがあります。その違いは項目を操作するアルゴリズムが特別だったり、並び順を常に維持している、複数の処理が並列に読み書きしたときにどうなるか、それに何か専用の機能との連携などです。

たとえば、LinkedList (読み方:リンクドリスト)を使うと、別の項目の前後やコレクションの先頭や後ろに新しい項目を追加でき、その処理の性能が良いです。項目を途中に挿入する処理が多い場合は選択肢になるでしょう。

英語ですが、こちらのページには主だった汎用のコレクションの一覧表もありよくまとまっているようです。

http://geekswithblogs.net/BlackRabbitCoder/archive/2011/06/16/c.net-fundamentals-choosing-the-right-collection-class.aspx

機能的にはListとDictionaryが使えればほぼ問題ありません。性能的にもコレクションがネックになることは通常はないでしょう。何か特別に実現したいものがある場合に他にもっと適したコレクションがあるか調べてみるのが良いと思います。

それまではListとDictionary以外のことは、「何か特別なコレクションもあったなぁ」という具合に頭の片隅においておくくらいをお勧めします。

博士のワンポイントレッスン
V太 V太:博士~。コレクションがいろいろあってわけがわかりません。List, Dictionary, Stack, Queue…。
B子 B子:V太は本当に整理するのが苦手ね。配列みたいに数値でアクセスしたいときはList、そうじゃなくって文字でアクセスしたい場合はDictionaryと覚えればいいじゃない!
V太 うーん。でも、なんかしっくりこないなぁ…。それに他にもLinkedListとかSortedDictionaryとかいろいろあるんでしょう?
博士 博士: まぁ混乱するのも無理はないな。「コレクション」という似たようなものがたくさんあるのに使い方がちょっとづつ違うのじゃからな。

まずはとにかくListを使ってみることじゃ。Listは実際に使ってみるととても簡単なんじゃよ。きっとすぐに使えるようになるじゃろう。

B子 ListになれたらDictionaryね。
博士 その後はヘルプを見ながら良さそうなコレクションを使っていくようにすれば自然といろいろなコレクションが使えるようになっていくじゃろう。
V太 はぁぁ。

 

メモ メモ  -  古いコレクション ArrayList, Hashtableなど

今となっては大分前のことですが、2015年12月のことです。私は新しく作成したシステムでArrayList (読み方:アレイリスト)というコレクションが使われているのを目撃して驚きました。ArrayListはVB.NET2002, 2003の時代に主流だったコレクションで、2015年時点で既に10年以上前の過去の遺物となっていたコレクションです。まさか、10年以上前に廃れたものを新しいシステムのプログラムで目にすることになるとは・・・。

VB.NET2002, 2003の時代には型パラメーターを定義できるジェネリックという機能がVBに導入されておらず、ArrayList (読み方:アレイリスト)はその時主流だったコレクションです。Hashtable(読み方:ハッシュテーブル)も同じで、こちらは今で言うDictionaryに該当します。

今でもArrayListやHashtableがなくなったわけではないので使うことはできますが、メリットがないので使わないようにしましょう。

私が見たシステムのプログラマにもそのように伝えておきました。

 

 

5.項目の列挙 For Each

5-1.For Each 構文 の基本

For Eachの構文(読み方:フォー イーチ)を使用してコレクションに追加されているすべての項目を順番に取り出して処理することができます。これを「列挙」(れっきょ)と呼びます。

処理の内容は自由にプログラムすることができます。単に値を表示するだけの場合もあれば、データベースに登録するということもあるでしょう。もっと別の処理もあります。

この章で説明する内容はListやDictionary、Stack、Queueなどすべてのコレクションで共通です。配列に対してまったく同じように使用できます。

 

たとえば、次のコードでは、Listであるkanjisに4つの漢字を追加し、For Eachを使って1文字ずつ取り出してそれぞれの旧字体を取得し、出力ウィンドウのデバッグに表示します。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim kanjis As New List(Of String)

kanjis.Add("国")
kanjis.Add("学")
kanjis.Add("宝")
kanjis.Add("殴")

#If NETCOREAPP Then
    '.NET Core/.NET 5以降の場合
    System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance)
#End If

For Each kanji As String In kanjis
    Dim newType As String '新字体
    Dim oldType As String '旧字体

    newType = kanji
    oldType = StrConv(kanji, VbStrConv.TraditionalChinese)
    Debug.WriteLine(newType & " - " & oldType)
Next

■リストF-1:新旧漢字対応表。(Windowsでのみ実行可能。まずやらないと思いますが、Wiondowsでも2.1以前の.NET Coreではこの例は実行できません)

Debug.WriteLineが表示される場所

結果は次のようになります。

新旧漢字対応表

発展 発展学習  -  #If NETCOREAPP Then

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

#If NETCOREAPP Then ~ #End If の間に記述したプログラムは、.NET Coreまたは.NET 5以降では実行されるのに、.NET Frameworkでは存在しないものとして扱われます。

このサンプルは.NET Frameworkでは素直に実行できるのですが、.NET Core または .NET 5以降では、System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance) を実行してから出ないと実行できません。.NET Coreまたは.NET 5以降では既定では日本語や中国語の文字コード情報が扱えるようになっていないため、追加の文字コードを使用する準備のために必要なのです。

一方、.NET Frameworkにはそもそもこの命令自体が存在せず、この命令を記述するとエラーになり実行できません。それで、#If NETCOREAPP Then の出番です。

このように # から始まる特別な命令がいくつかあり、これらはコンパイラーディレクティブと呼ばれます。通常の命令ではなく、コンパイラーにコンパイル方法を指示する命令です。

 

For Eachの構文は次のようになっています。

(VB.NET 2002 対応) VB.NET 2003 対応 VB2005 対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

For Each ①変数名 As ②型 In ③コレクション

    'ここに処理を書く 

Next

③コレクションにはループ処理する対象のコレクションを指定します。前回説明したように配列も指定できます。

For Eachはここで指定されたコレクションの項目数分処理を繰り返します。この例ではコレクションであるkanjisには4つの項目を追加していますから、このループは4周実行されます。

「4周」と言っても考え方としては、1周目、2周目、3周目、4周目ではなく、「国」の周、「学」の周、「宝」の周、「殴」の周というようにコレクションの各項目ごとに1周が割り当てられるというものです。

そのため、For~Nextのループと違って、カウンター変数で今何周目か調べることはできません。その代わりに、①変数名 を使って、今何の周か「国」か「学」か「宝」か「殴」かを取得することができます。

この①の変数をカウンター変数とも呼びます。

上の例でFor Eachの部分を次のように書き換えると、単純に今何の周か表示を見ながら確認できます。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

For Each kanji As String In kanjis
    Debug.WriteLine(kanji & "の周")
Next

■リストF-2:今何の周?

②型はコレクションの型パラメーターで指定した値の型にあわせておいてください(Dictionaryの場合はKeyValuePairです)。VB2008以上ならば型推論機能にお任せすることにして「As ②型」は書かなくても良いです。

メモ 参考  -  旧字体の取得

例として漢字の旧字体を取得するプログラムを紹介しましたが、全ての漢字でうまくいくわけではありません。このプログラムの実体は現代の漢字を中国語の簡体字にみたてて対応する繁体字を取得しているだけです。

私も中国語のことはよくわからないのですが、例で使った4つの漢字はうまい具合に日本の新字体=中国の簡体字、日本の旧字体=中国の繁体字となっている漢字のようです。この変換機能は内部ではWindowsの機能を使用しているためWindowsのバージョンによっては結果が異なる可能性があります。また、Windows以外の環境では実行できません。System.PlatformNotSupportedExceptionの例外が発生します。

 

5-2.DictionaryへのFor Each

For Eachはすべてのコレクションに有効なのですが、Dictionaryのように1つの項目にキーと値のような構造がある場合の処理のテクニック(といってもごく初歩的なもの)を紹介しておきます。

まず、前述しましたが、Dictionaryの場合、キーの型と値の型は型パラメーターで指定できますが、項目の型はKeyValuePair型です。だから、カウンター変数の型は常にKeyValuePair型になります。

ここから、キーと値を取り出すにはKeyプロパティ(読み方:キー)とValueプロパティ(読み方:バリュー)を使用します。

この例では型推論を使用して、カウンター変数の型の記述を省略しています。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim names As New Dictionary(Of String, String)

names.Add("江戸幕府", "徳川家康")
names.Add("室町幕府", "足利尊氏")
names.Add("鎌倉幕府", "源頼朝")
names.Add("大和朝廷", "神武天皇")

For Each item In names
    Dim note As String = "{0}の開祖は{1}です。"
    note = String.Format(note, item.Key, item.Value)
    Debug.WriteLine(note)
Next

■リストF-3:VB2005の場合、itemにAs KeyValuePair(Of String, String)をつければ動作します。

はじめからキーの一覧にしか興味がない場合は、names.Keysに対してFor Eachループを使用するのが早いです。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

For Each key In names.Keys
    Debug.WriteLine(key)
Next

■リストF-4:

この場合でも、キーがわかるので、DictionaryのItemプロパティを使って値を取得することも可能です。

同様に、値の一覧にしか興味がない場合は、names.Valuesに対してFor Eachループを使用するのが早いです。

 

5-3.列挙中は値の変更ができない

For Each ~ Nextでループしている間は、そのコレクションの値が変わるような操作(項目の追加・削除等)はできません。AddやRemoveを行うとInvalidOperationExceptionの例外が発生します。

もし、ループ内でそのような操作が必要な場合は、コレクションのコピーに対してループを実行します。

ループ用のコレクションのコピーは ToArrayメソッドで簡単に作成できます。ToArrayメソッドを実行するとコピーは配列になりますが、ループを回すだけなので配列でもコレクションでも差はありません。

 

たとえば特定の条件に合う要素を削除するというプログラムを次のように書くことはできません。

areas に対してループを実行しているのに、その内部で areas の項目を削除しようとしているからです。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

'この例はInvalidOperationExceptionの例外が発生します。

Dim areas As New List(Of String)

areas.Add("北海道")
areas.Add("千葉県")
areas.Add("東京都")
areas.Add("広島県")

For Each area As String In areas
    '「県」を削除する
    If area.EndsWith("県") Then
        areas.Remove(area)
    End If
Next

■リストF-5:列挙中に項目を削除して例外が発生する例

 

ToArrayに対してループを実行すれば、areasをコピーした配列をループしていることになるので、areasに対して追加や削除を行っても問題ありません。

次の例は成功します。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim areas As New List(Of String)

areas.Add("北海道")
areas.Add("千葉県")
areas.Add("東京都")
areas.Add("広島県")

For Each area As String In areas.ToArray
    '「県」を削除する
    If area.EndsWith("県") Then
        areas.Remove(area)
    End If
Next

 

 

メモ メモ  -  コピーが作成できない場合

何かの事情でコピーを作成できない場合は、面倒でも追加・削除したい項目をいったん別のコレクションに格納し、後で追加・削除することになります。

次はその例です。

VB2005対応 VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim areas As New List(Of String)

areas.Add("北海道")
areas.Add("千葉県")
areas.Add("東京都")
areas.Add("広島県")

Dim deleteAreas As New List(Of String)

For Each area As String In areas
    '削除する「県」を記録する
    If area.EndsWith("県") Then
        deleteAreas.Add(area)
    End If
Next

For
Each area As String In deleteAreas
    '「県」を削除する
    areas.Remove(area)
Next

■リストF-6:

最初のFor Eachはareasのループなので、areasの変更はできませんが、他のコレクションの変更は可能です。ここで削除する項目をdeleteAreasという別のコレクションに追加しています。

2つ目のFor EachはdeleteAreasのループなので、areasを変更することができます。

 

発展 発展学習  -  ラムダ式を使って、条件に合致する項目を削除する

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

この例のように「県」で終わる項目を削除する機能であれば、For Eachを使わずに次のように簡単に書くこともできます。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim areas As New List(Of String)

areas.Add("北海道")
areas.Add("千葉県")
areas.Add("東京都")
areas.Add("広島県")

areas.RemoveAll(Function(area) area.EndsWith("県"))

■リストF-7:ラムダ式を使って条件に合致しない項目を削除する

これはVB2008から導入されたラムダ式という機能を使っている例で、初級講座の現段階ではまだラムダ式を説明していないので詳細は割愛しますが、ラムダ式やLINQという技術はコレクションと非常に相性がよく、コレクションのプログラムを簡略化してくれます。

 

6.コレクションの共通機能

6-1.合計・平均・個数・最大値・最小値

コレクションでよくありそうな処理は簡単に利用できるように用意されており知っていると便利です。いくつかそういった処理を紹介します。

まず、合計・平均・個数・最大値・最小値は自分で計算しなくても下記メソッドを使って簡単に求められます。

  メソッド 読み方
合計 Sum サム
平均 Average アベレージ
個数 Count カウント
最大値 Max マックス
最小値 Min ミン

合計を求める例

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim points As New List(Of Integer)
points.Add(10)
points.Add(5)
points.Add(8)

'合計を取得。23になる。
Dim result As Integer
result = points.Sum

Debug.WriteLine(result)

■リストG-1:→ Debug.WriteLineが表示される場所

 

6-2.合成 (和集合・差集合・積集合)

2つのコレクションを合体させたり、2つのコレクションの中で一致するものだけを抜き出したり、差があるものだけを抜き出すことができます。

  メソッド 読み方  
合体 Union ユニオン 2つのコレクションの全体(和集合)を返します。
一致しないものを抽出 Except エクセプト 片方のコレクションに含まれていないものを返します。
一致するものを抽出 Intersect インターセクト 2つのコレクションの共通部分(積集合)を返します。

片方のコレクションに含まれないものだけを抽出する例

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim dic1 As New Dictionary(Of String, String)
dic1.Add("あ", "あめんぼ")
dic1.Add("い", "いのしし")
dic1.Add("う", "うま")

Dim dic2 As New Dictionary(Of String, String)
dic2.Add("あ", "あめんぼ")
dic2.Add("か", "からす")
dic2.Add("う", "うぐいす")

'dic1の中でdic2に含まれていないものだけを取得
Dim results = dic1.Except(dic2)

For Each result In results
    Debug.WriteLine(result)
Next

■リストG-2:

実行すると出力ウィンドウのデバッグに [い, いのしし] と [う, うま] が表示されます。

コレクションの合成(Union, Intersect, Except)

 

 

6-3.逆転・重複排除

コレクションの順番を逆転させたり、重複する項目を排除したりできます。

  メソッド 読み方  
逆転 Reverse リバース コレクションの並び順を逆転させます。
重複排除 Distinct ディスティンクト 重複しているものを排除したコレクションを作成します。

 

6-4.先頭・最後・位置指定

先頭の項目、最後の項目を取得できます。項目の位置を指定した取得もできますが、Dictionaryなどコレクションにそもそも位置という概念がない場合、状況によって取得できる項目が変わる場合があります。

また、対象の項目がない場合、その型の既定値(Integerなら0、StringならNothingといったもの)を取得する機能もあります。

  メソッド 読み方  
先頭 First ファースト  
先頭または既定値 FirstOrDefault ファーストオアデフォルト 項目がない場合、その型の既定値を返します。
最後 Last ラスト  
最後または既定値 LastOrDefault ラストオアデフォルト 項目がない場合、その型の既定値を返します。
指定位置 ElementAt エレメントアット  
指定位置または既定値 ElementAtOrDefault エレメントアットオアデフォルト 指定位置に項目がない場合、その型の既定値を返します。

 

6-5.型によるフィルター

OfTypeメソッド(読み方:オブタイプ)コレクションの項目の中で特定の型の項目だけを抽出することができます。

これを使うとフォーム上のコントロールのうち、TextBoxだけの一覧を取得するなどの処理がとても簡単になり私は重宝しています。

  メソッド 読み方  
型によるフィルター OfType オブタイプ 指定した型の項目だけを抽出します。

OfTypeメソッドは対象の型を指定するので、引数は型パラメーターです。つまり、引数の前にOfが必要であるということです。

フォームに直接貼り付いているTextBoxの一覧を取得し、すべてのTextBoxを使用不可にする例を紹介します。

この例ではPanel内部に貼り付いているなど、フォームに直接貼りついていないTextBoxは取得できません。FormのControlsプロパティが直接の子コントロールのコレクションだからです。

VB2008対応 VB2010対応 VB2012対応 VB2013対応 VB2015対応 VB2017 VB2019

Dim textBoxes = Me.Controls.OfType(Of TextBox)

For Each textBox In textBoxes
    textBox.Enabled = False
Next

■リストG-3:

 

6-6.Listへの変換・配列への変換

Listや配列は基本的で汎用的なので変換したくなることがありますが、それも簡単にできます。

古いメソッドやプロパティには配列を設定しなければいけないものもあるのですが、そんなときも安心です。

  メソッド 読み方
配列への変換 ToArray トゥーアレイ
Listへの変換 ToList トゥーリスト