Unityに関連する記事です

LINQ


 LINQ (Language Integrated Query)【リンク】は、.NETフレームワークの一部で、C#言語に組み込まれた強力なデータ操作機能です。
配列やリストなどのコレクションからデータを取り出したり、操作したりするための機能を提供します。

 LINQの主な機能は、データのフィルタリング、ソート、変換、集計などです。
SQL(データベースの問い合わせ言語)に似た構文を使用して、これらの操作を行います。

 LINQはコレクションだけでなく、データベースやXMLファイルなどの他の種類のデータソースに対しても使用できます。


LINQ に利用されているシンタックス(構文)


 LINQ に用意されているメソッドには、多くのシンタックス(英: syntax、構文)が利用されています。
 
 そのため、LINQ をしっかりと理解した上で活用していくためには、下記のような処理を理解しておくことが重要です。


1.ラムダ式

 
 LINQのクエリでは、ラムダ式を多用します。
これはメソッドを簡潔に記述するためのシンタックスで、例えば x => x > 10 という形で「xが10より大きい」ことを表します。

 ラムダ式の理解はLINQを使う上で大変重要です。

 またラムダ式を理解するためには、デリゲートについても理解しておく必要があります。

 学習の進め方としては、デリゲート → ラムダ式 → LINQ の順番がよいでしょう。


2.メソッドチェーン


 LINQでは複数の操作(メソッド)を.(ドット)を使ってつなげることができます。
この記述方法をメソッドチェーンと呼びます。

 これにより一連の操作を一行で記述することができます。

 例えば list.Where(x => x > 10).Select(x => x * 2) のように書けます。
これは「listから10より大きい要素を取り出し、それらを2倍にする」という操作を表します。


3.拡張メソッド


 LINQのメソッドの多くは拡張メソッドとして実装されています。
これにより、例えば IEnumerable<T> 型のオブジェクトに対して直接 Where や Select などのメソッドを呼び出すことができます。


4.ジェネリックメソッド


 LINQのメソッドの多くはジェネリックメソッドです。これにより、任意の型のデータに対して同じ操作を行うことができます。


5.遅延評価(遅延実行)


 LINQのクエリは実行が遅延される特性があります。
つまり、クエリが定義された時点ではデータの操作は行われず、実際に結果が必要となったタイミング
(例えば ToList() や ToArray() が呼び出された時、または foreach 文で反復処理が行われた時など)で初めて実行されます

 これにより、A の処理の結果を使って、B の処理を行うといった記述を1行で書くことができたり、そのときに必要なデータだけを効率的に処理することができます。



 これらの概念はLINQを使う上で重要で、それぞれがどのように動作するかを理解することで、LINQのクエリをより効果的に書くことができます。


LINQ の活用場所


 LINQ はコレクションに対して用意された機能です。

 List の操作を行う場合、LINQ を利用しない場合には、foreach を行って、if 文などを組み合わせることで、同じ挙動を実現可能です。
LINQは、そのようなforeachループとif文を使った操作をよりシンプルに、また効率的に行うためのツールとして設計されています。

 従来のforeachループによる方法は、理解しやすく、明示的な操作を行うことができますが、コードが冗長になることがあります。
特に、複雑な処理を行う場合や、複数の操作を一度に行う場合には、foreachループをネストしたり、複数のif文を組み合わせたりする必要があります。

 それに対して、LINQは、一連の操作を一行で記述することができ、コードの見通しが良くなることがあります。
また、LINQは遅延実行が可能なので、必要なデータだけを効率よく処理することができます。

 しかし、LINQは一部の特殊なケースでパフォーマンスに影響を及ぼす可能性があるため、大量のデータを処理する場合などは注意が必要です。
また、LINQのメソッドは、先ほどもお伝えしたように、多くのシンタックスを覚える必要があり、初学者にとっては理解するのが難しい機能でもあります。

 今回は、従来の foreach + if 文によって作成された List の操作をベースに、LINQ を使って置き換えて学習していくことが目的です。

 LINQ の処理と、自分の学習してきた従来の処理とを、比較しながら置き換えていくことにより、理解を深めていただくことが狙いです。


.灰譽ションから特定の条件を満たす要素を探す ーWhere メソッドー


 それでは実際に、リファクタリングしながら学習していきます。

 まず List 操作において頻繁に利用される、コレクション内特定の条件を満たす要素を探す処理について、
Where メソッドを使って処理を置き換えていきましょう。

 まずは、従来の処理の記述です。


List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> evenNumbers = new ();

foreach(int num in numbers){
    if(num % 2 == 0){
        evenNumbers.Add(num);
    }
}

 このコードでは、numbersリストの中から偶数を見つけてevenNumbersリストに追加しています。



 それではこの処理を LINQ を使った、Where メソッドに置き換えていきます。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> evenNumbers = numbers.Where(num => num % 2 == 0).ToList();

 Where()メソッドは条件を満たす要素をフィルタリングし、ToList()は結果を新しいリストに変換します。

 Where メソッドによって抽出された情報はまだ List にはなっておらず、一旦、抽出だけされた、いわば待機の状態です。

 それに対して、メソッドチェーンを行い、ToList メソッドをつなげることで、抽出結果を List にすることが出来ます。


