C# でツリー構造のデータを使う

なんで .Net にツリー構造のコレクションがないんでしょうね?

TreeNode と TreeView は GUI 用。
Hierarchy は Microsoft SQL Server Analysis Services 用。
TreeElement は Microsoft Docs で検索してもひっかからない。

欲しいのは純粋なツリー構造のデータを扱うためのコレクションであって、余計なメンバは要らない…。
私が探した範疇では .Net には見つかりませんでした。

C# でツリー構造を使うためのライブラリ

探したら NuGet でインストールして使えるパッケージを見つけました。

クラス名が Microsoft.AnalysisServices.AdomdClient にある Hierarchy と被っててややこしいですが、別のものです。

Visual Studio > ツール > NuGet パッケージマネージャー > パッケージマネージャーコンソール を開いて
コンソールに Install-Package Hierarchy -Version 1.1.1 を入力すれば使えるようになります。

ノードの ID と、そのノードの親の ID を struct か class に定義して、それを List に突っ込み、ToHierarchy() か ToGraph() するだけです。

以下は https://github.com/Unskilledcrab/Hierarchy にあるサンプルコードをビルドが通るように手直ししたものです。

using System;
using System.Collections.Generic;
using System.Linq;
using Hierarchy;

public class Person {
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string Name { get; set; } = "";
    public override string ToString() {
        return $"Person: {Id} '{Name}'";
    }
}

class Test {
    static void Main() {
        List flatList = new() {
            new() { Id = 1, ParentId = 0, Name = "CEO" },
            new() { Id = 2, ParentId = 1, Name = "North America Operaror" },
            new() { Id = 3, ParentId = 1, Name = "South America Operator" },
            new() { Id = 12, ParentId = 3, Name = "Brazil Operator" },
            new() { Id = 10, ParentId = 1, Name = "Europe Operator" },
            new() { Id = 11, ParentId = 1, Name = "Africa Operator" },
            new() { Id = 4, ParentId = 0, Name = "CFO" },
            new() { Id = 5, ParentId = 4, Name = "Financial Manager" },
            new() { Id = 6, ParentId = 4, Name = "Financial Designer" },
            new() { Id = 7, ParentId = 4, Name = "Financial Senior Team Lead" },
            new() { Id = 14, ParentId = 7, Name = "Financial Project 2: Lead" },
            new() { Id = 13, ParentId = 7, Name = "Financial Project 1: Lead" },
            new() { Id = 15, ParentId = 13, Name = "Financial Project 1: Member 1" },
            new() { Id = 16, ParentId = 13, Name = "Financial Project 1: Member 2" },
            new() { Id = 17, ParentId = 13, Name = "Financial Project 1: Member 3" },
            new() { Id = 18, ParentId = 13, Name = "Financial Project 1: Member 4" },
            new() { Id = 19, ParentId = 14, Name = "Financial Project 2: Member 1" },
            new() { Id = 20, ParentId = 14, Name = "Financial Project 2: Member 2" },
            new() { Id = 21, ParentId = 14, Name = "Financial Project 2: Member 3" },
            new() { Id = 22, ParentId = 14, Name = "Financial Project 2: Member 4" },
            new() { Id = 23, ParentId = 22, Name = "Financial Project 2: Grunt 1" },
            new() { Id = 24, ParentId = 22, Name = "Financial Project 2: Grunt 2" },
            new() { Id = 25, ParentId = 22, Name = "Financial Project 2: Grunt 3" },
            new() { Id = 26, ParentId = 22, Name = "Financial Project 2: Grunt 4" },
            new() { Id = 27, ParentId = 26, Name = "Financial Project 2: Leaf 1" },
            new() { Id = 30, ParentId = 26, Name = "Financial Project 2: Leaf 4" },
            new() { Id = 28, ParentId = 26, Name = "Financial Project 2: Leaf 2" },
            new() { Id = 29, ParentId = 26, Name = "Financial Project 2: Leaf 3" },
            new() { Id = 8, ParentId = 4, Name = "Marketing Manager" },
            new() { Id = 9, ParentId = 0, Name = "COO" },
        };

        // NOTE: You can order the incoming list before building the hierarchy 
        //       to make sure that your hierarchy is ordered
        //       the way you want it to be before presenting and traversing it
        var hierarchyList = flatList.OrderBy(f => f.Id).ToHierarchy(t => t.Id, t => t.ParentId);
        Console.WriteLine("We convert the flat list to a hierarchy");
        Console.WriteLine(hierarchyList.PrintTree());

        // NOTE: When you want to search through the entire tree, 
        //       you must start with the **AllNodes()** extension method 
        //       this will make sure you aren't performing linq operations just on the nodes at the top level
        var node = hierarchyList.AllNodes().First(n => n.Data.Id == 14);
        Console.WriteLine("We search through all nodes in the hierarchy for the one with this id");
        Console.WriteLine(node.Data);
        Console.WriteLine();

        var siblingNodes = node.SiblingNodes();
        Console.WriteLine("We get all other nodes that are at the same level as this node");
        Console.WriteLine(siblingNodes.PrintNodes());

        var childNodesBreadthFirst = node.DescendantNodes(TraversalType.BreadthFirst);
        Console.WriteLine("We get all descendant nodes of this node (Breadth First)");
        Console.WriteLine(childNodesBreadthFirst.PrintNodes());

        var childNodesDepthFirst = node.DescendantNodes(TraversalType.DepthFirst);
        Console.WriteLine("We get all descendant nodes of this node (Depth First)");
        Console.WriteLine(childNodesDepthFirst.PrintNodes());

        var parentNodes = node.AncestorNodes();
        Console.WriteLine("We get all ancestor nodes of this node");
        Console.WriteLine(parentNodes.PrintNodes());

        var leafNodesBreadthFirst = node.LeafNodes(TraversalType.BreadthFirst);
        Console.WriteLine("We get all leaf nodes (descendant nodes that do not have childen) of this node (Breadth First)");
        Console.WriteLine(leafNodesBreadthFirst.PrintNodes());

        var leafNodesDepthFirst = node.LeafNodes(TraversalType.DepthFirst);
        Console.WriteLine("We get all leaf nodes (descendant nodes that do not have childen) of this node (Depth First)");
        Console.WriteLine(leafNodesDepthFirst.PrintNodes());

        var rootNode = node.RootNode();
        Console.WriteLine("We get the top level node of this branch (the highest level ancestor)");
        Console.WriteLine(rootNode.Data);
    }
}

List を使っているので、Enumerator も使えますし、Linq にも対応してます。
ツリーは値の範囲検索をするときに、何度もルートから検索し直す必要があるのですが、線形リストを使って疑似的に?ツリーを表現しているので、値の範囲検索もしやすそうですね。

ノード用のクラスは HierarhyNode と GraphNode の2種類ありますが、違いは HierarchyNode は親(parent)がひとつ、GraphNode は親を複数持つ点だけです。
目的に応じて使い分けます。

サンプルコードや説明文は英語で書かれていますが、以下のサイトで機械翻訳すれば分かると思います。
以前は英語のドキュメントを日本語訳した記事を書いていたのですが、ブログを書く時間が取りにくくなったことと、機械翻訳が優秀になったのでやめました。

【Unity】スナップ処理を作る(2D用)

スナップ処理とは、UI によくある、何かのオブジェクトにある程度近づくと、くっつく(ように見える)処理です。
正確には、一定の場所に留める処理です。

本稿ではスナップ処理を Unity で作る方法について説明します。
細かい点までは説明していませんので、自分ひとりで Unity を使ってゲームを作れる中級者以上の方を対象としています。

執筆時に使用した Unity のバージョンは、2021.2.3f1
Windows10(Home) 64bit 版で作成し、動作確認しています。

“【Unity】スナップ処理を作る(2D用)” の続きを読む

【Unity】C#スクリプトをDLL化する手順

この記事では Unity の C# スクリプトをDLL化する手順と注意点について説明します。

