ExcelVBAでクラスモジュールを活用してCSVファイルをコレクションに格納する

ExcelVBAでクラスモジュールを活用してCSVファイルをコレクションに格納する

先日CSVファイルを2次元配列に格納するコードを書いたのですが、クラスを使うととっても便利だということがわかったのでご紹介します。


はじめに

この記事は、

こちらの内容を活用してクラスモジュールを作り、

こちらの記事でやっている「CSV→2次元配列へ格納」の処理を、「CSV→クラスを使ったコレクションへ格納」に置き換えてみよう、という内容です。コードの効率や可読性がこんなに違うんだー、というところが書けたらなと。

クラスモジュールの使い方は前述の記事でガッッツリ書いていてるので(めっちゃ長い)、まずはそちらからご覧ください。

クラスの概念はデータベースのテーブルに似ている

前述のクラスモジュールの記事で、説明にこんな図を描きました。

160914-09

Class1さんの運営する配車センターがあって、複数の荷台のついた車の手配をしてくれます。最初は看板を持って「この形の車を手配しますよー」って言っているだけなので、車はまだありません。

車を使いたいときは、Class1さんにお願いして、実際に手配してもらいます。この車を「インスタンス(実体)」と呼び、荷物を載せることができます。Class1さんに言えば、同じ形の車を何台も手配してもらえます。

Name Price Number
いちご 150 5
りんご 200 3
みかん 180 10

車に載せた荷物は、表にするとこんな感じで表せます。これを考えていて、思いました。何かに似ていないか…?

161004-2

データベースのテーブル…

161004-1