参考サイト
MicroSoft
Enumerable.Where メソッド




 List には FindAll メソッドが用意されており、Where メソッドを同じように、特定の条件に合致するすべての要素を抽出することが出来ます。
ただし、FindAll メソッドはメソッドチェーンを行うことが出来ません。

 そのため、特定の条件に合致する要素を抽出するだけではなくて、その後に他の処理を組み合わせたい場合には Where メソッドであれば対応可能です。
実際の活用例は、後ほど紹介します。


▲灰譽ションの各要素を操作する ーSelect メソッドー


 Where メソッドと同じく頻繁に利用されるメソッドです。

 コレクション内から特定の条件を満たす要素だけを取り出す機能です。
また、取り出すと同時に、その要素を加工することも出来ます。

 Select メソッドで行う、データの集まりから必要な項目だけを取り出すことを射影(しゃえい)処理といいます。

 それでは従来の処理からサンプルコードを提示します。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> squaredNumbers = new ();

foreach(int num in numbers){
    squaredNumbers.Add(num * num);
}

 ここでは、リスト内の各要素を二乗して新しいリストに追加しています。



 それではこの処理を LINQ を使った、Select メソッドに置き換えていきます。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> squaredNumbers = numbers.Select(num => num * num).ToList();

 Select()メソッドは各要素を操作し、その結果を返します。
また、結果を新しいリストに変換するために Where メソッドのときと同じように、ToList()を使用します。


参考サイト
MicroSoft
Enumerable.Select メソッド



コレクションの要素をフィルタリングし、フィルタリング結果を操作する ーWhere メソッドと Select メソッドを組み合わせるー


 ここまで紹介してきた Where メソッドによるフィルタリングと、Select メソッドによる操作を組み合わせることで実現できます。

 Select メソッドも遅延実行処理になるため、Select メソッドの前に別の処理(今回は Where メソッド)を書くことで、
その前のメソッドの処理の結果を使って、Select メソッドを実行することが可能です。

 まずは従来の処理を書いていきましょう。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> results = new ();

foreach(int num in numbers){
    if(num > 2){
        results.Add(num * num);
    }
}

 このコードでは、リストから数値が2より大きい要素を見つけ、それを二乗して新しいリストに追加しています。



 それではこの処理を LINQ を使って、Where メソッドと Select メソッドを組み合わせた処理に置き換えていきます。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> results = numbers.Where(num => num > 2).Select(num => num * num).ToList();

 Where() と Select()を連鎖して使用し、フィルタリングと操作を一度に行います。
このような記述方法をメソッドチェーンといい、LINQ においては非常に利用される記法です。

 メソッドチェーンの記述の場合、1行で書くことも可能ですが、ドットの部分で改行する記法が一般的です。

 上記であれば

List<int> results = numbers.Where(num => num > 2)
            .Select(num => num * num)
            .ToList();

 このように段落を付けて、メソッドの部分で改行して書くようになります。


IEnumerable インターフェース


 LINQメソッドのほとんどはIEnumerableまたはIEnumerable<T>を返します。これは、そのメソッドが一連の要素を表すシーケンスを返すことを意味します。

 IEnumerableとIEnumerable<T>は、.NETフレームワークのインターフェースで、コレクションが繰り返し処理(foreachなど)できることを示します。
つまり、これらのインターフェースを実装しているオブジェクトは、一連の項目を提供します。

 IEnumerableは非ジェネリックバージョンで、任意のオブジェクトのシーケンスを表します。
一方、IEnumerable<T>はジェネリックバージョンで、特定の型Tのオブジェクトのシーケンスを表します。

 LINQの各メソッドがIEnumerableまたはIEnumerable<T>を返す理由は、それによって返されたシーケンスに対して、さらにLINQメソッドを適用できるからです。
つまり、LINQメソッドの出力を別のLINQメソッドの入力として使用でき、これにより複数のLINQメソッドをチェーン(連結)することが可能となります。

 これがLINQの強力な部分であり、データ操作を大幅に単純化します。


メソッドチェーンについて


 メソッドチェーンとは、一連のメソッド呼び出しを一度に行う構文(シンタックス)のことです。
これは、各メソッドがその戻り値(通常は同じオブジェクトまたは新しいオブジェクト)に対して、呼び出される新しいメソッドを提供することによって可能になります。

 LINQ では、メソッドチェーンは非常に一般的です。
これは、ほとんどの LINQ メソッドが列挙可能なコレクションを返すため、次の操作をその結果に直接連鎖することができるからです。

 例えば、以下のコードではWhereとSelectのメソッドチェーンを作成しています。

