Withブロック使用上の注意

Withでまとめるときの注意

実験1

たとえば、ワークシート上に、

f:id:akashi_keirin:20170415072003j:plain

こんな表を作ったとする。知性のかけらもないのは許してほしい。

んで、この表に対して、

リスト1
Public Sub test01()
  Dim sh As Worksheet
  Set sh = ActiveSheet
  sh.Range("A1").CurrentRegion.Sort _
                                key1:=sh.Range("A1"), _
                                order1:=xlAscending, _
                                Header:=xlNo    '……(1)'
  sh.Range("A7").Value = "デコスケ"    '……(2)'
  sh.Range("A1").CurrentRegion _
                    .Borders.LineStyle = xlContinuous    '……(3)'
End Sub

こんなコードを実行したとする。

(1)の

sh.Range("A1").CurrentRegion.Sort _
                                key1:=sh.Range("A1"), _
                                order1:=xlAscending, _
                                Header:=xlNo

でA1セルを含むアクティブセル領域を昇順に並べ替えて、

(2)の

sh.Range("A7").Value = "デコスケ"

でA7セルに「デコスケ」と書き込み、

(3)の

sh.Range("A1").CurrentRegion _
                    .Borders.LineStyle = xlContinuous

でA1セルを含むアクティブセル領域に格子罫線を施しているので、

f:id:akashi_keirin:20170415072010j:plain

当然こうなる。

実験2

で、リスト1を次のように書き換えてみる。

スト2
Public Sub test02()
  Dim sh As Worksheet
  Set sh = ActiveSheet
  With sh.Range("A1").CurrentRegion    '……(1)'
    .Sort key1:=sh.Range("A1"), _
          order1:=xlAscending, _
          Header:=xlNo
    sh.Range("A7").Value = "デコスケ"    '……(2)'
    .Borders.LineStyle = xlContinuous    '……(3)'
  End With
End Sub

要するに、

sh.Range("A1").CurrentRegion

を2回も書くのがうっとうしいので、Withでまとめたわけだ。ちょうど、共通因数を括り出したような感じだな。

んで、こいつを実行すると、

f:id:akashi_keirin:20170415072021j:plain

こうなる。オーマイガ━━(゚Д゚;)━━ン!

原因

リスト2の(1)、

With sh.Range("A1").CurrentRegion

の時点で、CurrentRegionプロパティには、A1~A6セルの範囲がセットされている。

んで、(2)の

sh.Range("A7").Value = "デコスケ"

で、A7セルに値が入るので、CurrentRegionプロパティがA1~A7セルになりそうなもんなんだが、(3)の

.Borders.LineStyle = xlContinuous

の実行結果
f:id:akashi_keirin:20170415072021j:plain
からすると、CurrentRegionプロパティはA1~A6セルのまんま。

つまり、オブジェクトの取得はWithでまとめたそのときだけ、ということなんだな。

まあ、だからこそ計算(=プロパティの取得)回数が減らせるわけで、ごくごく当たり前のことなんだが、この程度のことでちょっと(ほんのちょびっとですけど)ハマったので、覚書として残しておく。

おわりに

With~End Wtihは共通因数でくくるみたいなもん

と思っていましたけど、そういう雑な理解だと失敗するよ、というお話でした。

@akashi_keirin on Twitter

文字列のカッコで括られた部分だけを狙い撃ちで削除するマクロ (2)

ネストされたカッコにも対応する

前回の記事が、この過疎ブログには珍しく反響があったので、ちょっと追加。

Replaceメソッドを使う方あり、Split関数を使う方あり、果ては正規表現を使うツワモノまで現れる始末……。

こうなったら、私も意地になって改良を加えますよー!

前回のコードの欠点

始めカッコがあったらスイッチオン、終わりカッコに出会ったらスイッチオフ、という単純なつくりなので、

f:id:akashi_keirin:20170410234627j:plain

たとえばこんなふうにカッコが設置されていたら、

f:id:akashi_keirin:20170410234635j:plain

途端に破綻……orz

そこで、少しだけコードに改良を加える。

改良したコード

リスト1
Sub deleteContents()
  Dim objCell As Range
  Dim objStr As String
  For Each objCell In Selection
    objStr = objCell.Value
    objCell.Value = _
      deleteContentsEnclosedByBracket _
                      (objStr, "(", ")")
  Next
End Sub

Private Function deleteContentsEnclosedByBracket _
                  (ByVal objStr As String, _
                   ByVal startBracket As String, _
                   ByVal endBracket As String) As String
  Dim enableToDelete As Boolean
  Dim tmp As String
  Dim chr As String
  Dim i As Integer
  Dim n As Integer
  n = 0
  For i = 1 To Len(objStr)
    chr = Mid(objStr, i, 1)
    If chr = startBracket Then
      n = n + 1    '……(1)'
      enableToDelete = True
    End If
    If chr = endBracket Then
      n = n - 1    '……(2)'
      If n = 0 Then    '……(3)'
        enableToDelete = False
        chr = ""
      End If
    End If
    If enableToDelete = False Then
      tmp = tmp & chr
    End If
    If enableToDelete = True Then
    End If
  Next
  deleteContentsEnclosedByBracket = tmp