似てる!!! (( ゚д゚)ガタッ

と、業務でデータベースを扱うことの多いわたしは、自分にとってのクラスの使いどころはココなんじゃ!? と思い、早速CSVファイルをクラスを使って処理してみようと思いました。

CSVファイルをクラスを使ったCollectionへ

それではまず、こんな形のCSVファイルがあったとします。

いちご,150,5
りんご,200,3
みかん,180,10

クラスモジュール(Class1)に、プロパティ設定/取得のための記述をしましょう。

Public Name As String '名称(Name)プロパティ
Public Price As Integer '値段(Price)プロパティ
Public Number As Integer '個数(Number)プロパティ

'売上(Sale)プロパティは取得のみ
Property Get Sale() As Integer
  Sale = Price * Number '値段×個数の値を使う
End Property

今回は特にプロパティに読み書き制限は設けませんので、Public変数で簡単に書いてしまいます。CSVを格納するだけなら上の3つだけでOKなのですが、勉強も兼ねて、取得した値を計算して使う「売上」プロパティも作ってみましょう。Getプロシージャだけ(読み取り専用)のプロパティにしておきます。

SQLでも、SELECTのときに計算フィールドつくれたりしますよね。やっぱりちょっと似ている…。

次に標準モジュール(Module1)に、実際の処理を書きましょう。

Sub Sample()
  Dim file As String: file = "C:\test.csv" 'CSVファイル指定
  Dim Items As New Collection 'コレクションを生成
  
  Open file For Input As #1 'CSVファイルを開く
  Do Until EOF(1) '最終行までループ
    Dim buf As String: Line Input #1, buf '読み込んだデータを1行ずつみていく
    Dim tmp As Variant: tmp = Split(buf, ",") 'カンマで分割
    
    Dim obj As New Class1 'インスタンスの生成
    obj.Name = CStr(tmp(0)) '名称
    obj.Price = CInt(tmp(1)) '値段
    obj.Number = CInt(tmp(2)) '個数
    Items.Add obj 'コレクションに追加
    Set obj = Nothing 'インスタンスの破棄
  Loop
  Close #1 'CSVファイルを閉じる
  
  Dim item As Class1 'ループ用の変数
  For Each item In Items 'コレクション内をループ
    Debug.Print item.Name, item.Price, item.Number, item.Sale 'プロパティを取得
  Next
  
  Set Items = New Collection 'コレクションの初期化(この場合は省略可)
End Sub

CSVの中身を1行ずつ見ていって要素を分割し、インスタンスを作成してそれぞれプロパティへ値を入れ、そのオブジェクトをコレクションに追加、というのをCSVの行数ぶん繰り返しています。

一応これでも動くんですが、ループ内のメインとなる10~15行目だけ見てみましょう。

Dim obj As New Class1 'インスタンスの生成
obj.Name = CStr(tmp(0)) '名称
obj.Price = CInt(tmp(1)) '値段
obj.Number = CInt(tmp(2)) '個数
Items.Add obj 'コレクションに追加
Set obj = Nothing 'インスタンスの破棄

objという変数でインスタンスを作って、それぞれプロパティに値を入れた後、5行目でコレクションに追加しています。このコードは同じプロシージャ内で何度もループするので、その都度きちんと破棄しないと不具合が出ますので、6行目は省略できません。

ここで、何度も出てくるobjは省略できないのかなー、と思って調べてみたら、いつもお世話になっているthomさんのブログで、With文でコレクションに追加する方法が紹介されていました。

なにこれすてき!!!

Public Name As String '名称(Name)プロパティ
Public Price As Integer '値段(Price)プロパティ
Public Number As Integer '個数(Number)プロパティ

'売上(Sale)プロパティは取得のみ
Property Get Sale() As Integer
  Sale = Price * Number '値段×個数の値を使う
End Property

Public Property Get Self() As Class1
  Set Self = Me
End Property

このように、クラスモジュールへ自己参照を返すプロパティを追加してやれば、

With New Class1 'インスタンスの生成
  .Name = CStr(tmp(0)) '名称
  .Price = CInt(tmp(1)) '値段
  .Number = CInt(tmp(2)) '個数
  Items.Add .Self 'コレクションに追加
End With

こんなふうに、インスタンス変数を作らなくてもコレクションに追加できちゃうのです。しかもこの書き方ならWithを抜けるときにインスタンスが破棄されるので、Nothingしなくていい。すごい。

追記:正しくは、「インスタンスへの参照が破棄される」でした。これに関してめちゃくちゃわかりやすい記事をthomさんが書いてくださいました!

というわけで、最終的には標準モジュールではこのようなコードに。

Sub Sample()
  Dim file As String: file = "C:\test.csv" 'CSVファイル指定
  Dim Items As New Collection 'コレクションを生成
  
  Open file For Input As #1 'CSVファイルを開く
  Do Until EOF(1) '最終行までループ
    Dim buf As String: Line Input #1, buf '読み込んだデータを1行ずつみていく
    Dim tmp As Variant: tmp = Split(buf, ",") 'カンマで分割
    
    With New Class1 'インスタンスの生成
      .Name = CStr(tmp(0)) '名称
      .Price = CInt(tmp(1)) '値段
      .Number = CInt(tmp(2)) '個数
      Items.Add .Self 'コレクションに追加
    End With
  Loop
  Close #1 'CSVファイルを閉じる
  
  Dim item As Class1 'ループ用の変数
  For Each item In Items 'コレクション内をループ
    Debug.Print item.Name, item.Price, item.Number, item.Sale 'プロパティを取得
  Next
End Sub

実行するとイミディエイトウィンドウにこのように出ます。

161004-3

2次元配列に格納していたときは、CSVファイルを開く前に中の行数を調べてその数で配列を宣言したり、取得するときには数値で指定しなければならなかったりと、比較するとやや難解です。

クラスを使ってコレクションに入れてしまえば、名称を取得したいときにはitem.Name、値段ならitem.Priceと、とってもわかりやすいですね! コードも短いですし。

以上です! 積極的にクラス使ってみたいですね。

公開日:2016/10/04
更新日:2016/10/09

6件のコメント

  1. 通りすがり より:

    解説ありがとうございます。
    クラスモジュールについて理解できていない部分があり、ご教授頂ければと思います。

    ===== 質問 =====
    コレクションに追加されたものをある検索条件で抽出して、その平均値を求めたいとします。
    標準モジュールのコードに動的配列を用意して、for eachで抽出したものを格納し、WorksheetFunction.Average(arr)とすればいいかと思いますが、この操作をクラスモジュールにメソッドとして作り、標準モジュールからは、item.平均値(抽出条件)のように呼び出す事はできるのでしょうか?

    抽出したいプロパティが多数の場合、その分だけ動的配列も用意する必要があり、コードの可読性が悪くなります。
    item.平均値(プロパティ, 抽出条件)
    のようにできればすっきりしたコードになっていいと思うのですが。

    よろしくお願いします。

    • *you より:

      こんにちは、コメントありがとうございます! できると思います。ちょっとコメント欄だと長くなっちゃうので、新たに記事を書きますね。少々お待ち下さい。

      追記:こちらに書きました!

  2. 名無し より:

    csvファイルの取り扱いに悩んでいた折、こちらのサイトにたどり着きました。
    とても分かりやすい解説でとても参考になりました。
    ありがとうございます。

    また、ご教授いただきたいことがございます。
    最終的にはユーザーが任意にプロパティを選択して
    データの加工ができるような設計にしたいのですが、
    なかなか思い通りにいかず悩んでおります。

    たとえばプロパティ名を変数で指定できたら、
    上の例でいうと、変数xの中身をユーザーが任意に選べ、
    x = Name のときは item.x でNameを拾ってくる・・・
    というような運用ができればベストなのですが。

    何かいい解決方法があれば、ぜひご教授いただければ幸いです。

    • *you より:

      こんにちは、コメントありがとうございます。プロパティ名を変数で指定するのはCallByNameを使うとできますよ! 本文中のModule1のコードを変更した例がこちらです。

      Sub Sample()
        Dim file As String: file = "C:\test.csv" 'CSVファイル指定
        Dim Items As New Collection 'コレクションを生成
        
        Open file For Input As #1 'CSVファイルを開く
        Do Until EOF(1) '最終行までループ
          Dim buf As String: Line Input #1, buf '読み込んだデータを1行ずつみていく
          Dim tmp As Variant: tmp = Split(buf, ",") 'カンマで分割
          
          With New Class1 'インスタンスの生成
            .Name = CStr(tmp(0)) '名称
            .Price = CInt(tmp(1)) '値段
            .Number = CInt(tmp(2)) '個数
            Items.Add .Self 'コレクションに追加
          End With
        Loop
        Close #1 'CSVファイルを閉じる
        
        Dim propName As String
        propName = InputBox("プロパティ名を指定してください", "入力要求")
        If propName = "" Then Exit Sub 'キャンセルもしくは空白だったら終了
        
        On Error GoTo Err_Handler 'エラーが起きたら"Err_Handler"へ
        
        Dim item As Class1 'ループ用の変数
        For Each item In Items 'コレクション内をループ
          Debug.Print CallByName(item, propName, VbGet) 'プロパティを指定して取得
        Next
        
        Exit Sub
        
      Err_Handler: '例外処理
        If Err.Number = 438 Then 'プロパティのエラーならこの番号
          MsgBox "存在しないプロパティです"
        Else
          MsgBox "Error #: " & Err.Number & vbCrLf & vbCrLf & Err.Description 'その他のエラーだった場合
        End If
      End Sub
      

      17行目までは同じです。20行目で指定のプロパティ名を変数に格納しています。今回はかんたんにInputBoxにしちゃいましたが、間違い防止でコンボボックスとかのほうが親切だと思います。27行目でCallByNameを使って指定のプロパティの値を取得しています。

      なお、このコードでは Name, Price, Number, Sale の4つのプロパティが有効で、それ以外の文字列が入った場合は27行目でエラーになりますので、32行目以降のエラートラップで処理します。無効なプロパティが入った場合のエラー番号は438なので、そこでメッセージボックスで注意を促す形です。

      なお、上のコメントで質問を受けて書いた回答記事では、プロパティを変数で2つ指定して、プロパティ1に対して条件を設け、それを満たすプロパティ2の平均値を出すという、なかなかにマニアックなことをしてますので、よろしければそちらもなにか参考になるかもしれません。

  3. 名無し より:

    コメントありがとうございます。

    CallByName というものがあるんですね。
    まさに思い描いていた機能で、動作させてうれしくなりました。

    リンク先の内容もとても興味深いです。
    想定していたデータ処理にとても役立ちそうです。

    ありがとうございました。

    • *you より:

      こんにちは、お返事ありがとうございます! お役に立てたようで光栄です(*゚ω゚*)


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

コメントは承認制ですので、反映までしばらくお待ち下さい。(稀にスパムの誤判定にて届かないこともあるようですので、必要な際はお問い合わせからお願い致します。)