Streetw☆さんのクラス。その1。

StreetwMemo(Of T) クラス。


Option Explicit On
Option Strict On

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Data
Imports System.Collections

Namespace SilverBouquet.Utility

''' <author>Streetw☆</author>
''' <publishDate>2007/09/25</publishDate>
''' <lastUpdateDate></lastUpdateDate>
''' <contact>コメント欄</contact>
''' <requiredEnv>.NET Framework 2.0</requiredEnv>
'''
''' <summary>
''' 様々な型のメモをとるクラスです。
''' </summary>
''' <remarks>
''' StreetwMemo ジェネリック クラスは、メモする内容をその型のままでメモできます。
''' メモ内容は、インスタンスの生成と破棄のタイミングで自動的に保存と復元が行われるため、手軽にメモをとることができます。
''' </remarks>
''' <typeparam name="T">メモの型。</typeparam>
Public Class StreetwMemo(Of T)
Implements IDisposable

#Region "コンストラクタ"

''' <summary>メモ内容の保存先。</summary>
Private _saveFilePath As String

''' <summary>メモ内容の格納用。</summary>
Private _dataMemo As DataTable
''' <summary>キー検索用。</summary>
Private _viewFindKey As DataView

''' <summary>
''' コンストラクタです。
''' メモ内容の保存先は、<see cref="GetDefaultMemoFilePath"/> に
''' 引数 "defsult" を渡して得られるファイルです。
''' <para>
''' 同じ型のメモは、同時に複数のインスタンスを生成しないでください。
''' 複数生成した場合、保存されるメモ内容は、
''' 最後に Dispose() を呼び出したインスタンスの内容になります。
''' </para>
''' </summary>
Public Sub New()
Me.New("")
End Sub

''' <summary>
''' コンストラクタです。
''' <para>
''' 同じ保存先を指定したメモは、同時に複数のインスタンス
''' 生成しないでください。
''' 複数生成した場合、保存されるメモ内容は、
''' 最後に Dispose() を呼び出したインスタンスの内容になります。
''' </para>
''' </summary>
''' <param name="saveFilePath">メモ内容の保存先ファイルパス。</param>
Public Sub New( _
ByVal saveFilePath As String)
'未指定時はデフォルト
If String.IsNullOrEmpty(saveFilePath) _
OrElse saveFilePath.Trim() = "" Then
saveFilePath = GetDefaultMemoFilePath("default")
End If
_saveFilePath = saveFilePath

'メモ内容格納用 DataTable の準備
_dataMemo = New DataTable("data")
_dataMemo.Locale = System.Globalization.CultureInfo.InvariantCulture
_dataMemo.Columns.Add("key", GetType(String))
_dataMemo.Columns.Add("val", GetType(T))
_dataMemo.Columns.Add("category", GetType(String))

'前回保存されたメモ内容の復元
RestoreMemo()

'メモ内容をキーで検索するための準備
_viewFindKey = New DataView(_dataMemo)
_viewFindKey.Sort = "key"
End Sub

#End Region

#Region "デストラクタと IDisposable メンバ"

''' <summary>
''' デストラクタです。
''' </summary>
Protected Overrides Sub Finalize()
Dispose(False)
End Sub

''' <summary>
''' インスタンスを破棄します。
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub

Private _disposed As Boolean = False

''' <summary>
''' インスタンスを破棄します。
''' </summary>
''' <param name="disposing">
''' Dispose() が明示的に呼び出されたとき true。
''' </param>
Protected Overridable Sub Dispose( _
ByVal disposing As Boolean)
If _disposed Then Return

If Not disposing Then
'Dispose() を実行して欲しいと願う
End If

'メモの内容を保存
SaveMemo()

'破棄
_viewFindKey.Dispose()
_dataMemo.Dispose()

_disposed = True
End Sub

#End Region

#Region "メソッド(Shared)"

''' <summary>
''' デフォルトの保存先を取得します。
''' </summary>
''' <param name="id">
''' メモを区別するID値。
''' ファイル名の一部になります。
''' </param>
''' <returns>保存先のファイルパス。</returns>
Public Shared Function GetDefaultMemoFilePath( _
ByVal id As String) As String
If String.IsNullOrEmpty(id) OrElse id.Trim() = "" Then
Throw New ArgumentException( _
"値を Null、空文字、または空白のみにすることはできません。", _
"id")
End If

