砂漠の旅人(たびと)

UNIX / MS-DOS 時代から電脳砂漠を旅しています

C#の自己参照クラスをJSONで出力すると、どんな結果になるのか?

こんにちは、たびとです。

昔、C言語で自己参照構造体という構造体の中に自分自身の構造体を定義することにより、 数珠つなぎに構造体を連結させていました。 その時は、現在の Excel のような簡易的な表計算の入力ライブラリとして、 1行を1構造体で表現し、構造体中の自己参照の部分で前後の行を 参照するといった使い方をしていました。

C言語の時は、複雑なポインタの制御で散々な苦労をしましたが、 これを C# のクラスで作ると簡単に実装できてしまいます。

この C# で作成した自己参照クラス(正式な名称なのかな?)を JSON で出力すると、 どんな結果になるのでしょうか?

この記事の対象者

  • C# で自己参照クラスを使ってみたい方
  • 自己参照クラスを JSON で出力した結果に興味のある方
  • C# における開発スキルの幅を広げたい方

C# による自己参照クラス

C言語からある自己参照には大きく2通りの使い方があります。 (自己参照クラスって正しい呼び方なのかな?ここではC言語にならって、便宜上、自己参照クラスと呼びます。)

  • 片方向のみ参照ができる
  • 双方向の参照ができる

片方向の自己参照クラス

適当なクラスとして、SelfRef を作成し、プロパティは Id と Name を定義します。 自己参照は、「 SelfRef? Next 」の箇所で、SelfRef クラスの中に、 自分自身のクラスである SelfRef クラスを定義しています。

クラスを New (インスタンス化)するたびに、ひとつ前の Next に今回作ったクラスを代入することで連結していきます。 片方向なので、連結した次のクラスへの参照はできますが、前には戻れません。

public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;

    public SelfRef? Next { get; set; } = null;
}

双方向の自己参照クラス

片方向のクラスに、もう一つ「 SelfRef? Prev 」を定義しました。 これで双方向クラスの出来上がりです。

クラスを New するたびに、ひとつ前の Next に今回作ったクラスを代入し、 今回作ったクラスの Prev にひとつ前のクラスを代入することで連結していきます。 前後の情報を持っているため、前後に進むことができます。

public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;

    public SelfRef? Prev { get; set; } = null;
    public SelfRef? Next { get; set; } = null;
}

自己参照クラスの実装

それでは、実際に動作するプログラムを作ってみましょう。 Visual Studio 2022 の C# .NET 6 で作っています。

片方向の実装

1から9までのクラスを連結する簡単なプログラムを作ってみます。 自己参照で重要なのは、先頭を保持するクラスで、 ここでは firstData に格納しています。

格納し終わったら、firstData から最初のクラスを取り出し、 Next を参照することで次のクラスを取り出して表示させます。

SelfRef? firstData = null;          //先頭
var lastData = firstData;
for (int i = 1; i < 10; i++)
{
    // クラス作成
    var current = new SelfRef()
    {
        Id = i,
        Name = $"Name{i}",
    };

    if (firstData == null) firstData = current;     // 先頭を格納する
    if (lastData != null) lastData.Next = current;  // ひとつ前に連結する
    lastData = current;
}

// 先頭から順番に表示する
Console.WriteLine("Next data.");
for (var data = firstData; data != null; data = data.Next)
{
    Console.WriteLine($" {data.Id}: {data.Name}.");
}

// 自己参照クラスの定義
public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;
    public SelfRef? Next { get; set; } = null;
}

このプログラムの実行結果は以下の通りです。

Next data.
 1: Name1.
 2: Name2.
 3: Name3.
 4: Name4.
 5: Name5.
 6: Name6.
 7: Name7.
 8: Name8.
 9: Name9.

双方向の実装

片方向クラスに前のクラスを参照するための「Prev」を追加します。 前の参照先を追加すればいいので、クラス生成時に前のクラス(lastData)を Prev に代入して連結させます。

また、表示の時には、ひとつ前を参照できるため、最後のデータとなる lastData から Prev をたどることで、前に遡ってデータを参照することが可能になります。

SelfRef? firstData = null;          //先頭
var lastData = firstData;
for (int i = 1; i < 10; i++)
{
    // クラス作成
    var current = new SelfRef()
    {
        Prev = lastData,            // ひとつ前を連結する
        Id = i,
        Name = $"Name{i}",
    };

    if (firstData == null) firstData = current;     // 先頭を格納する
    if (lastData != null) lastData.Next = current;  // ひとつ前に連結する
    lastData = current;
}

