連載

.NETで簡単XML

第16回 川俣流XMLプログラミングの定石(2)

株式会社ピーデー 川俣 晶
2004/03/24
Page1 Page2 Page3 Page4

 さて、このようなデータ構造を実現するクラスを書いてみよう。これは、安全なファイル書き込みのための定石や、マルチスレッドの安全を確保するためのロックなども入れて、少しだけ実戦的にしてある。

Public Class Item
  Private ReadOnly filename As String
  Private m_code As String

  Public Property Code() As String
    Get
      SyncLock Me
        Return m_code
      End SyncLock
    End Get
    Set(ByVal Value As String)
      SyncLock Me
        m_code = Value
      End SyncLock
    End Set
  End Property

  Public Sub New(ByVal filename As String)
    Me.filename = filename
    If Not File.Exists(filename) Then
      Code = "(no code)"
      Return
    End If
    Dim doc As XmlDocument = New XmlDocument
    doc.Load(filename)
    Code = doc.SelectSingleNode("/item/code").InnerText
  End Sub

  Public Sub Update()
    Dim tempFullPath As String = Path.ChangeExtension(filename, ".$$$")
    Dim bakFullPath As String = Path.ChangeExtension(filename, ".bak")

    SyncLock Me
      Dim writer As XmlTextWriter = New XmlTextWriter(tempFullPath, System.Text.Encoding.UTF8)
      Try
        writer.WriteStartElement("item")
        writer.WriteElementString("code", Code)
      Finally
        writer.Close()
      End Try

      If File.Exists(filename) Then
        File.Delete(bakFullPath)
        File.Move(filename, bakFullPath)
      End If
      File.Move(tempFullPath, filename)
    End SyncLock
  End Sub
