1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

オブジェクトに複数のリンクを張って縦横無尽に操作するライブラリ CrossLink

Posted at

CrossLink

Nuget Build and Test

ソースジェネレーターと Arc.Collection を使用したC#ライブラリです。

オブジェクト間に複数のリンクを張って、柔軟に管理したり検索したり出来ます。

よく分からない?

オブジェクトT に対して、カスタムList<T> を作成します。しかも、普通のジェネリックコレクションより柔軟で拡張性があり、なおかつ高速です。

一言で言えば、速くて便利!オブジェクトを扱うプログラムでは必須です!

ええ、こんな説明じゃ分からないでしょう。

下のサンプルコードをみてください。

Table of Contents

Quick Start

ソースジェネレーターなので、ターゲットフレームワークは .NET 5 以降です。

まずはPackage Manager Consoleでインストール。

Install-Package CrossLink

サンプルコードです。

using System;
using System.Collections.Generic;
using CrossLink;

#pragma warning disable SA1300

namespace ConsoleApp1
{
    [CrossLinkObject] // 対象のクラスに CrossLinkObject属性を追加します
    public partial class TestClass // ソースジェネレーターでコード追加するので、partial classが必須
    {
        [Link(Type = ChainType.Ordered)] // 対象のメンバーにLink属性を追加します。TypeにChainType(Collectionの種類のようなもの)を指定します。
        private int id; // 対象となるメンバー。これを元に、プロパティ Id と IdLink が追加されます。
        // プロパティ Id を使用して、値の取得・更新(値、リンク)を行います。
        // プロパティ IdLink はオブジェクト間の情報を保存します。CollectionのNodeのようなものです。

        [Link(Type = ChainType.Ordered)] // ChainType.Ordered はソート済みコレクション。SortedDictionary と考えていただけば
        public string name { get; private set; } = string.Empty; // プロパティ Name と NameLink が追加

        [Link(Type = ChainType.Ordered)]// 同上
        private int age; // プロパティ Age と AgeLink が追加

