ZLinqは、標準LINQに近い構文で、イテレータ生成によるヒープ割り当てを抑えるCysharp製のC#ライブラリです。コレクションに AsValueEnumerable() を1つ足すと、そこから先の Where や Select を重ねても、イテレータ生成による割り当ては増えません。ただし、ラムダのキャプチャやラムダ内の処理による割り当ては別です。ゲーム向けと紹介されることが多いものの、NuGetパッケージは netstandard2.0/2.1・net8.0/9.0/10.0 向けに配布されており、通常の.NETアプリでもUnity・Godotでも使えます。最新版は v1.5.6(2026年4月8日、MITライセンス)です。この記事では導入手順に加え、.NET 10で標準LINQと並べて測った割り当て量と速度、既存コードを移行するときに出るコンパイルエラー、採用を見送るべき場面までをまとめます。
まとめ
- 導入は1行。
dotnet add package ZLinqのうえAsValueEnumerable()を挟む。既存コードを書き換えたくなければZLinq.DropInGeneratorがLINQ呼び出しを自動で差し替える。 - .NET 10でも差は残る。
Where→Select→Takeの列挙は標準LINQが1回160B(配列)〜208B(List)を割り当て、ZLinqは0B。OrderByDescending→Take(10)は12,344Bが224Bになった(いずれも筆者の実測値)。 - 速さは場面次第。
Where→Select→Takeは筆者の計測で2.1〜2.4倍速くなった。一方でオーバーフロー検査付きのSumは標準LINQより遅く、速くなったのはSumUncheckedだけだった。 - Unityは2段階で入れる。NuGetForUnityで本体、UPMのgit URLで
ZLinq.Unity。UnityではZLinqのSIMD最適化は使われず、主な狙いは割り当てと列挙時の呼び出し負荷の削減。 - .NET 9以降は
ref struct。IEnumerable<T>を返す・受け取るコードや、foreachの中のyield/awaitはコンパイルエラーになる。全面置換ではなく、プロファイラで見つけたホットパスから入れる。
ZLinqの仕組み:構造体の列挙子による割り当て削減
標準LINQで割り当てが起きる場所
標準LINQの Where や Select は、呼ぶたびにイテレータのクラスインスタンスを作ります。生成されるオブジェクトの数は演算子や入力型によって異なります。また、標準LINQのイテレータは同一スレッドでの初回列挙では自身を列挙子として返すため、foreach で必ず別の列挙子が割り当てられるわけではありません。1回なら百数十バイトですが、毎フレームのUpdateや毎リクエストのハンドラで回るとGCの回数が増え、フレーム落ちやレイテンシのばらつきにつながります。Unityで「Update内でLINQを使うな」と言われてきたのはこのためです。LINQの文法と遅延評価そのものはLINQの読み方は「リンク」|C#・.NETの統合言語クエリを基礎から解説で、値型をobjectに渡したときに起きる割り当てはC#のボックス化とは?起きる場面と割り当てバイト数を.NET 10で実測・回避方法まで解説で扱っています。
ValueEnumerableとTryGetNextの設計
ZLinqは列挙子を構造体にし、チェーンの各段を型パラメーターとして積み上げます。中核の型は次の2つです(公式リポジトリのREADMEより)。
public readonly ref struct ValueEnumerable<TEnumerator, T>(TEnumerator enumerator)
where TEnumerator : struct, IValueEnumerator<T>, allows ref struct
{
public readonly TEnumerator Enumerator = enumerator;
}
public interface IValueEnumerator<T> : IDisposable
{
bool TryGetNext(out T current); // MoveNext と Current を1回にまとめたもの
// 最適化用
bool TryGetNonEnumeratedCount(out int count);
bool TryGetSpan(out ReadOnlySpan<T> span);
bool TryCopyTo(scoped Span<T> destination, Index offset);
}
構造体の列挙子を値として保持することで列挙子ごとのクラスインスタンス生成を避けられ、MoveNext と Current の2回の呼び出しが TryGetNext の1回に減ります。元が配列や List<T> なら TryGetSpan でSpanを取り出し、ToArray・Count・Sum などで、条件に応じて一括コピー、既知の要素数、SIMDによる集計を利用できます。ただし、途中の演算子によってSpanを取得できない場合や、要素単位のループが必要な場合もあります。作者の河合宜文氏はリリース時の解説記事で、この設計をアーキテクチャ図つきで説明しています。
対応環境と.NET 10相当の演算子
NuGetの ZLinq 1.5.6 は netstandard2.0/netstandard2.1/net8.0/net9.0/net10.0 の5種類のビルドを含みます(net10.0 版は v1.5.4 から)。READMEは「.NET 10のLINQと99%互換」とし、dotnet/runtime の System.Linq.Tests をZLinqに差し替えて約9,000件のテストを通したと書いています。Shuffle・LeftJoin・RightJoin・Sequence・InfiniteSequence といった.NET 10の新しい演算子も、netstandard2.0(.NET Framework 4.8)やUnityで使えます。ランタイムを上げられない業務システムに新しい演算子だけを持ち込める点は、ゲーム以外でも使い道があります。.NET 10本体の変更点は.NET 10(LTS)とは?SDKの選び方と入手・サポート期限・C# 14と移行を解説にまとめています。
.NETプロジェクトへのZLinq導入手順
AsValueEnumerable()による明示的な置き換え
パッケージを追加し、using ZLinq; を書いてチェーンの先頭に AsValueEnumerable() を置きます。
dotnet add package ZLinq
using ZLinq;
var source = new int[] { 1, 2, 3, 4, 5 };
// ここから先が ZLinq になる
var evens = source.AsValueEnumerable().Where(x => x % 2 == 0);
foreach (var x in evens) Console.WriteLine(x);
// .NET 9 / C# 13 以降は Span もそのままソースにできる
Span<int> span = stackalloc int[] { 1, 2, 3, 4, 5 };
var total = span.AsValueEnumerable().Select(x => x * x).Sum();
割り当てなしで変換できるのは、T[]・List<T>・ArraySegment<T>・Memory<T>・ReadOnlyMemory<T>・ReadOnlySequence<T>・Dictionary<TKey, TValue>・Queue<T>・Stack<T>・LinkedList<T>・HashSet<T>・ImmutableArray<T>・Span<T>・ReadOnlySpan<T> です。ただし、ImmutableArray<T> からの専用変換は.NET 8以上、Span<T>/ReadOnlySpan<T> からの変換は.NET 9以上が必要です。変数が IEnumerable<T> 型でも、実体が配列か List<T> なら割り当てなしで処理されます。それ以外の独自コレクションは列挙子の取得で割り当てが起きます。
ZLinq.DropInGeneratorによる既存LINQ呼び出しの差し替え
既存のコード全体に AsValueEnumerable() を足して回れない場合は、Source Generatorの ZLinq.DropInGenerator を使います。アセンブリ属性で対象の型を指定すると、同名・同引数の拡張メソッドが生成され、そちらが優先して選ばれます。
dotnet add package ZLinq.DropInGenerator
using ZLinq;
// 第1引数は生成先の名前空間、第2引数は対象の型
[assembly: ZLinqDropInAttribute("MyApp", DropInGenerateTypes.Collection)]
| DropInGenerateTypes | 対象 |
|---|---|
| Array | T[] |
| Span | Span / ReadOnlySpan |
| Memory | Memory / ReadOnlyMemory |
| List | List<T> |
| Enumerable | IEnumerable<T> |
| Collection | Array + Span + Memory + List |
| Everything | Collection + Enumerable |
READMEは、慣れないうちは Everything ではなく Collection から始めるよう勧めています。Enumerable を含める場合、生成先を空文字(グローバル名前空間)にすると、対象の拡張メソッド呼び出しではZLinqが優先されます。ただし、System.Linq.Enumerable.Where(source, predicate) のように静的メソッドとして明示すれば標準LINQも呼べます。アプリの既定名前空間(例では MyApp)を指定しておけば、その配下だけがZLinqになり、名前空間の外では標準LINQのままです。
UnityへのZLinq導入手順
NuGetForUnityとUPMによる2段階のインストール
- NuGetForUnity を入れ、Window → NuGet → Manage NuGet Packages で「ZLinq」をインストールする。
- Package Manager の「Add package from git URL」に次を入力し、
ZLinq.Unityを入れる。
https://github.com/Cysharp/ZLinq.git?path=src/ZLinq.Unity/Assets/ZLinq.Unity
1だけでも演算子は使えますが、GameObject/Transformを辿る機能は2のパッケージに入っています。ZLinq.Unity の package.json は対応バージョンを 2021.3 としています。ZLinq.DropInGenerator も使う場合は、Incremental Source Generator(Compiler Version 4.3.0)が要るため最低 2022.3.12f1 に上がります。2021.3系で「DropInを入れたのにZLinqにならない」ときは、まずこの要件を確認してください。
LINQ to GameObjectによるTransform階層の探索
using UnityEngine;
using ZLinq;
public class SampleScript : MonoBehaviour
{
public Transform Origin;
void Start()
{
foreach (var t in Origin.Ancestors()) Debug.Log(t.name);
foreach (var t in Origin.Children()) Debug.Log(t.name);
foreach (var t in Origin.Descendants()) Debug.Log(t.name);
// FooScript は利用側で定義した MonoBehaviour を想定
// 自分と直接の子から FooScript を持つものだけを取る
var foos = Origin.ChildrenAndSelf().OfComponent<FooScript>();
}
}
軸は Ancestors・Children・Descendants・BeforeSelf・AfterSelf の5つで、結果にそのままLINQを重ねられます。UI ToolkitのVisualElementも同じ軸で辿れるため、rootVisualElement.Descendants().OfType<Button>() のような検索も書けます。Unity Collectionsの NativeArray・NativeSlice は AsEnumerable() 経由でクエリでき、com.unity.collections 2.1.1以上なら NativeQueue・NativeHashSet・FixedList32Bytes などにも AsValueEnumerable() を呼べます。同じCysharp製でUnityと組み合わせることの多いRxライブラリについてはUnity R3の使い方|導入手順とオペレーター・ファクトリーメソッド早見表【R3 1.3.1】にまとめています。
Unity版の制約:SIMD非対応とIEnumerableへの変換条件
READMEには「Unityは.NET Standard 2.1を参照するためSIMDは使われない」と明記されています。Unityで得られるのは割り当てとイテレータ呼び出しの削減で、後述のSIMDによる集計の高速化は対象外です。
一方、.NET 9以降の ValueEnumerable は ref struct ですが、Unityでは通常の struct です。READMEにある拡張メソッドを1つ用意すれば IEnumerable<T> へ変換でき、IEnumerable<T> 前提の既存APIへ渡す逃げ道があります。ただし、通常の struct であることが下の String.Join の事故の原因にもなります。
.NET 10での標準LINQとZLinqの実測比較
READMEのベンチマークは公式の計測環境の値で、.NET 10の標準LINQ自体の改善がどこまで差を縮めたかは分かりません。そこで筆者の手元で、同じ処理を標準LINQとZLinqで書いて測りました。環境は Intel Core i9-9880H(AVX2)の macOS、.NET SDK 10.0.401(ランタイム10.0.12)、ZLinq 1.5.6、BenchmarkDotNet 0.15.8 です。他の処理が動いている開発機での計測なので、速度の絶対値ではなく、同じ表の中での比を見てください。
割り当ての実測:チェーン列挙の0B化とToArrayに残る配列確保
各処理を200回のウォームアップ後に1,000回実行し、GC.GetAllocatedBytesForCurrentThread() の差分を回数で割った値です。元データは要素数1,000の int[] と List<int> です。
| 処理 | 標準LINQ | ZLinq |
|---|---|---|
| Where→Select→Take(10) をforeach(int[]) | 160 B | 0 B |
| 同(List<int>) | 208 B | 0 B |
| IEnumerable<int>型のListにWhere→Count | 72 B | 0 B |
| OrderByDescending→Take(10)→ToArray | 12,344 B | 224 B |
| Where→Select→ToArray(500件) | 2,128 B | 2,024 B |
| 同を ToArrayPool で受ける | – | 0 B |
| Sum(int[]) | 0 B | 0 B |
差が出るのは、途中の段を列挙するだけで結果を新しい配列にしない処理です。ToArray は結果の配列そのものが2,000B前後あるので、ZLinqにしてもほとんど減りません。結果を一時的に使う今回のウォームアップ後の計測では、ToArrayPool() で受けて使用後に返却すると0Bでした。ただし、プールに再利用できる配列がない場合などは新たな割り当てが発生します。並べ替えを含む OrderByDescending→Take も、12,344Bが224Bまで減りました。
速度の実測:チェーン列挙の高速化とSumの検査コスト
BenchmarkDotNetの既定ジョブ(DefaultJob)で2回測った平均値で、「1回目/2回目」の順に並べています。Sumは要素数16,384、チェーンとOrderByDescendingは1,000、最後の行は5要素の配列に Select を10段つないで Sum した値です。
| 処理 | 標準LINQ | ZLinq | 割り当て(標準→ZLinq) |
|---|---|---|---|
| Sum(16,384件) | 2,536/2,593 ns | 3,882/3,635 ns | 0 B → 0 B |
| SumUnchecked(同) | – | 1,288/1,224 ns | – → 0 B |
| foreachで手書き加算(同) | 10,086/9,489 ns | – | 0 B |
| Where→Select→Take(100) | 1,615/1,259 ns | 767/517 ns | 160 B → 0 B |
| OrderByDescending→Take(10)→Sum | 11,434/18,942 ns | 22,488/20,336 ns | 12,280 B → 160 B |
| 5要素にSelectを10段→Sum | 863/818 ns | 47/68 ns | 1,344 B → 0 B |
計測したコードは次の形です(抜粋)。同じメソッドを標準LINQ版とZLinq版で並べ、[MemoryDiagnoser] で割り当ても同時に取っています。
[MemoryDiagnoser]
public class B
{
int[] big = Enumerable.Range(0, 16384).ToArray();
int[] k1 = Enumerable.Range(0, 1000).ToArray();
[Benchmark] public int Sum_Linq() => big.Sum();
[Benchmark] public int Sum_ZLinq() => big.AsValueEnumerable().Sum();
[Benchmark] public int Sum_ZLinqUnchecked() => big.AsValueEnumerable().SumUnchecked();
[Benchmark] public int Chain_Linq()
{
int s = 0;
foreach (var x in k1.Where(x => x % 2 == 0).Select(x => x * 2).Take(100)) s += x;
return s;
}
[Benchmark] public int Chain_ZLinq()
{
int s = 0;
foreach (var x in k1.AsValueEnumerable().Where(x => x % 2 == 0).Select(x => x * 2).Take(100)) s += x;
return s;
}
}
結果は「ZLinqなら全部速い」ではありませんでした。チェーンの列挙は2.1〜2.4倍速く、割り当ても0Bです。一方、オーバーフロー検査付きの Sum は2回とも標準LINQのほうが速く、ZLinqで速くなったのは検査を外した SumUnchecked だけでした(標準LINQ比で約2倍)。READMEの掲載値では Sum 同士はほぼ同じ速さなので、CPUや環境によって逆転しうると見てください。OrderByDescending→Take は回ごとのばらつきが大きく(1回目のZLinqは標準偏差4.7μs)、速度の優劣は付けられません。確実に言えるのは割り当てが12,280Bから160Bに減ったことで、速度を目的にOrderByを置き換える根拠にはなりません。5要素に10段の Select をつないだケースはREADMEが「標準LINQより遅くなりうる」と書く条件に近いものの、この計測ではZLinqが12〜18倍速く、標準LINQは10段ぶんのイテレータで1,344Bを割り当てていました。遅くなる境界は処理内容で変わるので、自分のコードで測るのが確実です。
ラムダのキャプチャに伴うクロージャの割り当て
ZLinqが消すのはイテレータと列挙子の割り当てで、ラムダ式のクロージャは対象外です。引数 t をキャプチャする Where(x => x > t).Count() を測ると、ZLinqでも1回24Bが残りました。キャプチャのない Where(x => x > 500) は0Bです。
| Where→Count | 標準LINQ | ZLinq | ZLinq(スタック割り当て無効) |
|---|---|---|---|
| 引数tをキャプチャ | 約137 B | 24 B | 88 B |
| キャプチャなし | 48 B | 0 B | 0 B |
24Bはキャプチャした変数を入れるクロージャのオブジェクトです。.NET 10のJITは、メソッドの外へ出ないデリゲートをスタックに置けるため、デリゲート本体の64Bは消えています。環境変数 DOTNET_JitObjectStackAllocation=0 でこの最適化を切ると88B(24B+64B)になりました。UnityのMonoやIL2CPPはこの最適化の対象外なので、88Bに近い値になると考えてください。毎フレーム呼ぶ箇所では、閾値を定数か static readonly フィールドにしてキャプチャをなくすと、ZLinqの0Bを活かせます。ラムダに static 修飾子を付けておけば、うっかりキャプチャした時点でコンパイルエラーになります。
既存コードをZLinqへ移行するときのコンパイルエラー
既存のメソッドに AsValueEnumerable() を足すと、次のようなコードがコンパイルエラーになります。net10.0・ZLinq 1.5.6でビルドしたときのエラーコードです。
| 書き方 | net10.0のエラー | 回避策 |
|---|---|---|
| foreach内でyield return | CS4007 | ToArrayPoolで確定させる |
| foreach内でawait | CS4007 | ToArrayPoolで確定させる |
| IEnumerable<T>を返す | CS0029 | 戻り値をT[]か配列プールにする |
| IEnumerable<T>引数へ渡す | CS1503 | usingで借用し、AsEnumerable()で渡す |
| 同じ変数へConcatを再代入 | CS0029 | ループで連結しない設計にする |
| string.Joinへ直接渡す | CS1503 | JoinToString(“,”) |
// CS4007 を避ける書き方(net10.0 でビルドを確認)
public static IEnumerable<int> Evens(int[] a)
{
using var pooled = a.AsValueEnumerable().Where(x => x % 2 == 0).ToArrayPool();
foreach (var x in pooled.AsEnumerable()) yield return x;
}
CS4007は「’ValueEnumerator<ArrayWhere<int>, int>’ 型のインスタンスは、’await’ または ‘yield’ 境界を越えて保持することはできません」というメッセージで、ref struct の列挙子を非同期メソッドやイテレーターの状態に保存できないことによります。ToArrayPool() はArrayPoolから借りた配列に結果を詰めるメソッドで、using で返却します。スコープの外へ持ち出したり、フィールドに保持したりしてはいけません。チェーンの段ごとに型が変わるので、q = q.Concat(...) をループで積み上げる書き方もできません。
String.Joinのターゲットフレームワーク別の挙動
READMEは「String.Join に渡すと params object[] のオーバーロードが選ばれ、意図した結果にならない」と注意しています。手元で確かめると、症状はターゲットフレームワークで分かれました。net10.0では ValueEnumerable が ref struct(IsByRefLike=True)なのでobjectに変換できず、上表のCS1503で止まります。net8.0向けにビルドすると IsByRefLike=False になってコンパイルが通り、実行結果は 1,2,3 ではなく次の型名でした。
string.Join = ZLinq.ValueEnumerable`2[ZLinq.Linq.ArraySelect`2[System.Int32,System.String],System.String]
JoinToString = 1,2,3
net8.0・netstandard2.0/2.1・Unityでは、ログ出力やCSVの組み立てで型名が黙って出力されます。ZLinqを入れたプロジェクトでは、string.Join の呼び出し箇所を検索し、引数にZLinqの ValueEnumerable を直接渡している箇所を JoinToString に置き換えます。
doubleのSum・Averageにおける丸め誤差の違い
.NET 8以上では、doubleの Sum・Average がSIMDで並列に加算されるため、加算の順序が変わって丸め誤差が変わります。0〜1000の乱数10万個(シード42)で比べると、標準LINQは 49992064.72134093、ZLinqは 49992064.72134083 で、差は約1.04×10-7でした。集計結果を既存の値とビット単位で照合するリグレッションテストや、金額の合計を扱う処理には入れないでください(金額は本来 decimal で持つべきでもあります)。
LINQ to SIMDが自動で効く演算子
.NET 8以上で、元のソースからSpanが取れる(TryGetSpan が true)とき、次の演算子にSIMDが自動で適用されます。Where や Select のようにデリゲートを受け取る演算子は自動適用の対象外です。
Sum/SumUnchecked/Average:sbyte・short・int・long・byte・ushort・uint・ulong・doubleMax/Min:整数型(Int128・UInt128・nint・nuint を含む)Contains/SequenceEqual:byte・sbyte・short・ushort・int・uint・long・ulong・nint・nuint・bool・charRange・RepeatからのToArray/ToList/CopyTo(Repeatはサイズが2のべき乗の unmanaged struct に限る)
Sum はオーバーフロー検査付き(checked)で、検査がSIMDの足を引っ張ります。値域が保証されているなら SumUnchecked を選びます。デリゲートを伴う処理をSIMDで回したいときは、using ZLinq.Simd; を書いて配列やSpanの VectorizedUpdate に直接ベクトル用の Func<Vector<T>, Vector<T>> と余り要素用の Func<T, T> を渡します。配列やSpanに AsVectorizable() を呼ぶと、Aggregate・All・Any・Count・Select・Zip もベクトル用と要素用の2つのラムダで書けます。Vector.IsHardwareAccelerated が false の環境では通常のループで処理されます。
LINQ to Treeによるファイル・JSON・Godotの階層探索
GameObjectと同じ Ancestors/Children/Descendants の軸は、ファイルシステムとJSONにも用意されています。パッケージは ZLinq.FileSystem と ZLinq.Json(どちらも1.5.6)です。次のコードは、ディレクトリ以下の .cs ファイル名と、JSON内で enabled が true のパスを取り出します。手元の実行結果は one.cs, two.cs と $.a.enabled, $.a.b.c.enabled でした。
using System.IO;
using System.Text.Json.Nodes;
using ZLinq;
var csFiles = new DirectoryInfo("data").Descendants()
.OfType<FileInfo>()
.Where(x => x.Extension == ".cs")
.Select(x => x.Name)
.JoinToString(", ");
var json = JsonNode.Parse("""{"a":{"enabled":true,"b":{"enabled":false,"c":{"enabled":true}}}}""")!;
var paths = json.Descendants()
.Where(x => x.Name == "enabled" && x.Node?.GetValue<bool>() == true)
.Select(x => x.Node!.GetPath())
.JoinToString(", ");
対象は System.Text.Json の JsonNode で、JsonDocument/JsonElement には使えません。Godotは dotnet add package ZLinq.Godot で入り、Godot 4.0.0以上で Node を同じ軸で辿れます。型の絞り込みは OfType<T>() です。
ZLinqを採用すべきでない場面
ZLinqは、割り当てが積み上がるホットパスや、SIMD・列挙処理の最適化が効く箇所で効果を期待できます。次のどれかに当たるなら、標準LINQのままにするか、入れる範囲を絞ってください。
- 割り当てが問題になっていない:起動時に1回だけ走る処理、管理画面のクエリ、I/O待ちが支配的なAPIでは、1回あたり数百バイトを削っても体感は変わりません。プロファイラでGCの割り当てが出ていない箇所に入れると、移行のコンパイルエラーと可読性のコストだけが残ります。
- 非同期パイプラインやイテレーターの途中:.NET 9以降は
foreachの中でawait/yieldを書けません。ToArrayPool()で確定させれば使えますが、そのぶん配列プールの寿命管理が増えます。 - 公開APIの引数・戻り値:ライブラリの公開メソッドで
IEnumerable<T>を受け渡している箇所は、ZLinqの型に置き換えると利用側に制約が波及します。ZLinqはメソッド内部だけで使い、境界では配列かIEnumerable<T>に戻します。 - 少数要素・長いチェーンで、実測でもコピーコストが支配的な処理:
ValueEnumerableは段を重ねるほど構造体が大きくなり、コピーのコストが増えます。READMEは、極端に長いチェーンと少ない反復では標準LINQより遅くなりうるとしつつ、実務上はまれとしています。筆者の計測(5要素に10段のSelect)ではZLinqが12〜18倍速かったので、要素数と段数だけで判断せず、測ってZLinqのほうが遅いと確かめた処理だけを標準LINQに戻してください。 - doubleの集計結果を既存値と一致させる必要がある:上で示したとおり、.NET 8以上では末尾の桁が変わります。
導入の順序は、プロファイラ(UnityならProfilerのGC Alloc列、.NETならVisual Studioの「.NETオブジェクトの割り当て追跡」など、割り当て元の呼び出し履歴を採取できるメモリプロファイラ)で割り当ての多い関数を特定し、その関数だけに AsValueEnumerable() を足して測り直す、です。変更前後の割り当て量の比較には、BenchmarkDotNetの [MemoryDiagnoser] が使えます。DropInGeneratorを使う場合も Collection を既定名前空間に入れるところから始め、Everything をグローバル名前空間に入れる構成は避けます。対象の拡張メソッド呼び出しが広範囲に置き換わり、制約に当たる箇所では標準LINQの明示的な静的呼び出しなど、個別の回避策が必要になるからです。計測の手順や、ウォームアップ不足で数値が狂う落とし穴はマイクロベンチマークとは?正しい測り方と落とし穴をJMH・Goの実測で解説で解説しています。Unityのゲームで毎フレームの割り当てを削る作業を外部に任せたい場合は、Unityでのゲームアプリ開発の支援も行っています。
よくある質問
ZLinqは標準LINQより常に速いですか?
いいえ。チェーンの割り当てはほぼ確実に減りますが、速度は演算子と要素数で変わります。筆者が.NET 10で測った範囲では、Where→Select→Take は2.1〜2.4倍速くなりました。一方、オーバーフロー検査付きの Sum(16,384件)は標準LINQの約2,500nsに対してZLinqが約3,600〜3,900nsと遅く、OrderByDescending→Take は優劣が付きませんでした。集計を速くしたいなら、値域を確かめたうえで SumUnchecked(約1,200〜1,300ns)を選びます。
ZLinqはUnity専用のライブラリですか?
いいえ。NuGetパッケージは netstandard2.0/2.1・net8.0/9.0/10.0 向けで、ASP.NET Coreやコンソールアプリ、.NET Framework 4.8でも使えます。Unity向けの ZLinq.Unity とGodot向けの ZLinq.Godot は、GameObjectやNodeを辿る機能を足す追加パッケージです。
UnityでLINQ to GameObjectを使うにはZLinqだけで足りますか?
足りません。NuGetForUnityで入れるZLinq本体に加えて、git URLで ZLinq.Unity を入れる必要があります。Descendants() や OfComponent<T>() はこのパッケージ側の機能です。
既存のLINQコードを全部書き換える必要がありますか?
いいえ。ZLinq.DropInGenerator を入れてアセンブリ属性を1行書けば、既存のLINQ呼び出しがZLinqで実行されます。ただし IEnumerable<T> を返すメソッドなどは上の表のとおりコンパイルエラーになるので、対象を Collection にとどめ、名前空間で範囲を区切るのが安全です。
ZLinqのライセンスと最新バージョンは?
MITライセンスで、最新は v1.5.6(2026年4月8日)です。v1.0.0は2025年4月8日に公開されました。2026年10月時点で、GitHubのスターは約5,300です。