End Function
リスト1の説明

改良ポイントは、

カウンタ式フラグ n を導入した

こと。

まず、(1)のところ。

If chr = startBracket Then
  n = n + 1
  enableToDelete = True
End If

開始カッコと出会うたびに n をインクリメントする。

んで、(2)と(3)。

If chr = endBracket Then
  n = n - 1    '……(2)'
  If n = 0 Then    '……(3)'
    enableToDelete = False
    chr = ""
  End If
End if

終わりカッコに出会うたびに、逆に n をデクリメントする。これが(2)。

で、n が 0 になっていたら、削除可能フラグをOffにした上で、終わりカッコを""にする。

たとえば、対象文字列が ( (ち~んw) )! だったとすると、

  • Forループ1回目で「(」に出会うので、削除可能フラグenableToDeleteはTrue、n が1になる
    削除フラグOnなので、「(」はtmpには追加されない→消されたように見える
  • Forループ2回目で「(」に出会うので、削除可能フラグenableToDeleteはTrue、n が2になる
    削除フラグOnなので、「(」はtmpには追加されない→消されたように見える
  • Forループ3回目
    削除可能フラグenableToDeleteがTrueなので「ち」はtmpに追加されない→消されたように見える
  • Forループ4回目
    削除可能フラグenableToDeleteがTrueなので「~」はtmpに追加されない→消されたように見える
  • Forループ5回目
    削除可能フラグenableToDeleteがTrueなので「ん」はtmpに追加されない→消されたように見える
  • Forループ6回目
    削除可能フラグenableToDeleteがTrueなので「w」はtmpに追加されない→消されたように見える
  • Forループ7回目で「)」に出会うのが、n が1になるだけなので、削除可能フラグenableToDeleteはTrueのまま
    削除可能フラグenableToDeleteがTrueなので「)」はtmpに追加されない→消されたように見える
  • Forループ8回目で「)」に出会い、今度は n が 0 になるので、削除可能フラグenableToDeleteがFalseになる
    削除フラグOffになるが、「)」は""になるのでtmpには追加されない→消されたように見える
  • Forループ9回目、enableToDeleteはFalseになっているので、「!」はtmpに追加される

という流れで、カッコ内の文字列が除去され、「!」だけが残ることになる。

実行結果

f:id:akashi_keirin:20170410234627j:plain

この状態で実行すると、

f:id:akashi_keirin:20170410234643j:plain

ほれ、この通り。カッコがネストされていても望む結果が得られた。

おわりに

コレ、むきになって更新するようなことなのかね……???

@akashi_keirin on Twitter

名簿作りマクロ(2)

文字列をコード番号に置き換えて取得する

前回

akashi-keirin.hatenablog.com

のつづき。

VLOOKUPのちょっと邪道な(?)使い方

一覧表の中の文字列をコード番号に変換するのには、VLOOKUPを使うというのが一般的だと思う。

ただ、私は、一覧表にずらずらとVLOOKUPの数式が並んでいるのがどうもイヤなんですよねー。

そこで、VLOOKUPを限定的に使うという方法をよく使う。

まあ、そもそもそんなことしなくて済むようにデータ集めりゃいいんですけどね。

で、どうすんの?

とにかく、対応する値さえ取得できりゃいいんだから、

セルの名前:PrefName

f:id:akashi_keirin:20170409210537j:plain

ここには都道府県名を突っ込みます。

セルの名前:PrefNumber

f:id:akashi_keirin:20170409210549j:plain

このセルにはVLOOKUPの数式を入れていて、"PrefName"セルの値をセル範囲"PrefTableReverse"で表引きした値がこのセルに返ります。

セルの名前:RacingStyle

f:id:akashi_keirin:20170409210601j:plain

ここには戦法名を突っ込みます。

セルの名前:RacingStyleNumber

f:id:akashi_keirin:20170409210611j:plain

上と同様、"RacingStyle"セルの値をセル範囲"RacingStyleTableReverse"で表引きした値がこのセルに返ります。

こんなふうに、値取得専用のセルを準備して、参照用のセルに値を突っ込んでは、返り値用のセルで必要な値を取得する(どうでもいいけど分かりにく表現だな、オイ)ことを繰り返すわけです。

コード番号取得用クラス

やはりここでも無駄にクラスモジュールを使いますよ。

例によってクラスモジュールを挿入して、オブジェクト名を「CodeGetter」にする。

リスト1-1 フィールド・アクセサ部分
Option Explicit
'フィールド
Private prefNum_ As Integer
Private styleNum_ As Integer
'アクセサ
Public Property Get prefNum() As Integer
  prefNum = prefNum_
End Property
Public Property Get styleNum() As Integer
  styleNum = styleNum_
End Property

まあ、何の変哲もないコード。