執筆時に扱った Unity のバージョンは 2021.1.6f1 です。
プラットフォームは Windows 10 (64ビット版) を対象としています。

“【Unity】C#スクリプトをDLL化する手順” の続きを読む

【Unity】Google スプレッドシートから直接データを読む方法

この記事では、↑の内容を試してみて、分かりにくかったところや疑問に思って調べたことをまとめています。
補足説明的な感じで読んでいただければと思います。

執筆時に扱った Unity のバージョンは 2021.1.5f1 です。
プラットフォームは Windows 10 (64ビット版) を対象としています。

この記事では、Google スプレッドシートを GSS と略記します。

“【Unity】Google スプレッドシートから直接データを読む方法” の続きを読む

【UE4 C++】デリゲートの使い方まとめ

この記事では、Unreal Engine の C++ でデリゲートを扱う方法についてまとめています。

執筆時に扱った Unreal Engine のソースコードのバージョンは 4.25.4
プラットフォームは Windows 10 (64ビット版) を対象としています。
ビルドに使用したアプリ(IDE)は Microsoft Visual Studio 2019 です。

以降、Unreal Engine 4 を UE4 と略記します。

“【UE4 C++】デリゲートの使い方まとめ” の続きを読む

【Unity】Addressable Asset System 公式マニュアルの和訳 Part3

もくじ
Part1 アドレッサブル・アセット・システム
Part2 アドレッサブル・アセットの概要
Part3 始めてみる(この記事)
Part4 開発サイクル
Part5 ホスティング・サービス
Part6 メモリ管理
Part7 非同期命令の処理
Part8 カスタム命令
Part9 解析
PartX マイグレーション・ガイド

これはゲーム開発エンジン Unity に関する記事です。
この記事は、
https://docs.unity3d.com/Packages/com.unity.addressables@1.1/manual/AddressableAssetsGettingStarted.html
に書かれている内容を日本語に翻訳したものです。

2021/01/07
非同期命令の説明に Assembly Definition ファイルについての補足を追加。

“【Unity】Addressable Asset System 公式マニュアルの和訳 Part3” の続きを読む

【.Net C#】Enumerator の使い方

この記事では Microsoft .Net Framework の Enumerator および IEnumerable<T> の使い方について説明しています。

動作確認した環境
Microsoft Visual C# Compiler 3.5.0 beta4
.Net Framework 4.8.03752

2021/01/07
サンプルソースコード内 GetEnumerator() の戻り値の型を IEnumerable から IEnumerator に修正。
 
“【.Net C#】Enumerator の使い方” の続きを読む

【Unity】Package Name has not been set up correctly

2020.2.1f1 で Android ビルドしたところ、以下の点でつまづいたのでメモしておきます。

ビルド時に

Package Name has not been set up correctly

と書かれたエラーダイアログが表示され、ビルドに失敗する。

Player Settings の Company Name か Product Name に日本語が含まれると、このエラーが表示されるようになったっぽいです。
半角の英語表記に直すことでビルドが通るようになりました。

Company Name と Product Name を設定する際に注意する点

・日本語は使えない
・半角英数字と半角のアンダースコア _ ←コレ のみ使用可能
・数字かアンダースコアから始まる名前はダメ

【Unity】Addressable Asset System 公式マニュアルの和訳 Part4

もくじ
Part1 アドレッサブル・アセット・システム
Part2 アドレッサブル・アセットの概要
Part3 始めてみる
Part4 開発サイクル(この記事)
Part5 ホスティング・サービス
Part6 メモリ管理
Part7 非同期命令の処理
Part8 カスタム命令
Part9 解析
PartX マイグレーション・ガイド

これはゲーム開発エンジン Unity に関する記事です。
この記事は、
https://docs.unity3d.com/Packages/com.unity.addressables@1.1/manual/AddressableAssetsDevelopmentCycle.html
に書かれている内容を日本語に翻訳したものです。

※この記事では、Addressable Asset System をAASと省略して書きます。

“【Unity】Addressable Asset System 公式マニュアルの和訳 Part4” の続きを読む