        [Link(Type = ChainType.StackList, Name = "Stack")] // Nameで名称を指定して、StackListを追加。コンストラクターには複数のLinkを付加出来ます。
        [Link(Type = ChainType.List, Name = "List")] // Listを追加
        public TestClass(int id, string name, int age)
        {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        public override string ToString() => $"ID:{this.id,2}, {this.name,-5}, {this.age,2}";
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("CrossLink Quick Start.");
            Console.WriteLine();

            var g = new TestClass.GoshujinClass(); // まずは、オブジェクト管理のクラス Goshujin を作成
            new TestClass(1, "Hoge", 27).Goshujin = g; // TestClassを作成し、Goshujinを設定します。Goshujin側にもTestClassが登録されます。
            new TestClass(2, "Fuga", 15).Goshujin = g;
            new TestClass(1, "A", 7).Goshujin = g;
            new TestClass(0, "Zero", 50).Goshujin = g;

            ConsoleWriteIEnumerable("[List]", g.ListChain); // ListChain(コンストラクタにLinkが追加されたやつ)は実質的に List<TestClass> です
            /* Result;  作成順に並びます
                 ID: 1, Hoge , 27
                 ID: 2, Fuga , 15
                 ID: 1, A    ,  7
                 ID: 0, Zero , 50 */

            Console.WriteLine("ListChain[2] : "); // インデックスアクセスが可能
            Console.WriteLine(g.ListChain[2]); // ID: 1, A    ,  7
            Console.WriteLine();

            ConsoleWriteIEnumerable("[Sorted by Id]", g.IdChain);
            /* IdChain は ChainType.Ordered なので、ソート済み
                 ID: 0, Zero , 50
                 ID: 1, Hoge , 27
                 ID: 1, A    ,  7
                 ID: 2, Fuga , 15 */

            ConsoleWriteIEnumerable("[Sorted by Name]", g.NameChain);
            /* 同様にNameでソート済み
                 ID: 1, A    ,  7
                 ID: 2, Fuga , 15
                 ID: 1, Hoge , 27
                 ID: 0, Zero , 50 */

            ConsoleWriteIEnumerable("[Sorted by Age]", g.AgeChain);
            /* 同様にAgeでソート済み
                 ID: 1, A    ,  7
                 ID: 2, Fuga , 15
                 ID: 1, Hoge , 27
                 ID: 0, Zero , 50 */

            var t = g.ListChain[1];
            Console.WriteLine($"{t.Name} age {t.Age} => 95");
            t.Age = 95; // Fugaの年齢を95にすると、
            ConsoleWriteIEnumerable("[Sorted by Age]", g.AgeChain);
            /* なんと AgeChain が更新されています!
                 ID: 1, A    ,  7
                 ID: 1, Hoge , 27
                 ID: 0, Zero , 50
                 ID: 2, Fuga , 95 */

            ConsoleWriteIEnumerable("[Stack]", g.StackChain);
            /* こちらは Stack
                 ID: 1, Hoge , 27
                 ID: 2, Fuga , 95
                 ID: 1, A    ,  7
                 ID: 0, Zero , 50 */

            t = g.StackChain.Pop(); // Stackの先頭のオブジェクトを取得し、Stackから削除します。影響するのはStackChainだけなのでご注意ください。
            Console.WriteLine($"{t.Name} => Pop");
            t.Goshujin = null; // 他のChainから削除するには、Goshujinをnullにします。
            Console.WriteLine();

            ConsoleWriteIEnumerable("[Stack]", g.StackChain);
            /* Zero が解放されました・・・
                 ID: 1, Hoge , 27
                 ID: 2, Fuga , 95
                 ID: 1, A    ,  7 */

            var g2 = new TestClass.GoshujinClass(); // Goshujin2 を作成
            t = g.ListChain[0];
            Console.WriteLine($"{t.Name} Goshujin => Goshujin2");
            Console.WriteLine();
            t.Goshujin = g2; // Goshujin から Goshujin2 に変更すると
            ConsoleWriteIEnumerable("[Goshujin]", g.ListChain);
            ConsoleWriteIEnumerable("[Goshujin2]", g2.ListChain);
            /*  各種Chainが更新されます
             *  [Goshujin]
                 ID: 2, Fuga , 95
                 ID: 1, A    ,  7
                [Goshujin2]
                 ID: 1, Hoge , 27*/

            // g.IdChain.Remove(t); // t は Goshujin2 の所有物なので、これはエラー
            // t.Goshujin.IdChain.Remove(t); // こちらはOK(t.GosjujinはGoshujin2)
            
            Console.WriteLine("[IdChain First/Next]");
            t = g.IdChain.First; // Link interfaceを使って、オブジェクトを列挙します
            while (t != null)
            {
                Console.WriteLine(t);
                t = t.IdLink.Next; // Nextの型はLinkではなく、Objectそのものなのでご注意ください
            }

            static void ConsoleWriteIEnumerable<T>(string? header, IEnumerable<T> e)
            {// オブジェクトを画面に出力
                if (header != null)
                {
                    Console.WriteLine(header);
                }

                foreach (var x in e)
                {
                    Console.WriteLine(x!.ToString());
                }

                Console.WriteLine();
            }
        }
    }
}

Performance

パフォーマンスは最優先事項です。

CrossLinkは、ジェネリックコレクションより込み入った処置を行っていますが、実際はジェネリックコレクションより高速に動作します(主にArc.Collectionのおかげです)。

SortedDictionary<TKey, TValue> と比べてみましょう。

H2HClass という簡単なクラスを作成します。

[CrossLinkObject]
public partial class H2HClass2
{
    public H2HClass2(int id)
    {
        this.id = id;
    }

    [Link(Type = ChainType.Ordered)]
    private int id;
}

ジェネリック版。クラスを作成し、コレクションに追加していきます。

var g = new SortedDictionary<int, H2HClass>();
foreach (var x in this.IntArray)
{
    g.Add(x, new H2HClass(x));
}

こちらはCrossLink版。同じような処理をしています。

var g = new H2HClass2.GoshujinClass();
foreach (var x in this.IntArray)
{
    new H2HClass2(x).Goshujin = g;
}

こちらが結果。 なんとSortedDictionary<TKey, TValue> より高速です。

Method Length Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
NewAndAdd_SortedDictionary 100 7,209.8 ns 53.98 ns 77.42 ns 1.9379 - - 8112 B
NewAndAdd_CrossLink 100 4,942.6 ns 12.28 ns 17.99 ns 2.7084 0.0076 - 11328 B

Id を変更すると、当然コレクションの更新(値の削除・追加)が必要です。

CrossLinkは断然高速で、SortedDictionary の約3倍のパフォーマンスです(CrossLinkは内部でNodeを保持しているので、当然と言えば当然ですが)。