If id.IndexOfAny(System.IO.Path.GetInvalidFileNameChars()) <> -1 Then
Throw New ArgumentException( _
"ファイル名として無効な文字が含まれます。", _
"id")
End If

Return _
System.IO.Path.Combine( _
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), _
String.Format( _
System.Globalization.CultureInfo.CurrentCulture, _
"StreetwMemo_{0}_{1}.dat", _
GetType(T).Name, id))
End Function

#End Region

#Region "メソッド"

''' <summary>
''' メモします。
''' </summary>
''' <param name="value">メモする内容。</param>
''' <returns>メモ内容にアクセスする際に使用するキー。</returns>
Public Function Write( _
ByVal value As T) As String
Return Write(value, "")
End Function

''' <summary>
''' カテゴリーを添えてメモします。
''' </summary>
''' <param name="value">メモする内容。</param>
''' <param name="category">カテゴリー。</param>
''' <returns>メモ内容にアクセスする際に使用するキー。</returns>
Public Function Write( _
ByVal value As T, _
ByVal category As String) As String
'キーを生成し、メモする
Dim key As String = GenerateDefaultMemoKey()
Write(key, value, category)
Return key
End Function

''' <summary>
''' キーを指定してメモします。
''' </summary>
''' <param name="key">メモ内容にアクセスする際に使用するキー。</param>
''' <param name="value">メモする内容。</param>
Public Sub Write( _
ByVal key As String, _
ByVal value As T)
Write(key, value, "")
End Sub

''' <summary>
''' キーを指定し、カテゴリーを添えてメモします。
''' </summary>
''' <param name="key">メモ内容にアクセスする際に使用するキー。</param>
''' <param name="value">メモする内容。</param>
''' <param name="category">カテゴリー。</param>
Public Overridable Sub Write( _
ByVal key As String, _
ByVal value As T, _
ByVal category As String)
Dim rowMemo As DataRow = GetMemoDataRow(key, False)
If rowMemo Is Nothing Then
_dataMemo.Rows.Add(key, value, category)
Else
rowMemo.BeginEdit()
rowMemo("val") = value
rowMemo("category") = category
rowMemo.EndEdit()
End If
End Sub

''' <summary>
''' メモを読みます。
''' </summary>
''' <param name="key">読むメモのキー。</param>
''' <returns>メモ内容。</returns>
Public Overridable Function Read( _
ByVal key As String) As T
Dim rowMemo As DataRow = GetMemoDataRow(key, True)
Return DirectCast(rowMemo("val"), T)
End Function

''' <summary>
''' メモを消します。
''' </summary>
''' <param name="key">消すメモのキー。</param>
''' <returns>
''' 消すことができたとき true。
''' 指定したキーに対応するメモが無かった場合 false。
''' </returns>
Public Overridable Function EraseMemo( _
ByVal key As String) As Boolean
Dim rowMemo As DataRow = GetMemoDataRow(key, False)
If rowMemo Is Nothing Then
Return False
End If

_dataMemo.Rows.Remove(rowMemo)
Return True
End Function

''' <summary>
''' 全てのメモを消します。
''' </summary>
Public Sub EraseAllMemo()
For Each key As String In GetAllKeys()
EraseMemo(key)
Next
End Sub

''' <summary>
''' キーを生成します。
''' </summary>
''' <returns>生成されたキー。</returns>
Protected Overridable Function GenerateDefaultMemoKey() As String
Return Guid.NewGuid().ToString()
End Function

