こんにちは、14日目に滑り込みで記事提出させていただきます。
アドベントカレンダーは今年が初めてではないですが、毎年「何を書こうか」と悩みあぐねている気がするこの頃。
今年はネタを事前に二本、予備含めて用意していたにも関わらず、検証段階でうまくいかないことが発覚して、敢え無く私の試みは無に帰しました。
その失敗記録を投稿することも後日考えたいですが、アドベントカレンダーではもう少し実りのある話題を提供したい。
ということで、この記事ではタイトル通り、直近コツコツと積み上げてきた「汎用実装」集を書き連ねてみようと思います。
1. MinimalUtilityについて
MinimalUtility、と題したこちらのリポジトリを作ったのは、gitのログ見る限り今年の3月末のことらしいです。
こちら前身となるリポジトリがあって、そちらの開発期間も含めるとおおよそ1年弱だろうと見込んでいるのでいます。
適当にバージョンを刻みつつ、長く開発できているなあ、と振り返って思います。
で、これは何のリポジトリかというといわゆる「ユーティリティ」、すなわち「汎用実装」のコレクションです。
よくある拡張メソッドなどをまとめたもの、と思っていただければわかりやすいかと思います。
どこかのリポジトリで勉強になった実装や思いついた実装で、汎用化できるものをコレクションしている感じです。
なんでそんなものを作っているのかというと...完全に趣味ですね。
そういうミニマムな実装を集めるのが楽しいんですよね、個人的に。
なので、こちら「ライブラリ」というには根幹となるスゴイ思想があるわけじゃありません。
今回はこの中から、ちょっと自信があったり気に入ったりしている実装をいくつかご紹介できればと思います。
皆さんの環境に持ち込める実装があったら幸いです。
2. インスペクター拡張
2-1. 既存コンポーネント拡張
紹介しやすいので、トップバッターでエディタ拡張、特にインスペクター拡張をいくつか紹介させていただきます。
Transform
・Animator
・Mesh Renderer
、頻出コンポーネントの拡張がまず一つ。
Transform
の拡張でワールド座標を表示する、というアイデアは珍しくないと思いますが、雑に実装すると Enable constrained proportions
というScaleを比率固定できる標準機能を消してしまったりします。
これ、いつからついていたのか覚えていませんが、まああれば使うかも、くらいの便利さです。
今後も既存コンポーネントのインスペクター拡張が公式で何かしら拡充される可能性はあるので、それを反映できる拡張にするのにこだわっています。
2-2. Buttonattribute
また、これは特定のコンポーネントのインスペクター拡張ってわけではありませんが、特定の属性をつけておいたメソッドを実行させられるButtonAttribute
の実装もしています。
Obinとか有名なエディタ拡張アセットでみるやつですね。
using UnityEngine;
public class Test : MonoBehaviour
{
[MinimalUtility.Button]
private void TestMethod()
{
Debug.Log("TestMethod");
}
}
「これ絶対必要?」といわれると、そんなことはない感。
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
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. まとめ
ということで、自分のお気に入り実装をいくつかひけらかしてみました。
需要はないだろうなと思っています、くっ、あのネタが失敗しなければ今頃は...。
今年は厄年なので最後までこんな調子なのかなと戦々恐々しておりますが、皆さんもどうかご自愛くださいませ。