Method Length Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
RemoveAndAdd_SortedDictionary 100 1,491.1 ns 13.01 ns 18.24 ns 0.1335 - - 560 B
RemoveAndAdd_CrossLink 100 524.1 ns 3.76 ns 5.63 ns 0.1717 - - 720 B

How it works

CrossLinkは既存のクラスに、GoshujinClassという内部クラスと、いくつかのプロパティを追加することで動作します。

実際には、

  1. GoshujinClass という内部クラスを追加
  2. Goshujin プロパティを追加
  3. Link 属性が付加されたメンバーに対応するプロパティを追加します。プロパティ名は、メンバー名の頭文字が大文字に変換されたものです(id なら Id になる)。
  4. Link 属性が付加されたメンバーに対応するLink フィールドを追加します。こちらの名称は、プロパティ名にLinkがついたものになります(Id なら IdLink になる)。

という流れです。

用語

  • Object: 情報を保持する、一般的なオブジェクト。
  • Goshujin: オブジェクトのオーナークラス。このクラスを介して、オブジェクトの管理・操作を行います。
  • Chain: コレクションのようなもの。Goshujin は複数の Chain を保持し、オブジェクトを様々な形式で管理できます。
  • Link: コレクションにおけるNodeのようなもの。オブジェクトは内部に複数のLinkを持ち、オブジェクト間の情報を保持します。

実際に、ソースジェネレーターでどのようなコードが生成され、どのようにCrossLinkが動作するのか見てみましょう。

まずは TinyClass という非常にシンプルなクラスを作成します。メンバーは id 一つだけです。

public partial class TinyClass // partial class が必須
{
    [Link(Type = ChainType.Ordered)] // Link属性を追加
    private int id;
}

プロジェクトをビルドすると、CrossLinkはまず GoshujinClassという内部クラスを作成します。GoshujinClassTinyClass を操作・管理するクラスです。

public sealed class GoshujinClass : IGoshujin
{// ご主人様は、日本語で Goshujin-sama という意味です
    
    public GoshujinClass()
    {
        // IdChainはTinyClassのソート済みコレクションです
        this.IdChain = new(this, static x => x.__gen_cl_identifier__001, static x => ref x.IdLink);
    }

    public OrderedChain<int, TinyClass> IdChain { get; } // 内部では Arc.Collection のコレクションクラスを使用しています
}

次のコードでは Goshujin インスタンス/プロパティを追加します。

private GoshujinClass? __gen_cl_identifier__001; // 実際の Goshujinインスタンス

public GoshujinClass? Goshujin
{
    get => this.__gen_cl_identifier__001;
    set
    {// Goshujinインスタンスをセットします
        if (value != this.__gen_cl_identifier__001)
        {
            if (this.__gen_cl_identifier__001 != null)
            {// TinyClassを以前のGoshujinから解放します
                this.__gen_cl_identifier__001.IdChain.Remove(this);
            }

            this.__gen_cl_identifier__001 = value;// インスタンスを設定します
            if (value != null)
            {// 新しいGoshujinにお仕えします
                value.IdChain.Add(this.id, this);
            }
        }
    }
}

最後に、メンバーに対応する Link と プロパティを追加します。

inally, CrossLink adds a link and a property which is used to modify the collection and change the value.

public OrderedChain<int, TinyClass>.Link IdLink; // Link is like a Node.

public int Id
{// プロパティ "Id" は、メンバー "id" から作成されました
    get => this.id;
    set
    {
        if (value != this.id)
        {
            this.id = value;
            // 値が更新されると、IdChainも更新されます
            this.Goshujin.IdChain.Add(this.id, this);
        }
    }
}

Chains

Chainはオブジェクトのコレクションクラスのようなもので、CrossLinkでは以下のChainを実装しています。

Name Structure Access Add Remove Search Sort Enum.
ListChain Array Index O(1) O(n) O(n) O(n log n) O(1)
LinkedListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1)
QueueListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1)
StackListChain Linked list Node O(1) O(1) O(n) O(n log n) O(1)
OrderedChain RB Tree Node O(log n) O(log n) O(log n) Sorted O(log n)
ReverseOrderedChain RB Tree Node O(log n) O(log n) O(log n) Sorted O(log n)
UnorderedChain Hash table Node O(1) O(1) O(1) - O(1)
ObservableChain Array Index O(1) O(n) O(n) O(n log n) O(1)

こーゆーChainが欲しい的な要望ありましたらご連絡ください。

Features

Serialization

複雑にリンクされたオブジェクトのシリアライズは結構面倒です。

しかし、Tinyhand との合わせ技で簡単にシリアライズできます!

やり方は簡単。Tinyhand パッケージをインストールして、TinyhandObject 属性を追加して、Key 属性を各メンバーに追加するだけです!

Install-Package Tinyhand
[CrossLinkObject]
[TinyhandObject] // TinyhandObject属性を追加
public partial class SerializeClass // partial class を忘れずに
{
    [Link(Type = ChainType.Ordered, Primary = true)] // Primary Link(すべてのオブジェクトが登録されるLink)を指定すると、さらにシリアライズのパフォーマンスが向上します
    [Key(0)] // Key属性(シリアライズの識別子。stringかint)を追加
    private int id;

    [Link(Type = ChainType.Ordered)]
    [Key(1)]
    private string name = default!;

    public SerializeClass()
    {// Tinyhandのデシリアライズ処理のため、デフォルトコンストラクタ(引数のないコンストラクタ)が必要です
    }

    public SerializeClass(int id, string name)
    {
        this.id = id;
        this.name = name;
    }
}

テストコード:

var g = new SerializeClass.GoshujinClass(); // Goshujinを作成
new SerializeClass(1, "Hoge").Goshujin = g; // オブジェクト追加
new SerializeClass(2, "Fuga").Goshujin = g;

var st = TinyhandSerializer.SerializeToString(g); // これだけでシリアライズ出来ます!
var g2 = TinyhandSerializer.Deserialize<SerializeClass.GoshujinClass>(TinyhandSerializer.Serialize(g)); // バイナリにシリアライズして、それをデシリアライズします。簡単でしょう?

AutoNotify

Link 属性の AutoNotifyプロパティを true にすると、CrossLinkは INotifyPropertyChanged を自動で実装します。

[CrossLinkObject]
public partial class AutoNotifyClass
{
    [Link(AutoNotify = true)] // AutoNotifyをtrueに
    private int id;

    public void Reset()
    {
        this.SetProperty(ref this.id, 0); // SetPropertyを呼ぶと、手動で値の更新とPropertyChanged の呼び出しが出来ます。
    }
}

テストコード:

var c = new AutoNotifyClass();
c.PropertyChanged += (s, e) => { Console.WriteLine($"Id changed: {((AutoNotifyClass)s!).Id}"); };
c.Id = 1; // 値を変更すると、自動的に PropertyChange が呼ばれます。
c.Reset(); // 手動で

生成コード:

public partial class AutoNotifyClass : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;

    protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(storage, value))
        {
            return false;
        }
        
        storage = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        return true;
    }

    public int Id
    {
        get => this.id;
        set
        {
            if (value != this.id)
            {
                this.id = value;
                this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs("Id"));
            }
        }
    }
}

AutoLink

デフォルトの動作では、オブジェクトのGoshujinが設定されると自動でオブジェクトをリンク(GoshujinのChainに登録する)します。

自動でリンクしたくない場合は、AutoLink プロパティを false に設定してください。

[CrossLinkObject]
public partial class ManualLinkClass
{
   [Link(Type = ChainType.Ordered, AutoLink = false)] // AutoLinkをfalse
   private int id;

   public ManualLinkClass(int id)
   {
       this.id = id;
   }

   public static void Test()
   {
       var g = new ManualLinkClass.GoshujinClass();

       var c = new ManualLinkClass(1);
       c.Goshujin = g; // 自動でリンクされません
       Debug.Assert(g.IdChain.Count == 0, "Chain is empty.");

       g.IdChain.Add(c.id, c); // 手動でリンクします
       Debug.Assert(g.IdChain.Count == 1, "Object is linked.");
   }
}

ObservableCollection

MVVM?バインディング?

面倒なことばかりでしょう。

ObservableChain を使うと、簡単にバインディングできます。

コンストラクタに [Link(Type = ChainType.Observable, Name = "Observable")] を追加するだけです。

[CrossLinkObject]
public partial class ObservableClass
{
    [Link(Type = ChainType.Ordered, AutoNotify = true)] // もちAutoNotify
    private int id { get; set; }

    [Link(Type = ChainType.Observable, Name = "Observable")]
    public ObservableClass(int id)
    {
        this.id = id;
    }
}

テストコード:

var g = new ObservableClass.GoshujinClass();
ListView.ItemSource = g.ObservableChain;// ObservableChainをObservableCollectionのように使用できます
new ObservableClass(1).Goshujin = g;// これでListViewが更新!
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?