''' <summary>
''' キーに対応するメモの DataRow を取得します。
''' </summary>
''' <param name="key">キー値。</param>
''' <param name="throwIfNotFound">
''' 対応するメモが存在しなければ例外を生成する場合 true。
''' </param>
''' <returns>
''' キーに対応するメモ内容を格納した DataRow。
''' キーに該当するメモがなければ null(Visual Basic では Nothing)。
''' </returns>
Private Function GetMemoDataRow( _
ByVal key As String, _
ByVal throwIfNotFound As Boolean) As DataRow
Dim index As Integer = _viewFindKey.Find(key)
If index = -1 Then
If throwIfNotFound Then
Throw New ArgumentException( _
"メモされていません。", _
"key")
End If

Return Nothing
End If

Return _viewFindKey(index).Row
End Function

''' <summary>
''' メモの内容を保存します。
''' </summary>
Protected Overridable Sub SaveMemo()
'メモがあれば保存、なければファイルを削除
If _dataMemo.Rows.Count > 0 Then
_dataMemo.WriteXml(_saveFilePath)
Else
System.IO.File.Delete(_saveFilePath)
End If
End Sub

''' <summary>
''' メモの内容を復元します。
''' </summary>
''' <returns>
''' メモ内容を復元できたとき true、
''' 復元できなかったとき false。
''' </returns>
Protected Overridable Function RestoreMemo() As Boolean
If Not System.IO.File.Exists(_saveFilePath) Then
Return False
End If

_dataMemo.Clear()
_dataMemo.ReadXml(_saveFilePath)

Return True
End Function

''' <summary>
''' 全てのキーを取得します。
''' </summary>
''' <returns>
''' キーの配列。
''' メモがない場合、長さゼロの配列。
''' </returns>
Public Function GetAllKeys() As String()
Return GetKeyArrayOf(_dataMemo.Rows)
End Function

''' <summary>
''' 指定する文字列がキーに含まれる全てのキーを取得します。
''' </summary>
''' <param name="partOfKey">
''' 検索するキーの部分文字列。
''' 指定した文字列がキー値に含まれるメモを検索します。
''' </param>
''' <returns>
''' キーの配列。
''' 該当がない場合、長さゼロの配列。
''' </returns>
Public Function FindKeys( _
ByVal partOfKey As String) As String()
Dim rows As DataRow() = _dataMemo.Select( _
String.Format( _
System.Globalization.CultureInfo.CurrentCulture, _
"key like '%{0}%'", _
partOfKey.Replace("'", "''")))

Return GetKeyArrayOf(rows)
End Function

''' <summary>
''' 指定する文字列がカテゴリーに含まれる全てのキーを取得します。
''' </summary>
''' <param name="partOfCategory">
''' 検索するカテゴリの部分文字列。
''' 指定した文字列がカテゴリに含まれるメモを検索します。
''' </param>
''' <returns>
''' キーの配列。
''' 該当がない場合、長さゼロの配列。
''' </returns>
Public Function FindKeysOfCategory( _
ByVal partOfCategory As String) As String()
Dim filter As String
If String.IsNullOrEmpty(partOfCategory) Then
filter = "category = ''"
Else
filter = String.Format( _
System.Globalization.CultureInfo.CurrentCulture, _
"category like '%{0}%'", _
partOfCategory.Replace("'", "''"))
End If

Dim rows As DataRow() = _dataMemo.Select(filter)

Return GetKeyArrayOf(rows)
End Function

