10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1年弱かけて構築した汎用実装を眺める

Last updated at Posted at 2024-12-13

こんにちは、14日目に滑り込みで記事提出させていただきます。

アドベントカレンダーは今年が初めてではないですが、毎年「何を書こうか」と悩みあぐねている気がするこの頃。

今年はネタを事前に二本、予備含めて用意していたにも関わらず、検証段階でうまくいかないことが発覚して、敢え無く私の試みは無に帰しました。

その失敗記録を投稿することも後日考えたいですが、アドベントカレンダーではもう少し実りのある話題を提供したい。

ということで、この記事ではタイトル通り、直近コツコツと積み上げてきた「汎用実装」集を書き連ねてみようと思います。

1. MinimalUtilityについて

MinimalUtility、と題したこちらのリポジトリを作ったのは、gitのログ見る限り今年の3月末のことらしいです。

こちら前身となるリポジトリがあって、そちらの開発期間も含めるとおおよそ1年弱だろうと見込んでいるのでいます。

適当にバージョンを刻みつつ、長く開発できているなあ、と振り返って思います。

で、これは何のリポジトリかというといわゆる「ユーティリティ」、すなわち「汎用実装」のコレクションです。

よくある拡張メソッドなどをまとめたもの、と思っていただければわかりやすいかと思います。

どこかのリポジトリで勉強になった実装や思いついた実装で、汎用化できるものをコレクションしている感じです。

なんでそんなものを作っているのかというと...完全に趣味ですね。

そういうミニマムな実装を集めるのが楽しいんですよね、個人的に。

なので、こちら「ライブラリ」というには根幹となるスゴイ思想があるわけじゃありません。

今回はこの中から、ちょっと自信があったり気に入ったりしている実装をいくつかご紹介できればと思います。

皆さんの環境に持ち込める実装があったら幸いです。

2. インスペクター拡張

2-1. 既存コンポーネント拡張

紹介しやすいので、トップバッターでエディタ拡張、特にインスペクター拡張をいくつか紹介させていただきます。

TransformAnimatorMesh Renderer、頻出コンポーネントの拡張がまず一つ。

Pasted image 20241213233614.png
Pasted image 20241213233620.png
Pasted image 20241213233617.png

Transformの拡張でワールド座標を表示する、というアイデアは珍しくないと思いますが、雑に実装すると Enable constrained proportionsというScaleを比率固定できる標準機能を消してしまったりします。

これ、いつからついていたのか覚えていませんが、まああれば使うかも、くらいの便利さです。

今後も既存コンポーネントのインスペクター拡張が公式で何かしら拡充される可能性はあるので、それを反映できる拡張にするのにこだわっています。

Pasted image 20241213233627.png

2-2. Buttonattribute

また、これは特定のコンポーネントのインスペクター拡張ってわけではありませんが、特定の属性をつけておいたメソッドを実行させられるButtonAttributeの実装もしています。
Obinとか有名なエディタ拡張アセットでみるやつですね。

Test.cs
using UnityEngine;

public class Test : MonoBehaviour  
{  
    [MinimalUtility.Button]
    private void TestMethod()  
    {        
	    Debug.Log("TestMethod");  
    }
}

Pasted image 20241213234230.png

「これ絶対必要?」といわれると、そんなことはない感。
UnityEditor.MenuItem使うと、Unityエディタ上から何か特定のメソッドを呼び出せる、みたいな機能ってそんなに難しい気はしません。

ただ、チーム開発を意識するなら、メニューとして閉じてあるUnityEditor.MenuItemよりも視認性高いこちらのほうがいいんじゃないかと個人的に気に入っています。

3. バックポート

自分が所属するコミュニティでちょっと議論があったというか、耳寄りな情報を知りました。

Unityの環境はC#9.0とかですが、最近のUnityのLTSはC#10であったりC#11であったりが動くコンパイラーらしく、ちょこっといじるだけで言語バージョンだけはC#11にあげられるという手法が発見されています。

「言語バージョンだけは」ですが。別に.NETはあがっておりませんが

それでこの話につながるんですけれども、コンパイラー的に対応しているのであれば本来C#10 & .NET6のDefaultInterpolatedStringHandlerとかも定義だけバックポートで使える模様です。

そう、C#のバージョンをあげていなくても!

C#バージョンはあがっていないのに、定義入れて補間文字列機能が強化される...。

きもいと思いつつ使っていた補間文字列を、今では楽しく使えるようになりました(狂)。

その他、record型を使えるようになるIsExternalInitクラスのバックポートなど、知られているやつも利用しています。

まあこれは別に、自分で実装したって胸張って言えるものでもないのであれですが、興味があれば皆さんも導入検討してみてください。

4. できるだけValueにしたい

前節で紹介したDefaultInterpolatedStringHandlerのおかげで、いわゆるValueな(つまり構造体の)StringBuilderが導入できました。やったね。

(それまではZStringを使って同様のことを果たしていましたが、やはり言語機能として記述面にサポートが効く方が個人的に好きですね。)

さて、StringBuilderをValueにしたんだったら、じゃあ他のものも色々Valueにしていこうぜ、となります。

この「Valueにしていこうぜ」は.NETのトレンドでもあり、有名な奴だと「Task->ValueTask」、「Tuple->ValueTuple」などなどがあります。

Unity界隈において、もはやデフォルトスタンダードなUniTask(今年は自分もいくつかコミットさせていただきました)があるので、非同期処理のValue化は最初からできているといって過言ではない。

なんかほかにあるかな、と思って取り組んでいたのがこのへんです。

