開発環境はWinXP+VB6SP6です。
ユーザー定義型を配列にしたデータがあります。
配列内のデータをインデックス番号をシフトさせる方法にはどのような方法があるでしょうか?
例:)
Data(0)=12
Data(1)=345
Data(2)=67
Data(3)=890
Data(4)=123
↓
Data(0)=0
Data(1)=12
Data(2)=345
Data(3)=67
Data(4)=890
これはただの配列変数ですが、ユーザー定義型の配列変数で同じような事をします。
もちろん、ループをまわしてインデックス+1でもすれば可能なのはもちろんですが、
ループを使わず、高速で安全な方法があれば教えてください。
とりあえず、ミジンコのような脳みそを振り絞ってユーザー定義型でうにゃうにゃやってみました。
Public Const MAX_DATA_NUM = 1000
Public Const Shift_UP = 1
Public Const Shift_2UP = 2
Public Const Shift_Down = -1
Public Const Shift_2Down = -2
Private Type GraphFormat 'この例ではグラフ4本分のデータ
Time As Date
RateE As Single
RateM As Single
RateTcE As Single
RateTcM As Single
End Type
Private Type GraphMemoryFormat
No(MAX_DATA_NUM) As GraphFormat
End Type
Private Type MoveAfterFormat
Data As GraphMemoryFormat
Dummy As GraphFormat
End Type
Private Type MoveBeforeFormat
Dummy As GraphFormat
Data As GraphMemoryFormat
End Type
Private GraphMemoryData As GraphMemoryFormat
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
(Destination As Any, Source As Any, ByVal Length As Long)
Public Sub Form_Load()
Dim i As Long
For i = 1 To MAX_DATA_NUM + 1
With GraphMemoryData.No(i - 1)
.Time = i ' 確認用ダミーデータ
.RateE = i * 10
.RateM = i * 100
.RateTcE = i * 1000
.RateTcM = i * 10000
End With
Next
Call GraphDataShift(Shift_UP)
' Call GraphDataShift(Shift_Down) '逆方向
End Sub
Public Sub GraphDataShift(ByVal Direction As Long)
Private MoveAfter As MoveAfterFormat
Private MoveBefore As MoveBeforeFormat
Select Case Direction
Case Shift_UP
MoveBefore.Data = GraphMemoryData
Call CopyMemory(MoveAfter, MoveBefore, Len(MoveBefore))
GraphMemoryData = MoveAfter.Data
Case Shift_Down
MoveAfter.Data = GraphMemoryData
Call CopyMemory(MoveBefore, MoveAfter, Len(MoveAfter))
GraphMemoryData = MoveBefore.Data
End Select
End Sub
これで一応期待通りの動作はします。
定義が三重ネストになっているのが少々不満ですが。
ただし、問題がありまして、ユーザー定義型の制限で64kByteまでしか定義できないことと、データーの配列数(GraphMemoryFormatの定義)を実行中に変更できない点とです。
まぁ、上限の64kByteのほうはまだいいのですが、あらかじめ MAX_DATA_NUM を多くしすぎると無駄にメモリを消費します。
データの列数が少なくても良いときには、変数のサイズはコンパクトにしたいところです。
それから、CopyMemory API関数はVB6でユーザー定義型に配列が含まれているとメモリリークする、とどこかの質問掲示板に書かれていたのを見かけたのですが、だとすれば上記のコードはちょっとつかえません。本当でしょうか?
ということで、以下の観点から、ほかにいい方法はないでしょうか?
1)ループは使わない(コードの質、量による)
2)DATA_NUMを実行中に変更できるようにしたい
3)変数による消費メモリはデーターの個数に比例したい
4)高速であればあるほどうれしい
全部満たすものというわけではなく、どれかに特化した方法でまったくかまいません。
また、上記のわたしのコードに対する非難轟々(改善点)も大歓迎です。
アクロバティックなウルトラC(コード)でもどんとこいです。
情報お待ちします。
変数名がNoGoodなのは勘弁してくださいな><
メモリコピーはお奨めできません。
ユーザー定義型メンバのアライメント調整によって、
Len(x) = LenB(x) になるようなデータ型の場合に問題が出ますし。
そもそも、ループを使うのは真っ当な解決策だと思うのですが、
それを嫌うのには、何か理由があるのでしょうか?
> 64kByteまでしか定義できないことと
ユーザー定義型でなければいけないのでしょうか? (クラスでは駄目ですか?)
たとえば ADODB.Recordset なら、ソートも検索も簡単に書けますよ。
Option Explicit
Private GraphMemoryData As ADODB.Recordset
Private Sub Form_Load()
'GraphFormat 型のデータ構造を定義
Set GraphMemoryData = CreateGraphFormat()
'100件のランダムデータを追加
FillRandom GraphMemoryData, 100
'確認用に画面に表示
Set DataGrid1.DataSource = GraphMemoryData
Command1.Caption = "ダンプ"
Command2.Caption = "正RateE,逆RateM"
Command3.Caption = "逆Time"
End Sub
Private Sub Command1_Click()
'現在の内容を、イミディエイト ペインに出力
GraphMemoryData.MoveFirst
Debug.Print GraphMemoryData.GetString(, , "|", vbCrLf)
End Sub
'★ ソート手順 ★
Private Sub Command2_Click()
'RateE の昇順、RateM の降順にソート
GraphMemoryData.Sort = "RateE ASC, RateM DESC"
End Sub
Private Sub Command3_Click()
'Time の降順にソート
GraphMemoryData.Sort = "Time DESC"
End Sub
'★ GraphFormat ユーザー定義型のかわり ★
Private Function CreateGraphFormat() As ADODB.Recordset
Set CreateGraphFormat = New ADODB.Recordset
With CreateGraphFormat.Fields
.Append "Time", adDate
.Append "RateE", adSingle
.Append "RateM", adSingle
.Append "RateTcE", adSingle
.Append "RateTcM", adSingle
End With
CreateGraphFormat.Open
End Function
' count 件のランダムなデータを生成
Private Sub FillRandom(ByVal rs As ADODB.Recordset, ByVal count As Integer)
Dim n As Integer
Dim d As Date
d = Now
For n = 1 To count
rs.AddNew _
Array("Time", "RateE", "RateM", "RateTcE", "RateTcM"), _
Array(DateAdd("s", n, d), Random(), Random(), Random(), Random())
Next
End Sub
'-50.0 〜 +50.0 の範囲の乱数を返す
Private Function Random() As Single
Random = Int(500 * Rnd() + -500) / 10
End Function
一か所修正 m(_ _)m
# 本題とは関係ないところですが。
> '-50.0 〜 +50.0 の範囲の乱数を返す
Private Function Random() As Single
Random = Int(1001 * Rnd() + -500) / 10
End Function
読み違えてました。
ソートではなく、固定件数に対する単純なシフトでしたか。
だったら、Recordset なら
For n = 1 To limit
rs.MoveFirst
rs.Delete
rs.AddNew
Next
でシフトできますが…逆シフトは、Sort と組み合わせないと駄目ですね。
# AddNew 直後のレコードは、AbsolutePosition が末尾値にしかならないし。
登録時も利用時もループ無しというのは難しいかな…。
.NET でいうところの、ArrayList や List(Of T) に相当する物が
用意されていれば良いのですが、VB6 でそれに相当するのは無さそうで。
(ListBox や TreeView などは、それに近いことができますが)
さて、どうするか。
※メモリ的に「一つずらす」のであれば、下に書いてあるものは無意味なのでスルーしてください
リングバッファを用いてそれっぽく見せるクラスを定義してはどうでしょうか?
'例
Option Explicit
Public Enum eShiftDirection
shift_Left
shift_Right
End Enum
Private innerArray() As Long
Private innerArraySize As Long
Private start As Long
Public Sub Initialize(ByVal size As Long)
ReDim innerArray(0 To size - 1)
innerArraySize = size
start = 0
End Sub
Private Function idx2innerIdx(ByVal idx_base1 As Long) As Long
idx2innerIdx = (start + (idx_base1 - 1)) Mod innerArraySize
End Function
Public Function size() As Long
size = innerArraySize
End Function
Public Property Get itemOf(ByVal idx_base1 As Long) As Long
If idx_base1 < 1 Or innerArraySize < idx_base1 Then Err.Raise 9
itemOf = innerArray(idx2innerIdx(idx_base1))
End Property
Public Property Let itemOf(ByVal idx_base1 As Long, ByVal nv As Long)
If idx_base1 < 1 Or innerArraySize < idx_base1 Then Err.Raise 9
innerArray(idx2innerIdx(idx_base1)) = nv
End Property
Public Sub shift(ByVal direction As eShiftDirection, ByVal n As Long, Optional ByVal clearValue As Long = 0)
If n < 0 Then Err.Raise 9
If n > innerArraySize Then
'初期化
ReDim innerArray(0 To innerArraySize - 1)
Else
'シフト
Select Case direction
Case eShiftDirection.shift_Left
For n = n To 1 Step -1
start = start - 1: If start < 0 Then start = innerArraySize - 1
innerArray(start) = clearValue
Next
Case eShiftDirection.shift_Right
For n = n To 1 Step -1
innerArray(start) = clearValue
start = start + 1: If start >= innerArraySize Then start = 0
Next
End Select
End If
End Sub
> アクロバティックな
トリッキーな方向に持っていってみました。
> VB6 でそれに相当するのは無さそうで。
JScript の Array オブジェクトを利用したコード。
'-----------
'Class1
Option Explicit
Public Time As Date
Public RateE As Single
Public RateM As Single
Public RateTcE As Single
Public RateTcM As Single
'-----------
'-----------
'Form1
Option Explicit
Private SC As Object
Private List As Object
Private Sub Form_Load()
'50件のコレクション作成
Set SC = CreateObject("ScriptControl")
SC.Language = "JScript"
SC.ExecuteStatement "List=new Array()"
Set List = SC.Eval("List")
Dim n As Integer
For n = 1 To 50
List.push NewClass1()
Next
Command1.Caption = "ダンプ"
Command2.Caption = "2件下シフト"
Command3.Caption = "2件上シフト"
End Sub
'ダンプ
Private Sub Command1_Click()
Dim c As Class1
Dim n As Integer
n = 0
For Each c In List
n = n + 1 '件数表示用
Debug.Print n, c.Time, c.RateE, c.RateM, c.RateTcE, c.RateTcM
Next
End Sub
'2件下シフト
Private Sub Command2_Click()
'末尾2件を除去
SC.ExecuteStatement "List.pop()"
SC.ExecuteStatement "List.pop()"
'先頭に空データ2件挿入
List.unshift New Class1
List.unshift New Class1
End Sub
'2件上シフト
Private Sub Command3_Click()
'先頭2件を除去
SC.ExecuteStatement "List.shift()"
SC.ExecuteStatement "List.shift()"
'末尾に空データ2件挿入
List.push New Class1
List.push New Class1
End Sub
Private Function NewClass1() As Class1
Set NewClass1 = New Class1
With NewClass1
.Time = Now()
.RateE = Random()
.RateM = Random()
.RateTcE = Random()
.RateTcM = Random()
End With
End Function
Private Function Random() As Single
Random = Int(1001 * Rnd() + -500) / 10
End Function
'-----------
お早い回答ありがとうございます。
> Len(x) = LenB(x) になるようなデータ型の場合に問題が出ますし
Len(x) <> LenB(x) のとき、でなくてですか?
メモリコピーの問題は、アライメントの問題だけなんですか?
まぁずれたらデータがバケルだけで、メモリリークとかは関係無いですよね。
> それを嫌うのには
いえ、ループを使ってやる方法なら淡々とできるのですが、それだと技術的なおもしろさが無いので、なにか別のアプローチがないかと考えたところです。
別にループを嫌っているわけではないです。
件数が多くなるとパフォーマンス的に不利なのかな、とは思いましたが。
ループを使ったほうが合理的かつ速やかに終わるならそっちを使います。
> ユーザー定義型でなければいけないのでしょうか
特にユーザー定義型である必要自体はないです。
なるほど、ADODB.Recordset ですか。
今回はソートではなく、データ行の削除が出来れば十分ですけどね。
ちょっとこの方法を勉強させてもらいます。
やろうと思えばListBOXでも同じような事ができますね。
まぁ型の指定が出来たほうがベターですが。
> Len(x) <> LenB(x) のとき、でなくてですか?
そのとおりです。m(_ _)m
で、アライメントを意識してコピーする分には問題ありませんが、
そこに気を使うぐらいなら、手動コピーの方がスマートかな、と。
> アライメントの問題だけなんですか?
データ型によっては、実データ部以外の場所に管理情報を保持している
場合もありますので、やはりおすすめできません。
(Date や Currency 等の単純型なら良いのですが)
極端な例で言えば、こんなのとか。
Option Explicit
Private Type UDT
Member As Object
End Type
Public Declare Sub RtlMoveMemory Lib "kernel32" _
(ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
#Const ManualCopy = True
Sub Main()
Dim a(1) As UDT
Set a(0).Member = CreateObject("Excel.Application")
a(0).Member.Visible = True
Debug.Print VarPtr(a(0).Member), ObjPtr(a(0).Member)
Debug.Print VarPtr(a(1).Member), ObjPtr(a(1).Member)
Debug.Print "-- copy ---"
#If ManualCopy Then
Set a(1).Member = a(0).Member
#Else
RtlMoveMemory a(1), a(0), Len(a(0))
#End If
Debug.Print VarPtr(a(0).Member), ObjPtr(a(0).Member)
Debug.Print VarPtr(a(1).Member), ObjPtr(a(1).Member)
Debug.Print "-- reference test --"
Debug.Print a(0).Member.Version
Debug.Print a(1).Member.Version
Debug.Print "-- release test --"
Set a(1).Member = Nothing '★
a(0).Member.Quit
Set a(0).Member = Nothing
Debug.Print "-- finish --"
End Sub
アドレス参照だけで見れば、手動コピーも API コピーも同じ結果ですが、
API コピーの場合、参照カウントの管理が行われないため、★ 以降の
処理結果に違いが出てしまいます。
# オブジェクト型のほか、可変長文字列型やバリアント型も注意が必要かと。
> 特にユーザー定義型である必要自体はないです。
だとすれば、やはりクラス等で管理した方が都合が良いと思います。
インデックスの管理情報をループでずらすにしても、ユーザー定義型では
API コピーまたは Let によって実データを複写する必要がありますが、
オブジェクトなら Set ステートメントで参照だけを移せば済みますし、
サイズ制限についてもクリアできますから。
>'Class1
>Option Explicit
>Public Time As Date
> :
>Public RateTcM As Single
>
としてコレクションで管理するとか。
ループコピーよりは速そうだけどメモリ消費は大きそう。
まぁ、ユーザー定義型のしばりがない時点で求めてるの違いそうwww
あ、ごめんなさい><
前回のわたしの書き込みは2007/11/23(金)21:20:27以降の3件を見ていないで書き込んでいました。
> ソートではなく、固定件数に対する単純なシフトでしたか。
> だったら、Recordset なら
> For n = 1 To limit
このlimitとは、シフト件数とイコールですか?
引数無しのAddNew の場合、レコードの内容は、各フィールドの型の初期値ということになるんですかね。
今回に限ればシフト方向は片側でも十分といえば十分です。
また、ソートをしたとして、かかる時間はデータ量に比例するなら、大量データはパフォーマンス的に難しいですね。
> ※メモリ的に「一つずらす」のであれば、下に書いてあるものは無意味なのでスルーしてください
もちろんデーターそのものにアクセスしたいだけなので、メモリ的にはどうでもいいです。
なるほど、データーのほうでなく開始アドレスをずらす方法はいいですね!
と、思ったら、クラス内なのでinnerArray周りを単純にユーザー定義型(As GraphFormat)にすればOKというわけにはいかないですよね。
項目の数だけプロパティを追加する形になるのかな。
そうならshiftメソッドにclearValue は実装できないのが少し残念です。
> JScript の Array オブジェクトを利用したコード。
これってJAVAが実行できる環境でないとダメなんでしょうか?
まぁ、よっぽどのことがないと最近のパソコンなら実行できそうですけど・・・
ループコピーよりは速そうだけどメモリ消費は大きそうなんですか?
> データ型によっては、実データ部以外の場所に管理情報を保持している場合もありますので
あー、確かにオブジェクト型は何がおきるか危険ですね。
ポインタとかが化けたりずれたら容易にガガーリン@宇宙飛行士が降臨しそうです。
なんにしろ、メモリコピーは限定的に使うべし、と。
# Re: 紅閃光 [HomePage] 2007/11/28(水) 00:45:35
> と、思ったら、クラス内なのでinnerArray周りを単純にユーザー定義型(As GraphFormat)にすればOKというわけにはいかないですよね。
多分 GraphFormat をグローバルモジュール(でしたっけ?)に配置すればイイ化と思います。
いちいちActiveXとして公開する(これも、多分)のが面倒なら、クラスにしてしまえばいいですし。
ところで、Collection クラスは使えないでしょうかね?
※(さらに、多分)使えるはず
> 引数無しのAddNew の場合、レコードの内容は、各フィールドの型の初期値ということになるんですかね。
初期値は、特に指定していなければ、Null または Empty です。
> また、ソートをしたとして、かかる時間はデータ量に比例するなら、大量データはパフォーマンス的に難しいですね。
オンメモリ Recordset の Sort は、データ量が多くても比較的高速に処理されます。
また、検索や並べ替えを頻繁に行う場合は、
rs.Fields(0).Properties("Optimize").Value = True
のように、Optimize 動的プロパティ を設定してインデックスを作成すると、
パフォーマンスが向上します。
>> JScript の Array オブジェクトを利用したコード。
> これってJAVAが実行できる環境でないとダメなんでしょうか?
JAVA … ですか? そもそも JAVA と JScript は、直接の互換性は無く、まったく無関係です。
ScriptControl が使える環境であれば、JScript も漏れなくインストールされているでしょうから、特に問題なく使えるかと。
とはいえ、単方向シフトで良いならば、ガッさんが書かれている VBA.Collection の方がお奨めです。
> ところで、Collection クラスは使えないでしょうかね?
使ったことがないのですが、今回は別に使う必要は無いのでは?
Private innerArray() As clsGraphFormat
とだけしておけば、リングバッファだから項目の追加や削除は必要ないかと。
あれ?でも、
ReDim innerArray(0 To size - 1)
としたとして、インスタンスはいつ、どのように行えば・・・
もしかしてこうとか。
For i = 0 To size - 1
Set innerArray(i) = New clsGraphFormat
Next
んでもって開放のときは
Dim Obj As Variant
For Each Obj In innerArray
Set Obj = Nothing
Next
えっ、なんか・・・
こういう場合にCollection を使えということでしょうか?
Private cInnerArray As Collection
Set cInnerArray = New Collection
Dim dummy As New clsGraphFormat
For i = 0 To size - 1
dummy.RateE = i
cInnerArray.Add dummy
Next
・・・わたしが勘違いしてるんでしょうか><
>> JScript の Array オブジェクトを利用したコード。
あ、JAVAとは関係ないんですね。勘違いです><
ScriptControl が使えればOKと。
>とはいえ、単方向シフトで良いならば、ガッさんが書かれている VBA.Collection の方がお奨めです。
もう少し調べてみます。
が、リングバッファでもいいのでは、と思ったりするんですが。
100 個登録するときは、
Private list As VBA.Collection
Private Sub Form_Load()
Set list = New VBA.Collection
Dim n As Integer
For n = 1 To 100
list.Add CreateClass1(123.456, …)
Next
End Sub
Public Function CreateClass1(ByVal rateE As Single, …) As Class1
Set CreateClass1 = New Class1
CreateClass1.rateE = rateE
:
End Function
という感じで。
取り出すときは、list(数値).rateE などで。
インデックスのシフトについては、こんな感じ。
'登録済みの先頭2件 list(0), list(1) を削除し、
'末尾に、初期値で2件 list(99), list(100) 追加
list.Remove 1
list.Add New Class1
list.Remove 1
list.Add New Class1
>使ったことがないのですが、今回は別に使う必要は無いのでは?
>
必要であるかないかは該当者にしか決められないことです。
ただ、GraphFormatがクラス化できるのであればコレクションで管理させた方が楽ですよ。
> 魔界の仮面弁士さん
>
名指しすみません。
ちょっと気になったことがあるのですが「単方向シフト」とは何でしょうか。
Add メソッドの Before, After を使用すれば任意のとこに挿入できると思うのですがそう言うことではないのかな?
》GODさん
> Before, After を使用すれば任意のとこに挿入できると思うのですがそう言うことではないのかな?
おぉぉぉ、そういえばそうですね。フォローありがとうございます。
ということは、Collection を使うのが一番スマートかな。
> '登録済みの先頭2件 list(0), list(1) を削除し、
> '末尾に、初期値で2件 list(99), list(100) 追加
これの逆方向シフトは…
'末尾2件を削除し、先頭に2件新規追加
list.Remove list.Count
list.Add New Class1, Before:=1
list.Remove list.Count
list.Add New Class1, Before:=1
やっと理解しました。
オブジェクト変数をうにゃうにゃした経験も無かったので、とりあえず実際にリングバッファ(オブジェクト変数)版を作ってみました。
実行中に件数を増減出来るようにするための手間がかなり煩雑になりました。
コレクション版ならインデックスポインタを管理する必要もないですし、件数の増減もデータを保持したまま出来ますね。
リングバッファでは気に食わないところがすっきりしました。
ところで、コレクションの削除(Nothing)はしたほうがいいのでしょうか?
さすがにコレクションの中身は全件Removeしなくてもいいとは思いますが・・・どうなんでしょう?
> ところで、コレクションの削除(Nothing)はしたほうがいいのでしょうか?
Collection 変数がスコープから外れれば、自動的に解放されます。
データ量が多く、何十、何百メガバイトもの容量を使っているならば、
早期解放も必要でしょうけれど、通常は強制解放は不要かと。
解放漏れが心配なら、クラスモジュール内に
Option Explicit
Private Sub Class_Initialize()
Debug.Print "-- Init --> "; ObjPtr(Me), TypeName(Me)
End Sub
Private Sub Class_Terminate()
Debug.Print "<-- Term -- "; ObjPtr(Me), TypeName(Me)
End Sub
のようなコードを埋め込んでおけば良いでしょう。
クラスのインスタンスが解放されれば、"<-- Term -- " のメッセージが
出力されます。
# (開発時ではなく)コンパイル後にログが欲しいなら、App.LogEvent メソッドで。
ということは、基本的に参照カウンタが有効に働いているオブジェクトは、スコープが無くなったときに勝手に解放されるので、早めに開放したいという理由が無ければほっとけば良いということですね。
ちなみにシフト部はガッさんのを参考に以下のようにしてみました。
Public Sub DataShift(Optional ByVal n As Long = 1, Optional ShiftDirection As eShiftDirection = shift_Up)
If n < 0 Then Exit Sub
If n > mList.Count Then n = mList.Count
Select Case ShiftDirection
Case eShiftDirection.shift_Up ' 順方向送り
For n = n To 1 Step -1
Call mList.Remove(mList.Count)
Call mList.Add(CreateGraphFormat, Before:=1)
Next
Case eShiftDirection.shift_Down ' 逆方向送り
For n = n To 1 Step -1
Call mList.Remove(1)
Call mList.Add(CreateGraphFormat)
Next
End Select
End Sub
< For Num = Num To 1 Step -1
ってあたりが小憎らしいですね。
今後パクらせていただきます (・ー・)b
今件の起こりは、たとえば件数が増えて1万×10個のデータがあるとして、データの単純な一括シフトで10万回変数にアクセスするのがカコワルイ(もちろん独断と偏見)と思ったからなんです。
提示していただいた方法を含めると
・ユーザー定義型のアライメントを利用してメモリコピーする
・リストコントロールで管理する
・データーベース(レコードセット)として管理する
・オブジェクト配列のリングバッファとしてインデックスポインタを管理する
・コレクションとして管理する
どれでも必要な機能は実装できましたが、コレクションを利用する方法が一番シンプルでパフォーマンスも悪くないという結果になりました。
コレクションはかなり使い道がありそうですねぇ。
以前、ユーザー定義型変数の配列でデータ管理しようとして64kByteの壁にぶち当たって泣く泣く分割したことがあります。
クラスを定義してコレクションにしてしまえばスマートですね。
なにはともあれ、とりあえずは目的達成ですので、ひとまず解決とさせていただきます。
まだ、別の面白い(?)方法があれば書き込みお願いします。
魔界の仮面弁士さん、GODさん、ガッさん、ありがとうございました。
今回も勉強になりました。
わたしのメモリコピーが一番アクロバティックでしたね。 (´▽`*)アハハー・・・