''' <summary>
''' キーの配列を取得します。
''' </summary>
''' <param name="rows">DataRow の配列またはコレクション。</param>
''' <returns>キーの配列。</returns>
Private Function GetKeyArrayOf( _
ByVal rows As IEnumerable) As String()
Dim resultKeyList As List(Of String) = New List(Of String)()

Dim keyColumn As DataColumn = _dataMemo.Columns("key")
For Each row As DataRow In rows
resultKeyList.Add(DirectCast(row(keyColumn), String))
Next

Return resultKeyList.ToArray()
End Function

#End Region

#Region "使用方法"

''' <summary>
''' 使用方法です。
''' このメソッドのソースコードを参考にしてください。
''' </summary>
<System.Diagnostics.Conditional("DEBUG")> _
Public Shared Sub Usage()
Dim key As String

'string 型のデータをメモします。
Using memo As New StreetwMemo(Of String)()
'メモをとるとキーがもらえます。
key = memo.Write("メモする内容")
'読む際にはキーが必要です。
Dim value As String = memo.Read(key)

Console.WriteLine(value)
End Using

'インスタンスを生成し直しても、メモの内容は保持されます。
Using memo As New StreetwMemo(Of String)()
Dim value As String = memo.Read(key)

Console.WriteLine(value)
End Using

'decimal 型のデータをメモします。
Using priceMemo As New StreetwMemo(Of Decimal)()
'メモする際に、キーを指定できます。
priceMemo.Write("某ブログ1の価格", 42372D)
priceMemo.Write("某ブログ2の価格", 68328D)
End Using

Using priceMemo As New StreetwMemo(Of Decimal)()
'キーを検索します。
For Each findKey As String In priceMemo.FindKeys("ブログ")
Console.WriteLine("{0} … {1:c}", findKey, priceMemo.Read(findKey))
Next
End Using

'カテゴリーでメモを分類できます。
Using priceMemo As New StreetwMemo(Of Decimal)()
priceMemo.Write("某ブログ1の価格", 42372D, "ブログの価格")
priceMemo.Write("某ブログ2の価格", 68328D, "ブログの価格")
End Using

Using priceMemo As New StreetwMemo(Of Decimal)()
'カテゴリーを指定してキーを取得します。
For Each findKey As String In priceMemo.FindKeysOfCategory("ブログの価格")
Console.WriteLine("{0} … {1:c}", findKey, priceMemo.Read(findKey))
Next
End Using

'メモの保存先を指定できます。
Dim filePath As String = StreetwMemo(Of String).GetDefaultMemoFilePath("汎用")
Using memo As New StreetwMemo(Of String)(filePath)
memo.Write("このメモは '" & filePath & "' に記録されます。")
End Using

'使用方法の後片付け
Using memo As New StreetwMemo(Of String)()
memo.EraseAllMemo()
End Using
Using memo As New StreetwMemo(Of Decimal)()
memo.EraseAllMemo()
End Using
Using memo As New StreetwMemo(Of String)(filePath)
memo.EraseAllMemo()
End Using
End Sub

#End Region

End Class

End Namespace

すでにメールで何度かやり取りさせていただいているのですが、論点はここ↓

メモ内容は、インスタンスの生成と破棄のタイミングで自動的に保存と復元が行われるため、手軽にメモをとることができます。

''' <summary>
''' インスタンスを破棄します。
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub

Private _disposed As Boolean = False

''' <summary>
''' インスタンスを破棄します。
''' </summary>
''' <param name="disposing">
''' Dispose() が明示的に呼び出されたとき true。
''' </param>
Protected Overridable Sub Dispose( _
ByVal disposing As Boolean)
If _disposed Then Return

If Not disposing Then
'Dispose() を実行して欲しいと願う
End If

'メモの内容を保存
SaveMemo()

'破棄
_viewFindKey.Dispose()
_dataMemo.Dispose()

_disposed = True
End Sub

Disposeメソッド(IDisposableではないですが)でSaveMemo メソッドを呼び出すのはどうなんだろうか?ということです。
んで、いままでのやりとり。
とりこびと曰く、

(SaveMemo メソッドを)呼び出しているDispose メソッドはIDisposable.Dispose メソッドではないけど、ちょっとユーザーの想像しうる実装ではなくありませんか?
ややこしくなく書くとDisposeメソッドがそこまでしちゃうとDisposeメソッドっぽくなくなくない?ってことですw(ややこしい。
Streetwさん曰く、

IDisposableはやめて、Open()とClose()を用意する方が、素直でしょうか?
ただそれだと、お手軽な感じがなくなるような気がしています
とりこびと曰く、

私は極力名は体を現していてほしいと思うタイプなので(^^;
Streetwさん曰く、

a. (アンマネージを含む)リソースの解放
b. 終了処理
このbがどこまでいい?という話ですかね。
http://www.atmarkit.co.jp/fdotnet/easyvs/easyvs03/easyvs03_06.html
の「クラス終了時に実行しなければならない処理」の表現で、
今回の場合がそれに当たるのかと。。。
作者は当たると考えていますw
ここ
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?mode=viewtopic&topic=31217&forum=7&start=16
とかでも少し議論されてますね。
TransactionScopeとかも、Disposeでいろいろやってると思いますし。
ご意見いただきたく存じます。
[参考文献]
MSDN:IDisposable インターフェイス
Dispose メソッドの実装