例によって、コンストラクタはなし。

リスト1-2 メソッド部分
'メソッド
Public Sub getCode(ByVal pStr As String, _
                   ByVal sStr As String)    '……(1)
  Range("PrefName").Value = pStr    '……(2)
  prefNum_ = Range("PrefNumber").Value    '……(3)
  Range("RacingStyle").Value = sStr    '……(4)
  styleNum_ = Range("RacingStyleNumber").Value    '……(5)
End Sub
リスト1-2の説明

(1)の

Public Sub getCode(ByVal pStr As String, _
                   ByVal sStr As String)

で引数を2つ設定している。第1引数は都道府県名、第2引数が戦法名

まず(2)では、

Range("PrefName").Value = pStr

と、引数で受け取った文字列を、"PrefName"セルにセット。

すると、"PrefNumber"セルのVLOOKUP関数が表引きの結果である都道府県コード番号を返すので、

prefNum_ = Range("PrefNumber").Value

と、その値を即クラス内の仮変数prefNum_にセット。prefNum_にセットされた値は、prefNumプロパティが参照されたときにprefNumプロパティの値として呼び出されることになる。

戦法についても、(4)、(5)で同様に戦法コードをクラス内の仮変数styleNum_にセット。

要するに、CodeGetterクラスのインスタンス都道府県名と戦法名を渡してgetCodeメソッドを実行したら、CodeGetterクラスのインスタンス都道府県コードと戦法コードの2つの値を保持するようになる、ということだ。

CodeGetterクラスを利用するコード

こちらは、標準モジュールに書く。

スト2
Dim cdGetter As CodeGetter
Set cdGetter = New CodeGetter
With gblRacer
  cdGetter.getCode .belongsTo, .rcStyle    '……(1)
End With

「gblRacer」は、前回ご紹介したGambleRacerクラスのインスタンス。belongsToプロパティ、rcStyleプロパティには、それぞれ都道府県名、戦法名がセットされている。

だから、(1)は、getCodeメソッドに都道府県名と戦法名を渡して実行しているということ。

(1)の実行後は、CodeGetterクラスのインスタンス「cdGetter」が都道府県番号を戦法番号を保持していることになる。

次回予告

今回のように、一時的なデータの置き場所として目に見えるセルが使える、というのがVBAの強みかも知れない。

それに、そこそこ大規模な表だと、VLOOKUPだらけにするとなんか重たい感じがしてイヤなので、こんなやり方で値だけを転記することが多い。まあ、ちょっとした力技なのかも知れませんが。

さて、次回は、前回作成したGambleRacerクラスと、今回のCodeGetterクラスを用いて、いよいよ名簿作りマクロを完成させます。

コチラもどうぞ!

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

名簿作りマクロ(1)

人物データを元に名簿を作成するマクロ

配置転換で行った先で見たもの

新年度の配置転換で、新しい部署に行くことになった。んで、早速名簿作りをしないといけなくなった。

だいたいこんな感じのものが出てきたと思ってください。

f:id:akashi_keirin:20170409201614j:plain

f:id:akashi_keirin:20170409201629j:plain

どうやら、

f:id:akashi_keirin:20170409201644j:plain

こんな元データを作って、それをいろんな条件で並び替えてコピペすることによって、注目するキーごとの名簿を作っていたみたいだ。あ、もちろん画像はイメージですよ。実際にはもっとたくさんパラメータのある大がかりな表です。

せいぜい200人分ぐらいの名簿だから、大したことないといえば大したことのない名簿作りだけど、よくやるよ、ホントに。

