VB.NET2005です。
A:おもしろかった 100件
B:まあまあだった 200件
というアンケートのデータがあり、A,Bの件数をパーセンテージで表示したいです。
データ構造に配列やListなどを使用せずに、自分でコレクションを作れば、合計を求めるための
ループ処理が不要になるのでは?と思い、
CollectionBase クラス
http://msdn2.microsoft.com/ja-jp/library/system.collections.collectionbase(VS.80).aspx
を参考に初めてカスタムコレクションを作りました。
で、一応希望通りに動くものができたのですが、我ながらダメだな〜という感じがしておりまして、
アドバイスしていただきたく投稿させていただきました。あつかましいお願いですがもしよろしければ…
-----------------以下、私が作ったものの説明です。
アンケートデータは MyData 型としました。(下のほうのコードを参照して下さい)
で、MyData型のコレクション MyDataCollection を作り、
Dim collection As New MyDataCollection
collection.Add(New MyData("A", 100))
collection.Add(New MyData("B", 300))
Console.WriteLine(collection(0).Percent) '→ 25% と表示されること。
このような使い方ができることを目指しました。
基本的な仕組みとして、MyDataCollectionは内部で合計値を保持し、Addされると加算される方法を取りました。
ダメな点というのは、割合を算出するために MyData型 に Percent という項目を持たせたのですが、
書こうと思えばこんなことも書けてしまうのがう〜む…という感じなのです。
Dim mydata As New MyData("A", 100)
mydata.Percent = "100%"
こんな風に書くことができないようにしたいのですが、何かよい方法はありますでしょうか?
-----------------以下コードです
Public Class Form1
Public Class MyData
Public Name As String
Public Value As Integer
Public Percent As String '***←Percentを持たせた
Public Sub New(ByVal n As String, ByVal v As Integer)
Name = n
Value = v
End Sub
End Class
Public Class MyDataCollection
Inherits CollectionBase
Private _Sum As Integer
Default Public Property Item(ByVal index As Integer) As MyData '***←MyDataを返す際にPercentに計算結果を入れる
Get
Dim w_CalcData As MyData
w_CalcData = CType(List(index), MyData)
w_CalcData.Percent = CInt(w_CalcData.Value / _Sum * 100) & "%"
Return w_CalcData
End Get
Set(ByVal value As MyData)
List(index) = value
End Set
End Property
Public Shadows Function Add(ByVal cd As MyData) As MyData
MyBase.List.Add(cd)
_Sum += cd.Value
Return cd
End Function
End Class
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim collection As New MyDataCollection
collection.Add(New MyData("A", 100))
collection.Add(New MyData("B", 300))
Console.WriteLine(collection(0).Percent)
End Sub
End Class
MyDataCollection の Item プロパティの Get に変なローカル変数名が入ってしまいました。
w_CalcData → work_MyData と読み替えてください m(_ _)m
> Console.WriteLine(collection(0).Percent) '→ 25% と表示されること。
それだと、コレクションを介さずに使った場合、Percent の値を想像しにくくないですか?
(0 を返すというのも、意味が変わってしまうし…)
x = New MyData("A", 100)
y = x.Percent 'この値は何をかえすべき?
それと、パーセンテージを算出できるのは、個々の MyData ではなく、
総計を知っている MyDataCollection のはずです。
となれば、MyData 側に値を保持させる仕様にするのは、不自然なようにも感じます。
たとえば、コレクション側に GetPercent(index) メソッドを持たせてみてはどうでしょう?
メソッドなら、値の代入も防げますし。
> 書こうと思えばこんなことも書けてしまうのがう〜む…という感じなのです。
それだけの話なら、Percent を ReadOnly なフィールド(またはプロパティ)にすれば
済むでしょうけれど……それ以前に、もっと大きな問題があります。
一番の問題は、現行の仕様では、
collection(0).Value = 5
のように行ったとしても、_Sum フィールドが更新されない点です。
値を後から編集させたくないのであれば、Value を ReadOnly にすべきですし、
読み書き可能にするのであれば、Value に対して変更が行われた場合に、
それを MyDataCollection に通知するための仕組みが必要でしょう。
(たとえば、MyData に ValueChange イベントを実装するなど)
>それだと、コレクションを介さずに使った場合、Percent の値を想像しにくくないですか?
>(0 を返すというのも、意味が変わってしまうし…)
…
>となれば、MyData 側に値を保持させる仕様にするのは、不自然なようにも感じます。
なるほど!読んだ瞬間そうだと思いました。
>一番の問題は、現行の仕様では、
> collection(0).Value = 5
>のように行ったとしても、_Sum フィールドが更新されない点です。
うわ〜ほんとですね。(^^;;
>読み書き可能にするのであれば、Value に対して変更が行われた場合に、
>それを MyDataCollection に通知するための仕組みが必要でしょう。
>(たとえば、MyData に ValueChange イベントを実装するなど)
ValueChangeを使う発想に再び感動しました。(私が無知なだけかも)
早速、新たにコードを書くことにします。
ものすごく参考になりました。ありがとうございます。まずはお礼まで…
>> となれば、MyData 側に値を保持させる仕様にするのは、不自然なようにも感じます。
> なるほど!読んだ瞬間そうだと思いました。
とはいえ、MyData 側に Percentage を実装した方が使いやすいこともあるでしょう。
その場合は、MyData 側をこのように実装するのも手かと。
・コレクション未登録時は、例外を発生させるようにする。
・Percentage 値は固定的に持たせるのではなく、内部で
親コレクションに問い合わせるような仕組みにする。
>(たとえば、MyData に ValueChange イベントを実装するなど)
これ、「ValueChanged」イベントの誤記です。m(_ _;)m
変更後イベントなので、過去形にすべきということで。
# まぁ、意図は伝わったようですが。
アドバイスを参考にコードを書き直したら望みどおりのものができました。
突っ込まれどころは沢山ありそうな気はしますが(^^;、せっかくですので晒します。
ちなみに、MyDataCollection と MyData の両方のクラスで RaiseEvent を行いました。
MyData クラスの Property Value の Set 時にChangedイベントを発生させただけでは足りなかったので、
MyDataCollection の Mybase.List.Add 時にもイベントを発生させるようにしました。
イベント関係について不勉強なのでもっと良い方法があるかもしれません。(というか、あるに違いない(^^;;)
もっと精進しますです。ともあれ、きっかけを教えていただきありがとうございました。
Public Class Form1
Public Class MyData
Public Event ValueChanged()
Public Name As String
Private _Value As Integer
Public Property Value() As Integer
Get
Return _Value
End Get
Set(ByVal value As Integer)
_Value = value
RaiseEvent ValueChanged()
End Set
End Property
Public Sub New(ByVal n As String, ByVal v As Integer)
Name = n
Value = v
End Sub
End Class
Public Class MyDataCollection
Inherits CollectionBase
Private Event ListChanged()
Private _Sum As Integer
Default Public Property Item(ByVal index As Integer) As MyData
Get
Return CType(List(index), MyData)
End Get
Set(ByVal value As MyData)
List(index) = value
RaiseEvent ListChanged()
End Set
End Property
Public Sub New()
AddHandler ListChanged, AddressOf SumRecalc
End Sub
Public Sub SumRecalc()
_Sum = 0
For Each item As MyData In MyBase.List
_Sum += item.Value
Next
End Sub
Public Shadows Function Add(ByVal cd As MyData) As MyData
MyBase.List.Add(cd)
RaiseEvent ListChanged()
AddHandler cd.ValueChanged, AddressOf SumRecalc
Return cd
End Function
Public Function Percent(ByVal index As Integer) As Integer
Dim per As Integer
per = CInt(CType(MyBase.List(index), MyData).Value * 100 / _Sum)
Return per
End Function
End Class
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim collection As New MyDataCollection
collection.Add(New MyData("A", 100))
collection.Add(New MyData("B", 300))
Console.WriteLine(collection.Percent(0) & "%") '25%→OK
collection(0).Value = 200
Console.WriteLine(collection.Percent(0) & "%") '40%→OK
End Sub
End Class
>> なるほど!読んだ瞬間そうだと思いました。
>とはいえ、MyData 側に Percentage を実装した方が使いやすいこともあるでしょう。
そう言われると確かにDataGridViewとかにバインドするときとか使いやすそうです。
(意見が変わりすぎ→自分(^^;;
>その場合は、MyData 側をこのように実装するのも手かと。
> ・コレクション未登録時は、例外を発生させるようにする。
> ・Percentage 値は固定的に持たせるのではなく、内部で
> 親コレクションに問い合わせるような仕組みにする。
親に問い合わせる、というのは、個々のMyDataに親へのポインタを持っておく、ということでしょうか?
おおよそこのような感じと理解しました。
Public Class MyData
Public Name As String
Public Value As Integer
Private _MyDataCollection As MyDataCollection = Nothing
Public ReadOnly Property Percent() As Integer
Get
If _MyDataCollection Is Nothing Then
'例外を発して戻る(例外の書き方を良く知らない→勉強する(^^;;
End If
Return CInt((Value / _MyDataCollection.Sum * 100))
End Get
End Property
Public Sub New(ByVal n As String, ByVal v As Integer, ByVal p As MyDataCollection)
Name = n
Value = v
_MyDataCollection = p
End Sub
End Class
たしかにこれならクライアントコードは楽!だと思います。
それではまたいろいろ試してみます。ありがとうございました。
MyData側にPercentを実装してみました。
'※このコードは勉強中のもので、発展途上中です。おそらく不備満載ですのでご注意を。(つっこみ歓迎)
Public Class Form1
Public Class MyData
Public Event ValueChanged()
Private _MyDataCollection As MyDataCollection
Private _Name As String
Public Property Name() As String
Get
Return _Name
End Get
Set(ByVal value As String)
_Name = value
End Set
End Property
Private _Value As Integer
Public Property Value() As Integer
Get
Return _Value
End Get
Set(ByVal value As Integer)
_Value = value
RaiseEvent ValueChanged()
End Set
End Property
Public ReadOnly Property Percent() As Integer
Get
If _MyDataCollection Is Nothing Then
Throw New ApplicationException("親不在")
End If
Return CInt(Value * 100 / _MyDataCollection.Sum)
End Get
End Property
Public Sub New(ByVal n As String, ByVal v As Integer, ByRef p As MyDataCollection)
Name = n
Value = v
_MyDataCollection = p
End Sub
End Class
Public Class MyDataCollection
Inherits CollectionBase
Private Event ListChanged()
Public Sum As Integer
Default Public Property Item(ByVal index As Integer) As MyData
Get
Return CType(List(index), MyData)
End Get
Set(ByVal value As MyData)
List(index) = value
RaiseEvent ListChanged()
End Set
End Property
Public Sub New()
AddHandler ListChanged, AddressOf SumRecalc
End Sub
Public Shadows Function Add(ByVal cd As MyData) As MyData
MyBase.List.Add(cd)
RaiseEvent ListChanged()
AddHandler cd.ValueChanged, AddressOf SumRecalc
Return cd
End Function
Public Sub SumRecalc()
Sum = 0
For Each item As MyData In MyBase.List
Sum += item.Value
Next
End Sub
End Class
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim collection As New MyDataCollection
collection.Add(New MyData("A", 100, collection))
collection.Add(New MyData("B", 300, collection))
Console.WriteLine(collection(0).Percent & "%") 'A:25%
collection(0).Value = 200
Console.WriteLine(collection(0).Percent & "%") 'A:40%
Dim dgv As New DataGridView
dgv.DataSource = collection
dgv.Dock = DockStyle.Fill
Controls.Add(dgv)
Dim mydata As New MyData("C", 500, Nothing)
'Console.WriteLine(mydata.Percent) '例外発生
End Sub
End Class
> 親に問い合わせる、というのは、個々のMyDataに親へのポインタを持っておく、ということでしょうか?
ですます。
親を保持するといった場合、たとえば親クラスそのものの参照を保持させる以外にも、
親クラスの総計取得メソッドのデリゲートインスタンスを保持させるとか、あるいは、
コレクションクラスそのものではなく、そのさらに上位のオブジェクトを保持させる場合もあります。
たとえば、TreeNode クラスを見てみると、TreeNodeCollection に Add すると、
TreeView という読み込み専用プロパティが、親コントロールを返す仕様になっていますね。
> Public Class MyData
> Sub New(ByVal n As String, ByVal v As Integer, ByRef p As MyDataCollection)
ByRef にする必要は無いと思いますよ。ByVal で十分な気がします。
> Throw New ApplicationException("親不在")
ApplicationException を直接投げるのではなく、その派生クラスを投げるようにします。
> Return CInt(Value * 100 / _MyDataCollection.Sum)
/ 演算子を使うと、結果が Double になります。整数値を得るなら、
\ 演算子を用いた方が良いでしょう。
> collection.Add(New MyData("A", 100, collection))
悪くは無いですが、上記のような使い方がメインになるのであれば、
Add(MyData) ではなく、Add(String, Integer)にして
collection.Add("A", 100)
という実装にした方が使いやすいかも知れません。
ただいずれの実装にしても、利用者が誤って
collection1.Add(New MyData("A", 100, collection2))
という使い方をした場合や、
Dim x As New MyData("A", 100, collection)
collection.Add(x)
x.Value = 200
collection.Add(x)
といった使い方をされた場合などを考慮する必要があります。
たとえば、このような感じですかね。
Sub Add(ByVal data As MyData)
If data Is Nothing Then
Throw New ArgumentNullException(…) 'Nothing はダメ
ElseIf data.owner IsNot Nothing Then
If data.owner Is Me Then
Throw New すでに同じコレクションに登録済み例外
Else
Throw New すでに他のコレクションに登録済み例外
End If
End If
'以下略
# なお、上記の data.owner は、親コレクションを返す Friend メンバをイメージしています。
ありがとうございます。レスを頂いて感激です。
>たとえば、TreeNode クラスを見てみると、TreeNodeCollection に Add すると、
>TreeView という読み込み専用プロパティが、親コントロールを返す仕様になっていますね。
そうか〜、既存のクラスの中で親子関係があるものをイメージして参考にすれば良かったですね。
>> Public Class MyData
>> Sub New(ByVal n As String, ByVal v As Integer, ByRef p As MyDataCollection)
>ByRef にする必要は無いと思いますよ。ByVal で十分な気がします。
うぁ、クラスなのでこの場合はByValで良いのですね…
>> Throw New ApplicationException("親不在")
>ApplicationException を直接投げるのではなく、その派生クラスを投げるようにします。
派生クラスが用意されているのですね。便利だなぁ。
今回の場合はApplicationExceptionよりもNullReferenceExceptionの方がしっくりきそうだと思いました。
>> Return CInt(Value * 100 / _MyDataCollection.Sum)
>/ 演算子を使うと、結果が Double になります。整数値を得るなら、
>\ 演算子を用いた方が良いでしょう。
そんな便利な演算子があったんですかーorz
今グラフィック描画関係のテストアプリを作っているのですが、座標計算の結果をCIntかけまくりで読みにくいったら…
>> collection.Add(New MyData("A", 100, collection))
>悪くは無いですが、上記のような使い方がメインになるのであれば、
>Add(MyData) ではなく、Add(String, Integer)にして
> collection.Add("A", 100)
>という実装にした方が使いやすいかも知れません。
全く同感です。TreeNodeCollection.Addを調べてみると、Stringを引数にするものがあって、更に納得。
それと、よく考えるとコレクションのAddメソッドにcollectionを渡す必要は無かったですね。自分自身なんだから知ってるはず。
> Dim x As New MyData("A", 100, collection)
> collection.Add(x)
> x.Value = 200
> collection.Add(x)
>といった使い方をされた場合などを考慮する必要があります。
これは全く考慮していませんでした。
確かに!起きたら嫌すぎのバグです。
こんな不整合を防ぐために
> Throw New すでに同じコレクションに登録済み例外
と、更に
> Throw New すでに他のコレクションに登録済み例外
のチェックが必要なのですね。これもすごく勉強になりました。
今回は本当にありがとうございました。良いコードを書けるようにもっと勉強します(^^)
後から気づいた部分があるので、さらに追加。
> Public Class MyDataCollection
(中略)
> Public Sum As Integer
Public 変数にしていますが、合計値を外部から操作できてしまうのは
問題があるので、ここは、ReadOnly Property とした方が安全でしょう。
また、ここが Integer 型ですと、Integer.MaxValue などの値を登録されたときに、
容易にオーバーフローしてしまいます。Long 型などの方が無難かと。
>>> Return CInt(Value * 100 / _MyDataCollection.Sum)
これも同様。「100」ではなく、「100L」にしておかないと、
Value が Integer.MaxValue だった場合にオーバーフローしますよ。
>> / 演算子を使うと、結果が Double になります。整数値を得るなら、
>> \ 演算子を用いた方が良いでしょう。
とはいえ、実際にはプロパティ値も含めて Double の方が良い気がしますけれどね。
なぜなら、同じ値を 100 個登録した時は、それぞれが「1%」なので 計100% ですが、
1000 個登録した時は、それぞれが「0%」に切り捨てられ、計 0% のように見えてしまうので…。
> そんな便利な演算子があったんですかーorz
英語圏だと、『/演算子』と『\演算子』のように見えるので分かりやすいのですが、
日本語だと、『/演算子』と『¥演算子』のように見えるので、見落としがちかも。(^^;
あと、\ の場合は、端数部(今回は整数演算なので、この端数とは小数部のこと)が切り捨てられますが、
/ と CInt の組み合わせの場合は、小数部が丸められる(四捨五入では無い)ことに注意が必要です。
> それと、よく考えるとコレクションのAddメソッドにcollectionを渡す必要は無かったですね。
> 自分自身なんだから知ってるはず。
ん? Add には渡していませんよね。
Yoshi さんが書かれたソースは、
collection.Add(New MyData("A", 100, collection))
であって、
collection.Add(New MyData("A", 100), collection)
では無かったはず。
あと、ListChanged イベントを受けてループで再計算していますが、このとき、
ListChanged イベントの引数で、差分値(NewValue - OldValue な値)を渡すようにすれば、
ループさせることなく、合計値を更新できるようになるかと思います。
返信ありがとうございます。
>…
>>>> Return CInt(Value * 100 / _MyDataCollection.Sum)
>これも同様。「100」ではなく、「100L」にしておかないと、
>Value が Integer.MaxValue だった場合にオーバーフローしますよ。
成程〜、計算途中のオーバーフローに無防備でした。そういえば昔これ系のバグを出した記憶が…(汗
100Lという書き方は勉強になりました。
>とはいえ、実際にはプロパティ値も含めて Double の方が良い気がしますけれどね。
>なぜなら、同じ値を 100 個登録した時は、それぞれが「1%」なので 計100% ですが、
>1000 個登録した時は、それぞれが「0%」に切り捨てられ、計 0% のように見えてしまうので…。
うわ、本当ですね。このケース、考え付きませんでした。
>ん? Add には渡していませんよね。
思いっきり早とちりしました。自分のコードなのに(^^;;;
>あと、ListChanged イベントを受けてループで再計算していますが、このとき、
>ListChanged イベントの引数で、差分値(NewValue - OldValue な値)を渡すようにすれば、
>ループさせることなく、合計値を更新できるようになるかと思います。
やっぱり引数が使えるのですね。要素数やアクセス数が増えてくるとループ負荷も馬鹿にならないかと思いますので、試してみます。
ツイート | ![]() |