// 先頭から順番に表示する
Console.WriteLine("Next data.");
for (var data = firstData; data != null; data = data.Next)
{
    Console.WriteLine($" {data.Id}: {data.Name}.");
}

// 最後尾から順番に表示する
Console.WriteLine("Prev data.");
for (var data = lastData; data != null; data = data.Prev)
{
    Console.WriteLine($" {data.Id}: {data.Name}.");
}

// 自己参照クラスの定義
public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;

    public SelfRef? Prev { get; set; } = null;
    public SelfRef? Next { get; set; } = null;

}

このプログラムの実行結果は以下の通りです。

Next data.
 1: Name1.
 2: Name2.
 3: Name3.
 4: Name4.
 5: Name5.
 6: Name6.
 7: Name7.
 8: Name8.
 9: Name9.
Prev data.
 9: Name9.
 8: Name8.
 7: Name7.
 6: Name6.
 5: Name5.
 4: Name4.
 3: Name3.
 2: Name2.
 1: Name1.

JSON化してみる

JSONを使うため、Visual Studio 2022 の NuGet から Newtonsoft.Json をソリューションに追加します。

片方向のJSON出力

Newtonsoft.Json JsonConvert.SerializeObject を使って、クラスの内容を JSON形式に変換し、適当な場所へファイルを出力します。

using Newtonsoft.Json;

SelfRef? firstData = null;          //先頭
var lastData = firstData;
for (int i = 1; i < 10; i++)
{
    // クラス作成
    var current = new SelfRef()
    {
        Id = i,
        Name = $"Name{i}",
    };

    if (firstData == null) firstData = current;     // 先頭を格納する
    if (lastData != null) lastData.Next = current;  // ひとつ前に連結する
    lastData = current;
}

// JSON
var jsonText = JsonConvert.SerializeObject(firstData);
using var sw1 = new StreamWriter(@"C:\Users\Public\SelfRef.json", false);
sw1.WriteLine(jsonText);

// 自己参照クラスの定義
public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;

    public SelfRef? Next { get; set; } = null;
}

Visual Studio Code で開いて、「ドキュメントのフォーマット Shift + Alt + F」で整形した結果を以下に記載します。

{
    "Id": 1,
    "Name": "Name1",
    "Next": {
        "Id": 2,
        "Name": "Name2",
        "Next": {
            "Id": 3,
            "Name": "Name3",
            "Next": {
                "Id": 4,
                "Name": "Name4",
                "Next": {
                    "Id": 5,
                    "Name": "Name5",
                    "Next": {
                        "Id": 6,
                        "Name": "Name6",
                        "Next": {
                            "Id": 7,
                            "Name": "Name7",
                            "Next": {
                                "Id": 8,
                                "Name": "Name8",
                                "Next": {
                                    "Id": 9,
                                    "Name": "Name9",
                                    "Next": null
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Next が入れ子状態になっていて、段々畑のようになっています。

双方向のJSON出力

双方向を JSON で出力すると Prev と Next が相互参照している状態なので、 循環参照となり、 JsonConvert.SerializeObject メソッドでエラーとなります。

Newtonsoft.Json.JsonSerializationException: 'Self referencing loop 
detected for property 'Prev' with type 'SelfRef'. Path 'Next'.'

これを回避するには、Prev と Next のどちらかの出力を諦めるしかありません。 JsonIgnore をプロパティの先頭に付けることにより、JSON出力時に除外させることができます。

// 自己参照クラスの定義
public class SelfRef
{
    public int Id { get; set; }
    public string Name { get; set; } = String.Empty;

    [JsonIgnore]
    public SelfRef? Prev { get; set; } = null;
    public SelfRef? Next { get; set; } = null;
}

JsonIgnore により、Prev はJSON出力が除外されます。 このため、JSON出力の内容は、片方向と同じ結果になります。

まとめ

今回は、自己参照クラスの定義と連結の仕組みを理解するため、 自己参照クラスを使って、JSON に出力すると結果がどうなるのかを確認しました。 JSON への出力結果は、自己参照の箇所がネストされて表示される結果となりました。

かつて C 言語では敷居が高かった自己参照構造体が、 C# では簡単に定義できることに時代の変化が感じられます。

では、皆さん、よい旅を。