End Class
ファイル名にデータ識別情報を埋め込んだサンプル・プログラム(VB.NET版C#版

 ここで、データ識別情報を含んだファイル名を扱うこのようなクラスを作成する上でのポイントをいくつか説明しよう。上記のサンプル・プログラムを見ながら読んでいただきたい。まず、このクラスは、ファイルとして存在するXML文書へのアクセスを全面的に受け持つものであることを確認しよう。つまり、このクラスのインスタンス1個は、ファイルとして存在するXML文書1個に対応する。そのため、読み込むときのファイル名と書き込むときのファイル名は常に同じになる。そこで、ファイル名はメンバ変数filenameに保存して必ずこれを使うようにしている。また、メンバ変数filenameはReadOnlyとすることで、途中で書き換えることができないようにしてある。これにより、インスタンスとファイルの1対1の関係が強く維持される。

 次に、コンストラクタが前の例と異なり、1種類しかない点に注意して見てみよう。このクラスは、ファイルと1対1の関係になるものなので、ファイル名が指定されないコンストラクタに存在価値はない。しかし、新規作成の場合もあり得るので、ファイルがすでに存在するかどうかを確認して、ファイルがなければデフォルト値で初期化する(code = "(no code)")ようにしてある。

 インスタンスの内容は、Codeプロパティへの代入などで変更することができるが、その結果は即座にファイルに反映されない。CodeプロパティのSet処理内でファイルに書き込むことも不可能ではないが、多数の書き換えが発生した場合、不必要な書き込みが繰り返し発生して処理が重くなる。また、同期して書き換えないと意味のない情報は、1つの項目だけが変更されたからといって、そのまま保存するのも不適切だろう。なぜなら、もし同時に変更すべき項目の一部だけ書き換えた段階でプロセスが異常終了しまった場合、一部だけが書き換えられたファイルが残ってしまうためだ。そこで、ファイルへの書き戻しは、Updateというメソッドを作成して、これによって一括して行うようにしてある。前の例ではSaveという名前だったが、ここではUpdateという名前にしてあることに注意していただきたい。あくまで、既存のファイルを更新する機能に特化しているという意味で、Saveとは異なるUpdateという名前を付けている。

 余談になるが、この例ではDOMによってXML文書の読み込みを行っているが、書き込みはDOMではなくXmlTextWriterによって行っている。それは、コーディングの簡潔さという点では、読み込みはDOMに分があり、書き込みはXmlWriter系クラスに分があると感じるためである。常に同じ種類の方法を使わねばならない制約はないので、こういう使い分けもOKである。ただし、DOMのLoadメソッドのパラメータに渡すのは「URL」、XmlTextWriterクラスのコンストラクタに渡すのは「ファイル名」となっていて、構文が異なっていることに注意が必要である。URLとファイル名で扱いが異なる文字が入っていると、書き込みと読み込みのどちらか一方はできるのに、他方はできないというケースがあり得る。そういう場合は、ファイル名をエンコードしたり、LoadメソッドのパラメータにURLではなくFileStreamクラスのインスタンスなどを渡したりといったコードを少し書き足すと容易に解決できる。

 このクラスを使う上で問題になるのは、プログラムの中でこのクラスのインスタンスはいくらでも自由に作成できるため、1インスタンスが1つのXML文書ファイルに対応するという原則が容易に崩れてしまうことである。また、あるタイミングで読み込んだXML文書を別のタイミングで使うためにプログラム中で保存する方法も決まっておらず、いちいち方法を考えていては面倒である。これを解決するためのクラスを次で紹介する。

XML文書をキャッシュする

 上で紹介したクラスを活用するには、一度読み込んだXML文書を管理する小さなモジュール(C#ではクラス)を用意してやるとよい。

Public Module ItemCache
  Private cacheTable As Hashtable = New Hashtable
  Public Function GetItem(ByVal filename As String) As Item
    SyncLock GetType(ItemCache)
      If cacheTable.ContainsKey(filename.ToLower()) Then
        Return CType(cacheTable(filename.ToLower()), Item)
      End If
      Dim item As Item = New Item(filename)
      cacheTable.Add(filename.ToLower(), item)
      Return item
    End SyncLock
  End Function
End Module
読み込んだXML文書をキャッシュするためのモジュール(VB.NET版C#版

 このモジュール(C#ではクラス)のコンセプトは簡単である。ハッシュテーブルにファイル名をキーとして、読み込み済みのItemクラスのインスタンスを保存しておく。同じファイル名に対する要求があった場合は、すでにハッシュテーブルにあればそれを返し、なければ新たなItemクラスのインスタンスを作成するだけである。

 これは以下のようなコードで呼び出して使う。

Dim item1 As Item = ItemCache.GetItem("c:\sample.xml")
System.Diagnostics.Trace.WriteLine(item1.Code)
item1.Code = "cod0123456789"
item1.Update()

Dim item2 As Item = ItemCache.GetItem("c:\sample.xml")
System.Diagnostics.Trace.WriteLine(item2.Code)
ItemCacheモジュールからItemインスタンスを取得するサンプル・プログラム(VB.NET版C#版
ファイル名をキーとして、独自のキャッシュからItemインスタンスを取得している。

 ここで、item1とitem2は同じインスタンスになることに注意しておこう。この場合、item1のインスタンスに対応するXML文書を読み込んだ時点でハッシュテーブルに登録されていて、item2を要求したときには、すでにハッシュテーブルに入っているものが返される。XML文書へのアクセスは必ずGetItemメソッド経由で行うと決めておけば、1つのXML文書の解析は、プロセスが生き続ける範囲内で1回のみに限ることができる。

弱い参照でキャッシュする

 一度解析したXML文書に含まれる情報をずっとメモリ上にとどめておく、という戦略は、メリットをもたらすばかりではない。その情報に再びアクセスしようとしたときに素早いという点ではよい方法ではあるが、めったに参照されない情報まで含めて、すべてがプロセス終了までメモリ上に残るため、メモリ消費量が増えてしまうことも考えられる。メモリ消費量が増えすぎると、かえって情報にアクセスする速度が落ちていくこともあり得る。そのような場合には、「弱い参照」という機能を使って、メモリ不足の状況では読み込んだ情報が破棄されるように作成することができる。

 弱い参照はWeakReferenceクラス(System名前空間)を使うことで容易に実現できる。通常、オブジェクトへの参照がどこかに存在すると、そのオブジェクトが破棄されることはない。しかし、WeakReferenceクラスを通して参照されている場合には、それは破棄対象となる。破棄された場合には、WeakReferenceクラスのTargetプロパティを通して参照を取得しようとすると、Nothing(null)が返ってくる。破棄されていた場合にもう一度解析を行いたいなら、再度解析を行うようにコーディングすればよいわけである。

 以上の機能を組み込んだItemCacheクラスは、以下のようなコード内容になる。なお、破棄されて再読み込みするタイミングを分かりやすく示すために、Traceクラス(System.Diagnostics名前空間)のWriteLineメソッドを1つ入れてある。

Public Module ItemCache
  Private cacheTable As Hashtable = New Hashtable
  Public Function GetItem(ByVal filename As String) As Item
    SyncLock GetType(ItemCache)
      If cacheTable.ContainsKey(filename.ToLower()) Then
        Dim target As Item = CType((CType(cacheTable(filename.ToLower()), WeakReference).Target), Item)
        If Not target Is Nothing Then
          Return target
        End If
        System.Diagnostics.Trace.WriteLine("すでに破棄されていることを検出しました。再読込を行います。")
      End If
      Dim item As Item = New Item(filename)
      cacheTable(filename.ToLower()) = New WeakReference(item)
      Return item
    End SyncLock
  End Function
End Module
「弱い参照」機能を使ってXML文書をキャッシュするサンプル・プログラム(VB.NET版C#版

 このクラスをただ単に使用しても、実際に破棄と再読み込みが起こるかどうかは、確実には予測できない。メモリの破棄が発生するタイミングは、通常はシステムが判断するので、いつそれが行われるともいえないためだ。しかし、それではプログラムの動作を実体験するのに不便なので、確実に再読み込みを発生させるサンプル・プログラムを作成した。以下にそれを示す。

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  Dim item1 As Item = ItemCache.GetItem("c:\sample.xml")
  System.Diagnostics.Trace.WriteLine(item1.Code)
  item1.Code = "cod0123456789"
  item1.Update()
  item1 = Nothing

  System.GC.Collect()

  Dim item2 As Item = ItemCache.GetItem("c:\sample.xml")
  System.Diagnostics.Trace.WriteLine(item2.Code)
End Sub
弱い参照の機能で確実にインスタンスを破棄させるためのサンプル・プログラム(VB.NET版C#版

 これを実行すると以下のような出力が得られる。

cod0123456789
すでに破棄されていることを検出しました。再読込を行います。
cod0123456789
「弱い参照の機能で確実にインスタンスを破棄させるためのサンプル・プログラム」の出力結果

 GCクラス(System名前空間)のCollectメソッドを呼び出した時点で、強制的にガベージ・コレクションが行われ、弱い参照を経由して参照されていたItemクラスのインスタンスは破棄される。ここで、「item1 = Nothing」という行がない場合は、変数item1からの参照があるために破棄されないことに注意が必要である。そして次に、変数item2に格納するために情報を取得した際に、再読みこみが発生するわけである。


 INDEX
  .NETで簡単XML
  第16回 川俣流XMLプログラミングの定石(2)
    1.データを処理しやすい形に変換するタイミング
    2.DOMツリーからデータを抜き取る方法
  3.XML文書とクラスの1対1対応およびXML文書のキャッシュ
    4.文字列定数にXML文書を書き込む場合の改行とインデント
 
インデックス・ページヘ  「連載 :.NETで簡単XML」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間