4-1. ValueDisposable

ValueDisposable.cs
public static class ValueDisposable
{
	public static Disposable<T> Create<T>(T state1, Action<T> onDispose) 
         => new (state1, onDispose);

     public readonly struct Disposable<T> : IDisposable
    {
    	private readonly T state1;
    	private readonly Action<T> onDispose;
    	
    	internal Disposable(T state1, Action<T> onDispose)
    	{
    		this.state1 = state1;
    		this.onDispose = onDispose;
    	}
    
    	void IDisposable.Dispose()
    	{
    		onDispose?.Invoke(state1);
    	}
    }
}

こんな感じの実装を使って、主にusingスコープやusingステートメントにて使うイメージです。

using (ValueDisposable.Create("Hello", static str => Debug.Log(str)))
{
	// 何かしらの処理
}

構造体にインターフェイス実装する場合、そのインターフェイスの型の変数に構造体を突っ込もうものならボクシングアロケーションしちゃうわけですが、usingなら大丈夫らしいです。C#魔法。

IDisposable型にしたい場合は、Rx系のDisposable.Create系の実装を使うべきですが、簡易的に使いたい場合はこちらの方がよきですね。

4-2. ValueTupleを利用したコレクションもどき

同様にC#魔法を一部使ったものとして、ValueTupleのforeach対応があります。

ちょっとした実装をしておくと(具体的にはGetEnumeratorメソッドを拡張メソッドで生やしておくと)以下のような記述ができるようになります。

// before
gameObject.SetActive(false);
gameObject.layer = 1 << 2;

var hoge = GameObject.Find("Hoge");
hoge.SetActive(false);
hoge.layer = 1 << 2;

var fuga = GameObject.Find("Fuga");
fuga.SetActive(false);
fuga.layer = 1 << 2;

// after
foreach (var obj in (gameObject, GameObject.Find("Hoge"), GameObject.Find("Fuga")))
{
	obj.SetActive(false);
	obj.layer = 1 << 2;
}

この手の実装はメソッド化・ローカルメソッド化によって実装をまとめるのがよくある手法ですが、メソッド化って記述場所離れちゃうと可読性下げますし、ちょっと手間でもあります。

ぱっとforeachで回せたら便利だよな、ってことで実装してみました。

こうしてforeach書けるようになると、ValueTupleが一気にコレクションもどきに見えてきます。

ValueTuple自体構造体ですし、GetEnumeratorメソッドも気を遣ってyield return使わず記述して、パフォーマンス悪くならないように頑張っております。

これは直近ではちょっとお気に入りの実装です。

5. SourceGenerator製System.Enumもどき

これはちょっと内部実装微妙かもしれない疑惑があるんですが...System.EnumもどきをSourceGeneratorで作っています。

System.EnumはAPIこそ頻出ケースを満たせる良いものですが、内部的に引数でアロケーション出す構造だったり何だったりで、あんまりよろしくありません。

ぶっちゃけ、ジェネリック系のAPIが少なくて...まあ歴史的経緯を考えれば仕方ないんですが。

これに対する対抗策として、C#界隈では有名なFastEnumがあったりしますが、私はUnityで動かせてジェネリックで、みたいなものを自作して使っています。

API一覧としてはこんな感じ。

  • public static T[] GetValues<T>() where T : struct, Enum
  • public static ReadOnlySpan<T> GetValues<T>(in Span<T> span) where T : struct, Enum
  • public static int GetLength<T>() where T : struct, Enum
  • public static string[] GetNames<T>() where T : struct, Enum
  • public static string GetName<T>(in T value) where T : struct, Enum
  • public static bool IsDefined<T, TValue>(in TValue value) where T : struct, Enum where TValue
  • public static T Parse<T>(in string value) where T : struct, Enum
  • public static bool TryParse<T>(in string value, out T result) where T : struct, Enum
  • public static string ToXEnumString<T>(this T value) where T : struct, Enum
  • public static string GetEnumMemberValue<T>(this T value) where T : struct, Enum
  • public static bool HasBitFlag<T>(this T value, in T flag) where T : struct, Enum
  • public static bool ConstructFlags<T>(this T value) where T : struct, Enum

結構あるし、一部System.Enumにはないオリジナルも混じっています。

これらは使用感としてはSystem.Enumと同じ感覚で使えます。

専用の属性をつけるのがSourceGeneraotr系だと定番ですが、我がXEnumはシンタックスツリーを利用して、それが参照されている際に適切な実装を生やす構造になっています。

専用属性付与とかの工程がないため、標準提供されている既存の列挙体対象でも苦も無く使えます。

まあこれ、実装としては重いかもしれないんですが、ほんと自己満足なところが大いにありけり。

個人的には、上記の二つ目のGetValuesがAPIとして気に入っています。

var values = XEnum.GetValues(stackalloc DateTimeKind[XEnum.GetLength<DateTimeKind>()]);  
foreach (var v in values))  
{  
    Debug.Log(v);  
}

はい、配列なし!

配列という参照型を駆逐して、というドクトリンも今年に入って定着しだした手法ですね、自分の中では。

この「引数でSpan指定してReadOnlySpanで返す」APIは自分の好みで、ほかの拡張メソッド等でも活用しています。

6. まとめ

ということで、自分のお気に入り実装をいくつかひけらかしてみました。

需要はないだろうなと思っています、くっ、あのネタが失敗しなければ今頃は...。

今年は厄年なので最後までこんな調子なのかなと戦々恐々しておりますが、皆さんもどうかご自愛くださいませ。

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?