C# - xmlファイルへ設定情報(自作クラスのフィールド・プロパティ)を読み書きする／色々な値を入れて実験してみた

忙しい人向け ⇒ [３章 まとめ](https://qiita.com/kob58im/items/af82b799215c42e5be2c#3-%E5%AE%9F%E9%A8%93%E7%B5%90%E6%9E%9C%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81)

# 1. 基本
# 1.1. 超概要

`XmlSerializer`クラスを使うと、

```csharp:C#

[Serializable]
public class なんちゃら
{
    public 型名 かんちゃら;
}
```
みたいな自作クラスのインスタンスの値を下記のようにXMLに保存したり、復元（XMLファイルの値に基づいて自作クラスのメンバに値を代入済みのインスタンスを生成）したりできます。

```xml:XMLファイルの中身

<?xml version="1.0" encoding="utf-8"?>
<なんちゃら xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <かんちゃら>ほげ</かんちゃら>
</なんちゃら>
```

# 1.2. やりかた（参考サイト）
丸投げ・・・

 - [オブジェクトの内容をXMLファイルに保存、復元する - .NET Tips (VB.NET,C#...)](https://dobon.net/vb/dotnet/file/xmlserializer.html)
 - [XmlSerializerを使ってシリアライズ／デシリアライズするには？［C#／VB］：.NET TIPS - ＠IT](https://www.atmarkit.co.jp/ait/articles/1704/19/news021.html)
 - [「xmlserializer」の検索結果 - Qiita](https://qiita.com/search?q=xmlserializer)

# 1.3. XMLに保存される対象

`public`なフィールドと、get/setとも`public`なプロパティが保存対象のようです。

[XmlSerializer.Serialize Method (System.Xml.Serialization) | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer.serialize?view=net-5.0) より

> The Serialize method converts <b>the public fields and read/write properties of an object into XML.</b> It does not convert methods, indexers, private fields, or read-only properties. To serialize all an object's fields and properties, both public and private, use the BinaryFormatter.
> 
> In the xmlWriter parameter, specify an object that derives from the abstract XmlWriter class. The XmlTextWriter derives from the XmlWriter.
> 
> Note
> The XmlSerializer cannot serialize the following: arrays of ArrayList and arrays of List<T>.


Google翻訳結果：
> Serializeメソッドは、オブジェクトの<b>パブリックフィールドと読み取り/書き込みプロパティ</b>をXMLに変換します。メソッド、インデクサー、プライベートフィールド、または読み取り専用プロパティは変換されません。パブリックとプライベートの両方のすべてのオブジェクトのフィールドとプロパティをシリアル化するには、BinaryFormatterを使用します。
> 
> xmlWriterパラメーターで、抽象XmlWriterクラスから派生するオブジェクトを指定します。 XmlTextWriterはXmlWriterから派生しています。
>
> 注意
> XmlSerializerは、ArrayListの配列とList <T>の配列をシリアル化できません。

# 1.4. 注意すべきこと

## 1.4.1. 使用上（システム構築／運用上）の注意

XMLファイルはただのテキストファイルであり、直接ユーザーが編集できてしまうので、設定ファイルを切り出すことで、セキュリティ上のリスクや、実行時エラーなどのリスクを生むかもしれません。[^1]
また、C#など.Net系の言語をビルドしたexeファイルは逆アセンブルが容易に可能なため、ハッシュ値などのチェック値を入れるなどの対策だけだと、完全にはガードできないものと思います。

[^1]: コードに直接埋め込んでいても似たようなものかもしれませんが。。

# 1.4.2. 実行ファイルとの相対パスに置きたい場合の注意

 - [作業ディレクトリと実行ディレクトリという罠 - 相対パスに気をつけろ！ - Qiita](https://qiita.com/kob58im/items/daf3757a38e9657aa950)

# 1.4.3. その他

 - 文字コード（UTF-8のBOMの有無にも念のため注意）
 - 例外処理（例：ファイルを開いていて書き込めないとき・・・`System.IO.IOException`）のケア
 - `null`の処置
 - 浮動小数点数を10進数表示することによる誤差
 - エスケープが必要な文字の扱い（XMLを直接編集する場合）

番外：

 - コマンドプロンプト上で日本語等のマルチバイト文字を出力させて試す場合は、コマンドプロンプトのコードページの変更が必要な場合があります。

# 2. 本記事の本題

自作ツールを作っている最中に、設定ファイルを切り出したくなることがよくあります。
デフォルトの設定値を埋め込んだXMLファイルを作りたいけど、それ用のソースコードを用意するのが面倒。
なので、どういう自作クラスを書くとどういうXMLが生成されるのか
をテンプレとして整理してみます。

色々試した結果をおいておきます。

## 2.1. メンバが数値や文字列などの基本的な型の場合

### 2.1.1. XMLへの書き込み

```csharp:C#ソースコード

using System;
using System.IO;

[Serializable]
public class SampleClass
{
    public string   SampleFieldString;
    public int      SampleFieldInt;
    public uint     SampleFieldUInt;
    public long     SampleFieldLong;
    public bool     SampleFieldBool;
    public double   SampleFieldDouble;
    public float    SampleFieldFloat;
    public char     SampleFieldChar;
    public Decimal  SampleFieldDecimal;
    public DateTime SampleFieldDateTime;
}

class SerializerSample
{
	[STAThread]
    static void Main()
    {
        var sampleValues = new SampleClass(){
            SampleFieldString   = "hoge",
            SampleFieldInt      = -1,
            SampleFieldUInt     =  1,
            SampleFieldLong     =  2,
            SampleFieldBool     =  false,
            SampleFieldDouble   =  1.2d,
            SampleFieldFloat    =  1.3f,
            SampleFieldChar     =  'c',
            SampleFieldDecimal  =  1.4m,
            SampleFieldDateTime =  DateTime.Now,
        };

        var serializer = new System.Xml.Serialization.XmlSerializer(typeof(SampleClass));
        using ( var sw = new System.IO.StreamWriter("SampleOutput.xml", false, new System.Text.UTF8Encoding(false)) ) // BOMなしUTF-8
        {
            serializer.Serialize(sw, sampleValues);
        }
    }
}
```

```xml:生成されたXMLファイルSampleOutput.xml

<?xml version="1.0" encoding="utf-8"?>
<SampleClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SampleFieldString>hoge</SampleFieldString>
  <SampleFieldInt>-1</SampleFieldInt>
  <SampleFieldUInt>1</SampleFieldUInt>
  <SampleFieldLong>2</SampleFieldLong>
  <SampleFieldBool>false</SampleFieldBool>
  <SampleFieldDouble>1.2</SampleFieldDouble>
  <SampleFieldFloat>1.3</SampleFieldFloat>
  <SampleFieldChar>99</SampleFieldChar>
  <SampleFieldDecimal>1.4</SampleFieldDecimal>
  <SampleFieldDateTime>2020-11-21T14:52:46.6364558+09:00</SampleFieldDateTime>
</SampleClass>
```

生成されたXMLファイルには型情報は含まれないようです。
（復元するときにチェックされるものと思われますが、調べてないので不明です。）

#### 2.2.1.1. `null`の扱い

`string`型のフィールドである`SampleFieldString`に`null`を代入しておいてXMLを生成したところ、
生成されたXMLから`SampleFieldString`がいなくなりました。

```xml:生成されたXMLファイル

<?xml version="1.0" encoding="utf-8"?>
<SampleClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SampleFieldInt>-1</SampleFieldInt>
  <SampleFieldUInt>1</SampleFieldUInt>
  <SampleFieldLong>2</SampleFieldLong>
  <SampleFieldBool>false</SampleFieldBool>
  <SampleFieldDouble>1.2</SampleFieldDouble>
  <SampleFieldFloat>1.3</SampleFieldFloat>
  <SampleFieldChar>99</SampleFieldChar>
  <SampleFieldDecimal>1.4</SampleFieldDecimal>
  <SampleFieldDateTime>2020-11-21T15:15:07.2962926+09:00</SampleFieldDateTime>
</SampleClass>
```

#### 2.2.1.2. エスケープされそうな文字を入れてみる

```csharp:C#抜粋

SampleFieldString   = "<hoge=foo\"bar\"piyo?piyo!hoge2 hoge3/hoge4\nhoge5\r\nhoge6\thoge7&hoge8;hoge9>",
```

```xml:生成されたXMLファイル抜粋

  <SampleFieldString>&lt;hoge=foo"bar"piyo?piyo!hoge2 hoge3/hoge4
hoge5
hoge6	hoge7&amp;hoge8;hoge9&gt;</SampleFieldString>
```

`<`と`>`と`&`は、それぞれ`&lt;`, `&gt;`, `&amp;`というHTMLっぽい感じにエスケープされています。
改行コード`\r`,`\n`とタブ`\t`はそのまま出力されるようです。

### 2.2.2. XMLからの復元

```csharp:C#
using System;
using System.IO;

[Serializable]
public class SampleClass
{
    同上のため省略
}

class SerializerSample
{
	[STAThread]
    static void Main()
    {
        SampleClass sampleValues;

        var serializer = new System.Xml.Serialization.XmlSerializer(typeof(SampleClass));
        using (var streamReader = new System.IO.StreamReader("SampleOutput.xml", System.Text.Encoding.UTF8))
        using (var xmlReader = System.Xml.XmlReader.Create(streamReader))
        {
            sampleValues = (SampleClass)serializer.Deserialize(xmlReader);
        }

        Console.Write("SampleFieldString  :");   Console.WriteLine(sampleValues.SampleFieldString  );
        Console.Write("SampleFieldInt     :");   Console.WriteLine(sampleValues.SampleFieldInt     );
        Console.Write("SampleFieldUInt    :");   Console.WriteLine(sampleValues.SampleFieldUInt    );
        Console.Write("SampleFieldLong    :");   Console.WriteLine(sampleValues.SampleFieldLong    );
        Console.Write("SampleFieldBool    :");   Console.WriteLine(sampleValues.SampleFieldBool    );
        Console.Write("SampleFieldDouble  :");   Console.WriteLine(sampleValues.SampleFieldDouble  );
        Console.Write("SampleFieldFloat   :");   Console.WriteLine(sampleValues.SampleFieldFloat   );
        Console.Write("SampleFieldChar    :");   Console.WriteLine(sampleValues.SampleFieldChar    );
        Console.Write("SampleFieldDecimal :");   Console.WriteLine(sampleValues.SampleFieldDecimal );
        Console.Write("SampleFieldDateTime:");   Console.WriteLine(sampleValues.SampleFieldDateTime);
    }
}
```

```text:実行結果

SampleFieldString  :hoge
SampleFieldInt     :-1
SampleFieldUInt    :1
SampleFieldLong    :2
SampleFieldBool    :False
SampleFieldDouble  :1.2
SampleFieldFloat   :1.3
SampleFieldChar    :c
SampleFieldDecimal :1.4
SampleFieldDateTime:2020/11/21 14:52:46
```


## 2.2. メンバが配列やリストの場合

```csharp:C#抜粋

using System;
using System.IO;
using System.Collections.Generic;

[Serializable]
public class SampleClass
{
    public List<string>  SampleFieldStringList;
    public string[]      SampleFieldStringArray;
    public List<int>     SampleFieldIntList;
    public int[]         SampleFieldIntArray;
    public List<long>    SampleFieldLongList;
    public long[]        SampleFieldLongArray;
    public List<decimal> SampleFieldDecimalList;
    public decimal[]     SampleFieldDecimalArray;
}

class SerializerSample
{
    [STAThread]
    static void Main()
    {
        var sampleValues = new SampleClass(){
            SampleFieldStringList  = new List<string>(){"hoge1","foo","bar"},
            SampleFieldStringArray =       new string[]{"hoge2","foo","bar"},
            SampleFieldIntList     = new List<int>(){1,2},
            SampleFieldIntArray    =       new int[]{3,4},
            SampleFieldLongList    = new List<long>(){5,6},
            SampleFieldLongArray   =       new long[]{7,8},
            SampleFieldDecimalList = new List<decimal>(){0.1m,0.2m},
            SampleFieldDecimalArray=       new decimal[]{0.3m,0.4m},
        };
        以下略(本記事の2.1.1.章参照)
    }
}
```

```xml:生成されたXMLファイル

<?xml version="1.0" encoding="utf-8"?>
<SampleClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SampleFieldStringList>
    <string>hoge1</string>
    <string>foo</string>
    <string>bar</string>
  </SampleFieldStringList>
  <SampleFieldStringArray>
    <string>hoge2</string>
    <string>foo</string>
    <string>bar</string>
  </SampleFieldStringArray>
  <SampleFieldIntList>
    <int>1</int>
    <int>2</int>
  </SampleFieldIntList>
  <SampleFieldIntArray>
    <int>3</int>
    <int>4</int>
  </SampleFieldIntArray>
  <SampleFieldLongList>
    <long>5</long>
    <long>6</long>
  </SampleFieldLongList>
  <SampleFieldLongArray>
    <long>7</long>
    <long>8</long>
  </SampleFieldLongArray>
  <SampleFieldDecimalList>
    <decimal>0.1</decimal>
    <decimal>0.2</decimal>
  </SampleFieldDecimalList>
  <SampleFieldDecimalArray>
    <decimal>0.3</decimal>
    <decimal>0.4</decimal>
  </SampleFieldDecimalArray>
</SampleClass>
```

- 配列と`List<>`は同じ出力になるようです。
- `<メンバ名><要素の型名>値</要素の型名>・・・</メンバ名>` となるようです。<br>(C#以外(VB.Netとか)だとどうなるのだろうか？)

## 2.3. メンバの型が自作クラスの場合

```csharp:C#

using System;
using System.IO;

[Serializable]
public class SampleClass
{
    public SampleChildClass SampleFieldChildMember;
}

[Serializable]
public class SampleChildClass
{
    public string SampleFieldString;
}

class SerializerSample
{
    [STAThread]
    static void Main()
    {
        var sampleValues = new SampleClass(){
            SampleFieldChildMember  = new SampleChildClass(){SampleFieldString="hoge"},
        };
        以下略(本記事の2.1.1.章参照)
    }
}
```


```xml:生成されたXMLファイル

<?xml version="1.0" encoding="utf-8"?>
<SampleClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SampleFieldChildMember>
    <SampleFieldString>hoge</SampleFieldString>
  </SampleFieldChildMember>
</SampleClass>
```

`<メンバ名><子クラスのメンバ名>値</子クラスのメンバ名>・・・</メンバ名>` となるようです。
(子クラスのクラス名`SampleChildClass`は出力されませんでした。)


## 2.4. メンバが構造体(`System.Drawing.Point`とか`System.Drawing.Size`)の場合

```csharp:C#抜粋

using System;
using System.IO;
using System.Drawing;

[Serializable]
public class SampleClass
{
    public Point SampleFieldPoint;
    public Size  SampleFieldSize;
}

class SerializerSample
{
    [STAThread]
    static void Main()
    {
        var sampleValues = new SampleClass(){
            SampleFieldPoint = new Point(10,20),
            SampleFieldSize  = new Size(30,40),
        };
        以下略(本記事の2.1.1.章参照)
    }
}
```

```xml:生成されたXMLファイル

<?xml version="1.0" encoding="utf-8"?>
<SampleClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <SampleFieldPoint>
    <X>10</X>
    <Y>20</Y>
  </SampleFieldPoint>
  <SampleFieldSize>
    <Width>30</Width>
    <Height>40</Height>
  </SampleFieldSize>
</SampleClass>
```

2.3章と同様ですね。

# 2.5. `Dictionary<,>`・・・そのままだとシリアライズできない

下記エラーがでました。（※見やすさのためエラーメッセージに適宜改行を加えています。）

```text

ハンドルされていない例外: System.InvalidOperationException: 型 'SampleClass' を反映中にエラーが発生しました。
--->
System.NotSupportedException: IDictionary が実装されているため、
型 System.Collections.Generic.Dictionary`2
[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],
 [System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
のメンバー SampleClass.SampleFieldDict1 はシリアル化できません。
```

対応されている方がいらっしゃるので参考のためリンクを貼っておきます。

- [Dictionaryをシリアル化する（XMLファイルに保存） - Qiita](https://qiita.com/rohinomiya/items/b88a5da3965a1c5bed0d)

# 3. 実験結果のまとめ

2章の通り、実験した限りでは下記のようになりました。

- 値が`null`のメンバは出力されない。
- 文字列の場合、文字列中の`<`,`>`,`&`はそれぞれ`&lt;`,`&gt;`,`&amp;`にエスケープされる。
- 型情報は（配列や`List<>`の要素の場合を除き、）基本的に出力されない。
- メンバの型が配列や`List<>`の場合<br>
⇒`<メンバ名><要素の型名>値</要素の型名>・・・</メンバ名>`
- メンバの型が構造体やクラスの場合<br>
⇒`<メンバ名><子クラスのメンバ名>値</子クラスのメンバ名>・・・</メンバ名>`
- `Dictionary<,>`はそのままだとシリアライズできない。(2.5章参照)