ナマケモノの私は、もちろんマクロでやることを考えましたよ。たとえ1回こっきりでもマクロでやりますね。だって、その方が楽しいから(キリッ

方針

もちろん、クラスモジュール入門者なので、今回も無駄にクラスモジュール使いますよ。

それぞれのワークシートはこんな感じ。

f:id:akashi_keirin:20170409210413j:plain

f:id:akashi_keirin:20170409210428j:plain

f:id:akashi_keirin:20170409210440j:plain

んで、「参照」と名付けたシートのB1~E1セルには、

f:id:akashi_keirin:20170409210537j:plain

f:id:akashi_keirin:20170409210549j:plain

f:id:akashi_keirin:20170409210601j:plain

f:id:akashi_keirin:20170409210611j:plain

こんな風に、名前を定義したり、数式を入れたりしている。

処理の手順としては、

  1. 選手を表すクラスを作る
  2. 「選手データ」シート1行分のデータを選手クラスのインスタンスに持たせる
  3. 「戦法別」または「都道府県別」シートにデータを転記する
  4. 全部転記し終わったらそのシートにふさわしいやり方でデータを並べ替える

とまあ、こんなところか。

選手クラスを作る

リスト1

クラスモジュールを挿入して、オブジェクト名は「GambleRacer」にする。

フィールド
Option Explicit
'フィールド'
Private rcName_ As String    '……選手名'
Private rcPhonetic_ As String    '……ふりがな'
Private belongsTo_ As String    '……都道府県'
Private prefNum_ As Integer    '……都道府県No.'
Private graduateTerm_ As Integer    '……卒業期'
Private rcGrade_ As String    '……級'
Private rcClass_ As String    '……班'
Private rcStyle_ As String    '……戦法'
Private styleNum_ As Integer    '……戦法No.'
Private isEliminated_ As Boolean    '……失格フラグ'

……とまあ、選手のパラメータです。

アクセサ
'アクセサ'
Public Property Get rcName() As String
  rcName = rcName_
End Property
Public Property Get rcPhonetic() As String
  rcPhonetic = rcPhonetic_
End Property
Public Property Get belongsTo() As String
  belongsTo = belongsTo_
End Property
Public Property Get prefNum() As Integer
  prefNum = prefNum_
End Property
Public Property Get graduateTerm() As Integer
  graduateTerm = graduateTerm_
End Property
Public Property Get rcGrade() As String
  rcGrade = rcGrade_
End Property
Public Property Get rcClass() As String
  rcClass = rcClass_
End Property
Public Property Get rcStyle() As String
  rcStyle = rcStyle_
End Property
Public Property Get styleNum() As Integer
  styleNum = styleNum_
End Property
Public Property Get isEliminated() As Boolean
  isEliminated = isEliminated_
End Property

……とまあ、何の変哲もないコードです。例によってLetは作っていない。値のセットについては、別途メソッドを作る方が性に合っている気がする。これは完全に個人の好みです。

コンストラク

今回もありません。

メソッド
'メソッド'
Public Sub setData(ByRef rcData As Racer)
  With rcData
    rcName_ = .rcName
    rcPhonetic_ = .rcPhonetic
    belongsTo_ = .belongsTo
    graduateTerm_ = .graduateTerm
    rcGrade_ = .rcGrade
    rcClass_ = .rcClass
    rcStyle_ = .rcStyle
    isEliminated_ = .isEliminated
  End With
End Sub

Public Sub setCode(ByVal pNum As Integer, _
                   ByVal sNum As Integer)
  prefNum_ = pNum
  styleNum_ = sNum
End Sub

メソッドは2つ。一覧表から直に値を渡すことができるやつは、setDataメソッドで。ちなみに、メソッドの引数は自作の構造体で渡している。めんどくさいけど、書きやすさと読みやすさはバツグン。……ていうか、ここまでするんなら、いっそクラスをこのクラスのフィールドにしてしまえば良いような気もする。

んで、2つ目のsetCodeメソッドなんですが、元の表が、都道府県とか戦法なんかをベタの文字列で表しているのがそもそもの間違いなんですよねー。後で並べ替えのキーにすることが分かってるんだから、初めから振り番しといて、別の「都道府県番号表」とか「戦法番号表」なんかと紐付けしてりゃよかったんですよ。

データベースのなんたるかが全く分かってないやつ(まあ、私も大して分かってやしませんが)がデータ処理の基礎部分を作ってしまうと、後任の者が異様に苦労する、ということですな。

おっと、話がそれてしまった。

ともかく、都道府県と戦法については、後で並べ替えのキーにするために数値化しないといけない。そのために、「参照」シートでVLOOKUPなんか使っているわけなんですよ。

いや、もちろん、元データの表にVLOOKUPをゴリゴリ押し込むこともできるんですけど、VLOOKUPだらけの表ってなんか品がないような気がするんですよ。

スト2 標準モジュールの宣言セクション

GambleRacerクラスのsetDataメソッドに引数を大量に渡すのがメンドクサイので、基礎データにあたる部分を構造体にして渡すことにした。その宣言が以下のコード。

Public Type Racer
  rcName As String    '……名前'
  rcPhonetic As String    '……ふりがな'
  belongsTo As String    '……都道府県'
  graduateTerm As Integer    '……卒業期'
  rcGrade As String    '……級'
  rcClass As String    '……班'
  rcStyle As String    '……戦法'
  isEliminated As Boolean    '……失格フラグ'
End Type

Dim racerData As Racer

……とまあ、こんな感じ。

次回予告

後は、GambleRacerクラスのインスタンスに、

  1. setDataメソッドを使って「選手データ」シートの各データをセットし、
  2. setCodeメソッドを使って都道府県番号、戦法番号をセットし、
  3. 「戦法別」または「都道府県別」シートに各パラメータを書き込ませる

ということをデータの数だけ繰り返したら、一覧表が完成することになる。

クラスって、作るまでが結構メンドウだけれど、一旦作ってしまうと、その後のプログラミングとメンテナンスが異様に楽になる。まだまだちゃんと使いこなせているわけではないけど、十分メリットを感じている。

コチラもどうぞ!

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

ループ処理で何もせずにカウンタだけ前に進める

何もせずに次のループに進む

ForループやDoループを書いているとき、

先頭で条件判定して、当てはまっていたら何もせずに次のループに進むことができたらいいのに

と思うことがちょいちょいある。

もちろん、たとえば、

リスト1
For i = 1 to hogehoge
  If foo = "bar" Then
    '処理1
    '処理2
    '処理3
    '処理4
       ・
       ・
       ・
       ・
       ・
  End If
Next

こんな風にしてやれば良いというのは分かっているんだけど、処理が結構な行数に及ぶ場合、Ifブロックがタテに長~くなるのはちょっとイヤなんですよねー。

For(Do)ループ先頭にガード節を置く

最近、ちょっとハマっている「ガード節」という考え方。

入り口のところで門前払いを食らわしてしまう、というのはIfブロックと同じなんですけどね。

たとえば、

スト2
  For i = 1 to hogehoge
    If foo = "bar" Then GoTo myJump    '……(1)
    '処理1
    '処理2
    '処理3
    '処理4
       ・
       ・
       ・
       ・
       ・
myJump:    '……(2)
  Next

こんな感じ。

(1)がガード節に当たる部分で、条件(この場合は変数fooの値が文字列「bar」だったらというもの)に当てはまっていたらNextの直前までワープさせる、というやり方で、処理1、処理2……をすっ飛ばしてiをインクリメントする、という処理を実現している。

おわりに

う~ん……。GoToの行き先になるラベルってインデントが効かない(強制的に左端に戻される)から、For~Nextブロックの視認性が落ちてしまうんだよなあ……。

何かいい方法があったら教えください。

@akashi_keirin on Twitter

文字列のカッコで括られた部分だけを狙い撃ちで削除するマクロ

セル内のカッコで括られた文字列のみ削除する

都道府県番号の一覧表

ひょんなことで、都道府県番号の一覧表が欲しいなあと思ってggってみたら、

f:id:akashi_keirin:20170409102348j:plain

こんな一覧ばっかり(画像はWikipediaのものです)で、表形式のまま取り込めるものがなかなか見つからなかった。

やっとのことで、

f:id:akashi_keirin:20170409102403j:plain

こんなページを見つけて、Excelの表に貼り付けたんだが、

f:id:akashi_keirin:20170409102426j:plain



カッコの中の読み仮名がジャマ!!!!!!!!

なんですよねー。

まあ、しょせん47都道府県のことだから、手作業でやっても大して時間はかからないわけなんですが、マクロでやりましたよ。

カッコで括られた部分のみ削除するマクロ

今回は、次のような感じで組み立ててみました。

処理の流れ
  • 文字列の文字数を取得して文字数分ループ
  • 文字列の先頭から1文字づつ調べる
  • 開始カッコのところまでは順に連結
  • 開始カッコのところに来たら削除フラグOn
    削除フラグがOnの間は文字を連結しない
  • 終了カッコのところまで来たら削除フラグOff
    削除フラグOffの間は文字を連結する
  • できあがった文字列をセルに返す

これを、For Each ~ In Selectionで回す、というちょい書きマクロ。

リスト1-1

まずは、メインのコード。

Sub deleteContents()
  Dim objCell As Range
  Dim objStr As String
  For Each objCell In Selection    '……(1)'
    objStr = objCell.Value    '……(2)'
    objCell.Value = _
      deleteContentsEnclosedByBracket _
                      (objStr, "(", ")")    '……(3)'
  Next
End Sub
コードの説明

(1)の

For Each objCell In Selection
  '処理の内容
Next

は、選択範囲の各セルに処理を施すというやつで、今回のようなちょい書きマクロでは非常によく使う方式(って、私だけ……?)。

処理を施す範囲を柔軟に設定できるので、重宝している。

(2)の

objStr = objCell.Value

では、一旦変数にセルの中身をセット。

んで、(3)では、自作のdeleteContentsEnclosedByBracket関数(ってか、長ぇ名前だな、オイ!)に文字列を渡して「カッコでくくった文字列を削除する」処理をさせている。

別に、この程度の処理なので外に括り出すまでもなかったかも知れないけれど、もしかしたらあとあと拡張できるかもしれないと思ったので、今回もYAGNYの原則に反する対応をとってしまった……orz

まあ、Forループがネストするのはできれば避けたいというのもある。

リスト1-2

コチラは、呼び出されるdeleteContentsEnclosedByBracket関数のコード。

Private Function deleteContentsEnclosedByBracket _
                  (ByVal objStr As String, _
                   ByVal startBracket As String, _
                   ByVal endBracket As String) As String    '……(1)'
  Dim enableToDelete As Boolean    '……(2)'
  Dim tmp As String
  Dim chr As String
  Dim i As Integer
  For i = 1 To Len(objStr)    '……(3)'
    chr = Mid(objStr, i, 1)    '……(4)'
    If chr = startBracket Then    '……(5)'
      enableToDelete = True
    End If
    If chr = endBracket Then    '……(6)'
      enableToDelete = False
      chr = ""    '……(*)'
    End If
    If enableToDelete = False Then    '……(7)'
      tmp = tmp & chr
    End If
    If enableToDelete = True Then    '……(8)'
    End If
  Next
  deleteContentsEnclosedByBracket = tmp    '……(9)'
End Function
リスト1-2の説明

まず、(1)。

Private Function deleteContentsEnclosedByBracket _
                  (ByVal objStr As String, _
                   ByVal startBracket As String, _
                   ByVal endBracket As String) As String

第1引数のobjStrが、処理対象の文字列。

第2引数のstartBracketは、開始カッコ。

第3引数のendBracketは、終了カッコ。

引数名がやたら長いのはいつものことなんですが、意味の取れる引数名にしようとするとどうしてもこうなっちゃうんですよねー。

要するに、第1引数で渡された文字列に対して、第2・3引数で渡された文字で括られた文字列を削除して返す、という処理をするわけです。

当然、第2引数や第3引数に2文字以上の文字列を渡されたら困るわけですが、ちょい書きなので……。

(2)からの4行

Dim enableToDelete As Boolean
Dim tmp As String
Dim chr As String
Dim i As Integer

は変数の宣言。いちおう、

  • enableToDelete……削除可能フラグ
  • tmp……一時的に文字列を入れておく
  • chr……処理対象文字列から取り出した1文字を入れておく
  • i……Forループ用のカウンタ

といったところ。

(3)では、

For i = 1 To Len(objStr)
  '処理の内容
Next

の形で受け取った文字列の文字数分ループ処理を行う。Len関数は、

Len(文字列

の形で文字列の文字数を返してくれる。

(4)では、

chr = Mid(objStr, i, 1)

Mid関数を用いて、objStrで渡した文字列の中からi番目1文字を変数chrにセットしている。

Mid関数は、

Mid(文字列,開始位置,切り出したい文字数)

の形で、文字列の[開始位置]番目から[切り出したい文字数]文字分の文字列を返してくれる。

んで、(5)と(6)では、

If chr = startBracket Then
  enableToDelete = True
End If
If chr = endBracket Then
  enableToDelete = False
  chr = ""    '……(*)
End If

切り出した1文字が開始カッコまたは終了カッコかどうかを判定して、削除可能フラグのOn / Offを切り替えるようにしている。Elseを使えば行数を減らせるのは分かっていますが、最近は極力Elseを使わないようにしているので。

ちなみに、(*)を忘れると、けったいな結果になるので注意。当たり前ですが。

(7)の

If enableToDelete = False Then
  tmp = tmp & chr
End If

は、削除可能フラグがOffの場合の処理。普通に切り出した1文字を追加しているだけ。

(8)の

If enableToDelete = True Then
End If

は、別にいらないんだけど、あえて書いている。削除可能フラグOnのときは何もしない、ということ。

削除可能フラグOnの間に出てくる文字列は削除対象なので、追加しない、という形で削除を実現。

で、(9)。

deleteContentsEnclosedByBracket = tmp

Forループを抜けたということは、全文字に対する処理が終わったということだから、できあがった文字列tmpを呼び出し元に戻り値として返してやる。

実行結果

f:id:akashi_keirin:20170409102452j:plain

範囲を選択して実行すると、

f:id:akashi_keirin:20170409102500j:plain

ほれ。カッコで括られていた文字列が消えた。

おわりに

こういうちょっとした処理がサクサクっと書けるようになると、Excelを使った作業系の仕事は激速になると思う。

@akashi_keirin on Twitter

脱・初心者のために(1)

私が脱・初心者を自覚した瞬間

……といっても、ある瞬間にスイッチが入ったように「今日を以て初心者を卒業します。私のことを嫌いになっても、初心者のことは嫌いにならないでください!」とか思ったわけではない。

何度も何度も、それはもう何度も何度も、Time After Time……ですよ。「ぼちぼち初心者の域はだっしたかなー」、「いやいや、こんなことも分かっていないようじゃあまだまだだな……」というのを何度も繰り返して今に至るわけです。

今回は、そのたくさんある瞬間のうちの一つ、ということでご理解ください。

「値渡し」と「参照渡し」

コレ、最初何のことだかサッパリ分からなかった。

たいていの本には、

「値渡し」は、変数のコピーを呼び出した側のプロシージャに渡します。
「参照渡し」はその変数への参照を呼び出した側のプロシージャに渡します。

とかいうようなことが書いてある。

これ、最初全然意味が分からなかった。「ByVal」と「ByRef」を使い分けると何がうれしいのか分からなかったんだな。

で、たいていの本には、「とりあえず引数を受け取る側ではByValつけときゃいい」みたいな投げやりなアドバイスが載っていたりする。まあ、それでたいてい問題はないし、「ByVal」で渡せない引数(オブジェクト系)だとコンパイルエラーが出て実行させてもらえないから、「とりあえずByVal」で行けてしまう。

でもねえ……。

プログラミングというのは論理的思考の権化みたいなもんなんだから、そこを「おまじない」みたいな理解でごまかすのは良くないと思う。

で、どうすんの?

使い分けることで何がうれしいのか、については私もよく分かっていないが、「値渡し」と「参照渡し」がどう違うのか、というのはちゃんと理解しておいた方が良いと思う。

そもそも、「引数」ってのはよく料理やなんかの「材料」にたとえられる。メソッドとかプロシージャが「調理」という処理で、「調理」に必要な「材料」が「引数」というわけだ。

「引数」というもののイメージをつかむにはこれで問題ないと思うんだが、その理解しかないと、「値渡し」だの「参照渡し」だのといったときにつまづくもとだと思う。

「値渡し」と「参照渡し」の違い

「処理の材料」という意味では、値渡しにしようが参照渡しにしようがどっちでもいい。ただ、渡し方というか、「渡す」ということの意味が違う。

変数hogeに、「ち~んw」という文字列が入っているとしよう。

VBAのコードだと、

Dim hoge As String
hoge = "ち~んw"

こういう状態だな。

で、この「ち~んw」という文字列を「値渡し」にする場合と「参照渡し」にする場合とで何が違うのか、ということだ。

結論から述べる。渡しているものが違う。見た目は同じでも。

へ??? どういうこと?

次のコードを実行したら、どうなるだろうか。

リスト1
Sub hogeCaller()
  Dim hoge As String
  hoge = "ち~んw"
  Call hogeCalledByVal(hoge)
  Call hogeCalledByRef(hoge)
End Sub

Sub hogeCalledByVal(ByVal str As String)
  MsgBox str
End Sub

Sub hogeCalledByRef(ByRef str As String)
  MsgBox str
End Sub
リスト1の実行結果

f:id:akashi_keirin:20170402215641j:plain

まずはこいつが表示され、[OK]をクリックしたら、

f:id:akashi_keirin:20170402215650j:plain

こいつが表示される。

1回目のメッセージボックスと、2回目のメッセージボックスは、全く同じものに見えるし、実際同じものだ。

しかし、1回目の「ち~んw」と2回目の「ち~んw」の意味合いはまるで違う。

だから、何が違うのさ?

まず、hogeCalledByValに渡された「ち~んw」。こいつは、

純粋な文字列としての「ち~んw」

だ。

一方、hogeCalledByRefに渡されたのは、

ただの文字列「ち~んw」ではない

ということだ。

じゃあ、何なのか。hogeCalledByRefに渡されたのは、

変数hogeの中身としての「ち~んw」

ということだ。

といっても、(゚Д゚)ハァ? だろう。もうちょっと説明する。

「値渡し」の場合、渡された時点で「ち~んw」という文字列には、もはや「変数hogeの中身」という意味合いはない。「純粋な文字列」と言ったのはそういうことだ。

それに対して、「参照渡し」の場合は、文字列を渡しているのではない。ざっくり言うと、

変数hogeが使っているメモリの番地を教えている

のだ。

たとえば、変数hogeがメモリの1丁目1番地に値を保持しているとしたら、この場合「ち~んw」という文字列がメモリの1丁目1番地に保存されていることになる。

変数hogeを「参照渡し」にするということは、

そっちの処理で材料がいるって言うからくれてやるぜ!
ほれ! 中身が知りたきゃメモリの1丁目1番地にあるから好きに使いな!

という感じだ。

リスト1の場合、変数hogeの中身は「ち~んw」だから、確かに「ち~んw」を渡しているように見えるし、その通りなんだが、「参照渡し」の場合は、どこまでも

変数hogeの中身としての「ち~んw」

ということだ。

だから、参照渡しにした場合、渡した先で引数を加工すると、当然変数hogeの中身そのものが加工されることになる。

で、何なの?

たとえば、リスト1を次のように書き換えてみる。

スト2
Sub hogeCaller()
  Dim hoge As String
  hoge = "ち~んw"    '……(1)
  Call hogeCalledByRef(hoge)    '……(2)
  MsgBox hoge    '……(6)
End Sub

Sub hogeCalledByRef(ByRef str As String)
  MsgBox str    '……(3)
  str = "(゚Д゚)ハァ?"    '……(4)
  MsgBox str    '……(5)
End Sub

こいつを実行するとどうなるか。

リスト2の実行結果

f:id:akashi_keirin:20170402215655j:plain

まずはこいつが出てくる。

f:id:akashi_keirin:20170402215702j:plain

次はこいつ。

f:id:akashi_keirin:20170402215709j:plain

んで、こうなる。

リスト2の説明

カラクリはこうだ。

まず、(1)の

hoge = "ち~んw"

で、変数hogeに「ち~んw」が代入される。

次に、(2)でhogeをhogeCalledByRefに渡して処理をさせるわけだが、参照渡しなので、

hogeCalledByRefにhogeが値を保持しているメモリ上の位置を教えている

ことになる。

ここで処理がhogeCalledByRefに移る。hogeCalledByRefでは、変数strで引数を受け取るわけだが、「参照渡し」で受け取っているので、

strの中にはhogeの値を保持しているメモリ上の位置情報が入っている

と思えば良い。

だから、(3)の

MsgBox str

で、メッセージボックスに表示するためにプロシージャがstrの中身を取得しようとするが、そこにあるのは変数hogeのメモリ番地情報なので、そこを見に行って文字列「ち~んw」を得る。

だから、1回目のメッセージボックスには「ち~んw」が表示される。

その後、(4)の

str = "(゚Д゚)ハァ?"

で、strに「(゚Д゚)ハァ?」を代入している。代入しているといっても、strの正体はhogeの参照先なので、当然、

変数hogeの値を保持するメモリ上の位置に文字列「(゚Д゚)ハァ?」が書き込まれる

ことになる。

だから、(5)の

MsgBox str

を実行すると、メッセージボックス(2回目)には「(゚Д゚)ハァ?」が表示される。

ここで、hogeCalledByRefプロシージャが終わるので、処理が元のhogeCallerに戻る。

んで、(6)の

MsgBox hoge

で、メッセージボックスに表示するためにプロシージャがhogeの中身を得ようとするのだが、hogeが参照しているメモリ上の位置には、既にhogeCalledByRefプロシージャ内の(4)で「(゚Д゚)ハァ?」が書き込まれているので、当然プロシージャはhogeの値として「(゚Д゚)ハァ?」を得て、メッセージボックス(3回目)に「(゚Д゚)ハァ?」を表示する。

ざっと、こんな理屈で処理が進んでいたわけだ。

まとめ

このような理屈なので、基本的に値だけしか持たない変数を参照渡しにする意味はまるでないと思う。

変数の中身をいじくりたいのなら、変数を宣言したプロシージャ・メソッド内でやるべきであり、わざわざスコープ外でやる意義が見いだせないからだ。

んじゃ、なんで「参照渡し」なんてものがあるのか?

現時点での素人考えだけれど、

値渡しのしようがないものがある

からだと思う。

簡単な例だと、Excelのとあるセルを引数にしたいとき、

セルを値渡しにする

なんて意味不明でしょ?

「オブジェクト」レベルのものになると「値渡し」なんてしようがない。やりたくてもできない。だから、

Sub hogeHoge(ByVal cell As Range)

とか書いても、コンパイルエラーになって実行すらさせてくれないのだろう。

逆に、「整数」とか「文字列」といったものなら、

純粋な単独データ

として存在しうる。

そもそも「整数」とか「文字列」といったプリミティブなデータについて「値渡し」とか「参照渡し」について議論すること自体が無意味なんじゃないのかなあ……?

「値渡し」なんてしようのないデータ型があるから、「参照渡し」という概念が存在して、プリミティブなデータ型についても「参照渡し」自体はできるから「値渡し」も「参照渡し」もできるようになっている、そういうふうに理解した方がいいんでないか。

改めて入門者向けの書籍の「値渡し」・「参照渡し」の箇所を読んでみてそう思ったのだった。

追記

thom (id:t-hom)さんからのご指摘で、

Sub hogeHoge(ByVal cell As Range)

というのも普通にできると分かった。前に何かで「ByValなんてできねーよ、ハゲ!」みたいなエラーが出たことがあって、ずっと勘違いしていたみたい。でも、分かったつもりになっていた「値渡し」・「参照渡し」がまたまたよく分からなくなってしまった。情けないけど、今後の宿題ということにしよう。

練習問題

次のコードを実行したらどうなるか、考えてみてください。

リスト3
Sub pCaller()
  Dim x As Integer
  x = 10
  Debug.Print "1:xの値は " & x & " ですわ。"
  Debug.Print "2:xを値渡しの引数にしてpCalledWithValプロシージャを呼びまんねん。"
  Call pCalledWithVal(x)
  Debug.Print "5:処理がpCallerに帰ってきたで。xの値は、 " & x & " でんがな。"
  Debug.Print "6:じゃ、今度はxを参照渡しの引数にしてpCalledWithRefプロシージャを呼ぶぜ。"
  x = 10
  Call pCalledWithRef(x)
  Debug.Print "9:処理がpCallerに帰ってきたのう。xの値は、 " & x & " になっとりますの。"
End Sub

Sub pCalledWithVal(ByVal x As Integer)
  Debug.Print "3:こちらpCalledWithVal。今から受け取った" & x & " を10倍しまぁす。" & _
              "STAP細胞はありまぁす。"
  x = x * 10
  Debug.Print "4:こちらpCalledWithVal、受け取った x を10倍したので、xは" & x & "ですわ。"
End Sub

Sub pCalledWithRef(ByRef x As Integer)
  Debug.Print "7:こちらpCalledWithRef。今から受け取った" & x & " ば10倍するばい。"
  x = x * 10
  Debug.Print "8:こちらpCalledWithRef、受け取った x ば10倍したけん、xは" & x & "ばい。" & _
              "まさに、10倍ばい!"
End Sub

実際に、試してみてください。

@akashi_keirin on Twitter