List<int> numbers = new List<int> {1, 2, 3, 4, 5};
List<int> results = numbers.Where(num => num > 2).Select(num => num * num).ToList();

 ここで、numbers.Where(num => num > 2)の部分が新しい列挙可能なコレクションを返し、それに対して直接Selectメソッドを呼び出しています。

 このように、メソッドチェーンによって、コードは一般的に読みやすく、簡潔になります。
複数の操作を一行で表現することができ、また、一時変数を作成して操作を分割する必要がなくなります

 ただし、非常に長いメソッドチェーンは逆に読みにくくなる場合もあるので、適度な長さに保つことが大切です。


ケーススタディ


 ゲーム内で、特定のアイテム(例えば、価値が100以上のアイテム)を持つキャラクター名をすべて表示したいとします。

 以下のようなクラス構造を想定します。

public class Item
{
    public string Name { get; set; }
    public int Value { get; set; }
}

public class Character
{
    public string Name { get; set; }
    public List<Item> Items { get; set; }
}

 ここでは、Characterのリストから、名前が特定の文字列に一致する最初のCharacterを探す処理を考えます。

 まずは初期化を行い、List の準備を整えます。

// アイテムの作成
Item sword = new Item { Name = "Sword", Value = 100 };
Item shield = new Item { Name = "Shield", Value = 150 };
Item potion = new Item { Name = "Potion", Value = 50 };

// キャラクターの作成とアイテムの割り当て
Character john = new Character 
{ 
    Name = "John", 
    Items = new List<Item> { sword, shield } 
};

Character mary = new Character 
{ 
    Name = "Mary", 
    Items = new List<Item> { potion } 
};

Character tom = new Character 
{ 
    Name = "Tom", 
    Items = new List<Item> { sword, potion } 
};

// キャラクターをリストに格納
List<Character> characters = new List<Character> { john, mary, tom };

 この情報を利用します。

 まずはLINQを使わない従来の方法で実装します。

// 価値が100以上のアイテムを持つキャラクターの名前を格納するリスト
List<string> characterNames = new ();

foreach (Character character in characters)
{
    foreach (Item item in character.Items)
    {
        if (item.Value >= 100)
        {
            characterNames.Add(character.Name);
            break;  // 1つ見つかったら次のキャラクターへ
        }
    }
}

// 名前のリストを表示
foreach (string name in characterNames)
{
    Console.WriteLine(name);
}

 ここでは、foreachを二重に使って各キャラクターの各アイテムを調べ、その価値が100以上のアイテムを持つキャラクターの名前をリストに追加しています。



 それに対して、LINQを使うと、この処理は大幅にシンプルになります。

var characterNames = characters
    .Where(character => character.Items.Where(item => item.Value >= 100).ToList().Count > 0)
    .Select(character => character.Name);

// 名前のリストを表示
foreach (string name in characterNames)
{
    Console.WriteLine(name);
}

 ここでは、まずWhereメソッドで各キャラクターが持つアイテムの中から価値が100以上のアイテムを抽出します。
そして、それらのアイテムの数が0より大きい(つまり、少なくとも1つは存在する)キャラクターを抽出します。

 その後、Selectメソッドでそのキャラクターの名前を取得しています。



 次に学習する Any メソッドを利用すると、さらにシンプルになります。

var characterNames = characters
    .Where(character => character.Items.Any(item => item.Value >= 100))
    .Select(character => character.Name);

// 名前のリストを表示
foreach (string name in characterNames)
{
    Console.WriteLine(name);
}

 character.Items.Any(item => item.Value >= 100) は、キャラクターが保持するアイテムの中に価値が100以上のものが1つでもあるかどうかをチェックしています。
これが true の場合、そのキャラクターは結果のリストに含まれます。

 この部分が、先ほどとは異なる部分です。


<遅延評価と即時評価>


 INQクエリは遅延評価されます。
つまり、クエリが定義される時点ではデータはまだ取得されず、実際にデータにアクセスするとき
(例えばforeachループで走査するときや、ToListメソッドを呼び出すときなど)に初めてデータが取得されます。

 上記の var の型は IEnumerable<string>型ですが、未確定の状態です。
この時点では List として利用しないため、Select メソッドのあとに ToList メソッドはありません。

 この場合、次に用意されている foreach メソッド内で変数が実際に利用される際に、遅延評価されて、その時点で List<string>として取得されます。

 そのようにして処理の運用に合わせて型を確定していきたいため、宣言の時点では var としています。



 しかし、データの取得をすぐに行いたい、またはクエリ結果をリストとして扱いたい場合は、ToListメソッドを使うことでそれを実現できます。
これにより、クエリ結果はすぐに評価され、その結果がList<string>として返されます。この操作は即時評価と呼ばれます。

List<string> characterNames = characters
    .Where(character => character.Items.Any(item => item.Value >= 100))
    .Select(character => character.Name)
    .ToList();

 したがって、ToListを呼び出すかどうかは、その時点でデータを取得する必要があるか、またはリストとして扱いたいかどうかによります。



  => 既存の処理をリファクタリングして学ぶ LINQ 活用例

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます