CrossLink
ソースジェネレーターと Arc.Collection を使用したC#ライブラリです。
オブジェクト間に複数のリンクを張って、柔軟に管理したり検索したり出来ます。
よく分からない?
オブジェクトT に対して、カスタムList<T> を作成します。しかも、普通のジェネリックコレクションより柔軟で拡張性があり、なおかつ高速です。
一言で言えば、速くて便利!オブジェクトを扱うプログラムでは必須です!
ええ、こんな説明じゃ分からないでしょう。
下のサンプルコードをみてください。
Table of Contents
Quick Start
Performance
How it works
Chains
Features
Serialization
AutoNotify
AutoLink
ObservableCollection
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という内部クラスと、いくつかのプロパティを追加することで動作します。
実際には、
GoshujinClass という内部クラスを追加
Goshujin プロパティを追加
Link 属性が付加されたメンバーに対応するプロパティを追加します。プロパティ名は、メンバー名の頭文字が大文字に変換されたものです(id なら Id になる)。
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という内部クラスを作成します。GoshujinClass は TinyClass を操作・管理するクラスです。
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が更新!
↧