Upload
ryota-murohoshi
View
2.753
Download
0
Embed Size (px)
Citation preview
UnityでのLINQ活用例室星亮太
2015/02/20(金)
質問です
Unityでゲームを作成している方?
LINQ知っている方?
LINQ使っている方?
LINQがっつり使っている方?
ありがとうございました
UnityでのLINQの活用事例を通し、LINQの良さや利用方法を紹介します。
今日の目標みなさんに• LINQを使うとコードが読みやすくなることを知ってもらう
• LINQを活用できるシーンが多いことを知ってもらう
• LINQを使ってみよう・勉強してみようと思ってもらう
• LINQにこんな便利なメソッドあったんだと知ってもらう
Unityでの例を通して!
ところで
ゲーム開発で大切なものたくさんありますよね
その中のひとつ
簡潔で読みやすいコード
なぜかって?動けば同じじゃないかって?
仕様、変わりますよね?リリース後もアプデしますよね?
「汚いコードでとりあえず動く物」短期的には早いかもしれない
×"短期的な開発速度◯!中・長期的な開発速度
仕様は開発途中で変わりますよね?簡潔で読みやすく奇麗なコードこれに比べて汚く長いコードの仕様変更はとても大変!
遊びながら試行錯誤し面白い物を作るそれには簡潔で読み安いコードが必要!あと汚く長いコード、まじアプデしんどい!
読みやすいコードは大切賛成していただけますか?
ところで今日のテーマはLINQです
LINQを使うと、リストなどのコレクションを扱うコードが、
簡潔に読みやすくなりますよ!
さて、LINQとは
LINQとは統合言語クエリ!(LINQ)!は、強力なクエリ機能を!C#!言語および!Visual!Basic!言語の構文へと拡張する、Visual!Studio!2008!で導入
された機能のセットです。!
MSDN%LINQよりh*ps://msdn.microso7.com/ja:jp/library/bb397926.aspx
イメージわきます?
配列とかリストとかディクショナリとか
シーケンス(↑のようなオブジェの集まり)を
を扱うコードがLINQを使うと綺麗に書ける
ってことだけとりあえず今はイメージしてください(適当ですいません)
今日お話しするのは、LINQ%to%Objectsです
今日の話で「LINQが使えるようになる」とはなりません。みなさんに便利さを紹介しようと思います。
自己紹介• 名前:室星亮太
• 所属:Fuller,)Inc.
• 仕事:Unityを用いたゲーム開発
• 投稿先:h3p://qiita.com/RyotaMurohoshi
• Twi3er:@RyotaMurohoshi
• コミュニティ:Androidの会Unity部
ライブラリUniLinq作ったりUniBookでLINQについて書いたり
LINQ大好きです!「LINQのないC#なんてC#じゃない」
って言う方もいますが、私もそう思います!
そんな私がこういう風に使える、こんな状況で使えるという例をガンガン紹介していきます
第一部
今すぐに使って欲しいメソッド3選
Count・All・Any
こんなクラスがあってpublic class Monster{ public int Id { get; set; } public string Name { get; set; } public int Hp { get; set; } /* 中略 */}
List<Monster>の中でHpが0以下の要素を数える
のようなリスト内の指定の条件を満たした要素を数える
ってことやりません?
Count特定の条件を満たした要素を数える
Hpが0以下の要素、つまり死んでいるモンスター(Monster)の数を数えたい
よくあるforeach、ifのコードList<Monster> monsterList = LoadMonsterList ();
// LINQを使わないとint deadMonsterCount = 0;foreach (Monster monster in monsterList) { if(monster.Hp <= 0) { deadMonsterCount++; }}
Count指定の条件を満たした要素を数える
List<Monster> monsterList = LoadMonsterList ();
// Hpが0以下の要素の数にint deadMonsterCount = monsterList.Count ((Monster monster) => monster.Hp <= 0);
さっきの2つのコードどう読めます?
このコード、どう読みます?List<Monster> monsterList = LoadMonsterList ();
// LINQを使わないとint deadMonsterCount = 0;foreach (Monster monster in monsterList) { if(monster.Hp <= 0) { deadMonsterCount++; }}
int型のdeadMonsterCountを0で初期化
foreach文でmonsterListをまわす
もし要素のmonsterのHpが0以下ならば
deadMonsterCountをインクリメント
あ、つまり!
HPが0以下のモンスターを数えるのか!
という1回頭の中での変換が必要
一方LINQは?どう読みます?// LINQを使うとList<Monster> monsterList = LoadMonsterList ();
// Hpが0以下の要素の数にint deadMonsterCount = monsterList.Count ((Monster monster) => monster.Hp <= 0);
int型のdeadMonsterCountを次の値で初期化
monsterList内の次の条件を満たす要素数だ!
条件は要素のHPが0以下だ!
ほぼ意味的に、「Hpが0以下のモンスターを数えろ!」
左から右に素直に読める!
foreach文、if文のは「どう処理しているか」が書かれている
LINQでは「何がしたいか」が書かれている
だからLINQのコードは読みやすいしかも簡潔
ここからは基本、LINQ版のみでさくさく行きます
Any特定の条件を満たす要素が存在するか調べる
Any特定の条件を満たす要素が存在するか調べる
List<Monster> monsterList = LoadMonsterList ();
// Hpが0以下のモンスターが存在すればtruebool isExistDeadMonster = monsterList.Any((Monster monster) => monster.Hp <= 0);
All全ての要素が指定の条件を
満たすか調べる
All全ての要素が指定の条件を満たすか調べる
List<Monster> monsterList = LoadMonsterList ();
// 全てのモンスターのHpが0以下ならばtruebool isAllMonsterDead = monsterList.All((Monster monster) => monster.Hp <= 0);
次はUnityっぽいのでかつ
割とマイナーなやつを
第二部
Editor拡張でも活躍
OfType
OfType指定の型の要素のみにフィルタリングし、
指定の型のシーケンスに変換
Projectウィンドウ中で選択中の物の中で、テクスチャ(Textureクラス)のみを処理する
(UnityEditor拡張)
• UnityEditor.Selec0onクラスのobjectsプロパティを利用
• Selec0on.objectsは現在選択しているものを取得
• 型はUnityEngine.Object[]で取得
• これには当然テクスチャ(Textureクラス)以外のものも含まれる
• けどTextureのみを対象にしたい
フィルタリングといえばWhereこれをis演算子とあわせて
foreach (UnityEngine.Object target in Selection.objects.Where (o => o is Texture)){
// キャストする or as演算子利用がめんどい Texture texture = target as Texture;
// ここでTextureに何かしらの処理}
Castメソッドでキャストもできますがforeach (Texture texture in Selection.objects .Where (o => o is Texture) .Cast<Texture>()){ // ここでTextureに何かしらの処理}
そこでOfTypeですよ!
OfType指定の型の要素のみにフィルタリングし、指定の型のシーケンスに変換
// Selection.objectsの中の要素をTexture型のみにフィルタリングし、// IEnumerable<Texture>に変換foreach (Texture texture in Selection.objects.OfType<Texture> ()){ // ここでTextureに何かしらの処理}
UnityのEditor拡張で、Selec.onは利用シーンが多い!
(MenuItemと合わせて導入が楽!)
「Selec%on.objects.OfType<Texture>7()」、ぜひ!
実際に使った例がこちら!!h#p://qiita.com/RyotaMurohoshi/items/b01e3cdb91fea96f4574
OfTypeメソッド
他ではNUnitLiteというライブラリを使った際
System.Objectから特定の型への変換でも使いました
Editor拡張でのLINQ活用LINQ、実はiOSで問題があります
けれどUnityEditor内であれば大丈夫!
Editor拡張から利用し始めるのはオススメ!
第三部
指定の条件を満たす先頭の要素を取得する
First、FirstOrDefault
List<Monster>の中でHpが0以下の要素が一つ欲しい
のようなリスト内の指定の条件を満たす要素を一つ取得する
ってことやりません?
First指定の条件を満たす先頭の要素を取得する条件を満たした要素がない場合は例外発生
First指定の条件を満たす先頭の要素を取得する。条件を満たした要素がない場合は例外発生
List<Monster> monsterList = LoadMonsterList ();
// ひとつもHpが0以下の要素が存在しないなら例外発生Monster deadMonster = monsterList .First ((Monster monster) => monster.Hp <= 0);
Firstメソッドは
条件を満たす要素がない場合例外が発生!
じゃあ例外を発生たくない場合?
Anyで存在チェックしてあった場合のみ、もしくは...
FirstOrDefault指定の条件を満たす先頭の要素を取得する条件を満たした要素がない場合は
その型の規定値を返す
条件を満たした要素がない場合、
「規定値を返す=nullを返す」じゃない!
ここ要注意!値型の規定値はnullじゃない!
規定値• クラス型の場合、null
• intの場合、0
• floatの場合、0.0F
• boolの場合、false
• Vector3の場合、x、y、zが全て0.0F
FirstOrDefault指定の条件を満たす先頭の要素を取得する。条件を満たした要素がない場合はその型の規定値を返す
List<Monster> monsterList = LoadMonsterList ();
// ひとつもHpが0以下の要素が存在しないならMonsterクラスの規定値nullにMonster deadMonster = monsterList .FirstOrDefault ((Monster monster) => monster.Hp <= 0);
ここでさらにもう一つ!
DefaultIfEmptyシーケンスが空ならば引数で渡したものを唯一の要素とするシーケンスに変換
Firstと組み合わせてこんなことができる
DefaultIfEmptyシーケンスが空ならば引数で渡したものを唯一の要素とするシーケンスに変換
List<Monster> monsterList = LoadMonsterList ();Monster defaultMonster = GetDefaultMonster ();int targetId = GetTargetId ();
// 指定IDのモンスターを探す。なかったらdefaultMonsterに。Monster targetMonster = monsterList .Where ((Monster monster) => monster.Id == targetId) .DefaultIfEmpty (defaultMonster) .First (); // このオーバーロードは単純に先頭要素取得
ところで
デリゲートとラムダ式• 「(Monster*monster)*=>*monster.Hp*<=*0」はラムダ式
• 「ラムダ式」ではなく「デリゲート」を引数にとる
• LINQの多くのメソッドがFunc<T,*TResult>を取る
• ラムダ式の用途の一つはデリゲートの生成
詳しくは! h"p://qiita.com/RyotaMurohoshi/items/740151bd772889cf07de にまとめました
第四部
算術計算系
Max、Min、Sum、Average
こんなクラスを使いますpublic class PlayLog{ public int StageId { get; set; } public int Score { get; set; } public DateTime PlayedAt { get; set; } public TimeSpan PlayTime { get; set; }}
List<PlayLog>の要素の中で一番大きいScoreを求める
のようなリスト内要素を変換して、その一番大きい値を求める
ってことやりません?
Max各要素に変換を施しその最大値を計算する
Max各要素に変換を施しその最大値を計算する
List<PlayLog> playLogList = LoadPlayLogList ();
// PlayLogListのScoreの最大値int maxScore = playLogList.Max ((PlayLog playLog) => playLog.Score);
Min各要素に変換を施しその最小値を計算する
Min各要素に変換を施しその最小値を計算する
List<PlayLog> playLogList = LoadPlayLogList ();
// PlayLogListのScoreの最小値int minScore = playLogList.Min ((PlayLog playLog) => playLog.Score);
Sum各要素に変換を施しその合計値を計算する
Sum各要素に変換を施しその合計値を計算する
List<PlayLog> playLogList = LoadPlayLogList ();
// PlayLogListのScoreの合計値int sumOfScore = playLogList.Sum ((PlayLog playLog) => playLog.Score);
Average各要素に変換を施しその平均値を計算する
Average各要素に変換を施しその平均値を計算する
List<PlayLog> playLogList = LoadPlayLogList ();
// PlayLogListのScoreの平均値double averageOfScore = playLogList.Average ((PlayLog playLog) => playLog.Score);
ところで
LINQとiOS(1)• Unity+iOSだと実機実行時エラーになることがある
• 理由はAOTコンパイルがうまくいっていないみたい
• それを解決するためにUniLinqっていうやつを作った
LINQとiOS(2)• IL2CPP登場
• IL2CPPで大丈夫か試したら、テストするやつが実機でエラーに
• AssemblyクラスのCodePathプロパティがnullなのが原因
• 今後も正しく動くかどうかなど、継続して調べていきます!
第五部
並び替え
OrderBy、OrderByDescending
ThenBy、ThenByDescending
List<PlayLog>をScroreの降順で並び変える
のような整列操作
ってやりません?
OrderBy各要素に変換を施しその結果で並び替え
OrderBy各要素に変換を施しその結果で並び替え
List<PlayLog> playLogList = LoadPlayLogList ();
// スコアの降順で並び替えIEnumerable<PlayLog> orderedPlayLog = playLogList .OrderByDescending ((PlayLog playLog) => playLog.Score);
OrderByは昇順で並び替え、OrderByDescendingは降順で並び替え
元のデータを並び変えるのでなく、新たに並び替えたシーケンスをつくる
もし同じScoreならどうしよう?
同ScoreならばPlayTimeが短い順にしたい場合は?
もう一度OrderByを呼ぶのは間違い!
ThenByOrderByで並び替えたもので、同評価だったものを並び替え
ThenByOrderByで並び替えたもので、同評価だったものを並び替え
List<PlayLog> playLogList = LoadPlayLogList ();
// スコアの降順で並び替えて、同点なら時間が短い方を先にIEnumerable<PlayLog> orderedPlayLog = playLogList .OrderByDescending ((PlayLog playLog) => playLog.Score) .ThenBy ((PlayLog playLog) => playLog.PlayTime);
第六部
スキップと取得
Skip、Take、SkipWhile、TakeWhile
データを画面に10個ずつ表示
インデックス0の1ページ目、0番目から9番目
インデックス1の2ページ目、10番目から19番目
インデックス2の3ページ目、20番目から29番目
ってことやりません
Skip%+%Take
Skip先頭から引数で渡した個数だけ
要素を飛ばす
Take先頭から引数で渡した個数だけ
要素を返す
Skip+Takeデータを画面に10個ずつ表示、今回は3ページ目(インデックスは2)
List<PlayLog> playLogList = LoadPlayLogList ();
int numPerPage = 10;int pageIndex = 2;int skipNum = numPerPage * pageIndex;
// 3ページ目(インデックスは2)、20番から10個の要素を取得IEnumerable<PlayLog> selectedPlayLogs = playLogList .Skip (skipNum) .Take (numPerPage);
今、DateTime型のPlayedAtの昇順でPlayLogが並んでいる
ある時刻から、他のある時刻までのデータを取得したい
SkipWhile)+)TakeWhile
SkipWhile先頭から引数で渡した条件が真の間
要素を飛ばす
TakeWhile先頭から引数で渡した条件が真の間
要素を返す
SkipWhile)+)TakeWhileList<PlayLog> playLogList = LoadPlayLogList ();
DateTime start = GetStartTime ();DateTime end = GetEndTime ();
// PlayedAtがstartより後or同時刻で、endより前な要素を取得IEnumerable<PlayLog> selectedPlayLogs = playLogList
.SkipWhile ((PlayLog playLog) => playLog.PlayedAt < start) .TakeWhile ((PlayLog playLog) => playLog.PlayedAt < end);
第七部
その他オススメ
ElementAtシーケンス中の
指定のインデックスの要素を取得
⚪番インデックスの要素が欲しいこういう時、ありますよね。どうします?
あまり書かないかもしれないけど、リファクタリングの途中で必要なこともある?
多くのLINQメソッドの返値型である、IEnumerable<T>型は
インデクサで要素にアクセスできない!
ToArrayはダメ絶対!// ToArrayで配列にしてからアクセス?// 効率悪い!!!// playLogsはIEnumerable<PlayLog>型PlayLog index5PlayLog = playLogs.ToArray ()[5];
ElementAt
// インデクサはないけれど// ElementAtメソッドでインデックスにアクセスできるPlayLog index5PlayLog = playLogs.ElementAt (5);
ToDic&onaryシーケンスから辞書を作る
キーを指定する。バリューも指定できる。
こんなクラスを使いますpublic class Item{ public int Id { get; set; } public string Name { get; set; } public ItemType ItemType { get; set; }}
public enum ItemType{ Weapon, ConsumptionItem, ValuableItem}
List<Item>*からItemのIdをキーとする
Dic$onary<int,-Item>をつくる
みたいなことしたくありません?
それLINQで一行でかけます!
ToDic&onaryシーケンスから辞書を作る!(その1)
List<Item> itemList = LoadItemList ();
// Idプロパティをキーとするディクショナリの生成// もしキーの重複があったら例外が発生Dictionary<int, Item> itemDict = itemList .ToDictionary((Item item) => item.Id);
ToDic&onaryシーケンスから辞書を作る!(その2)
List<Item> itemList = LoadItemList ();
// Idプロパティをキー、Nameプロパティバリューとする// ディクショナリの生成// もしキーの重複があったら例外が発生Dictionary<int, string> itemDict = itemList.ToDictionary( (Item item) => item.Id, (Item item) => item.Name);
ToLookupシーケンスからILookupを作る
グループ化したコレクションを生成
List<Item>*itemListがあって
Dic$onary<ItemType,2List<Item>>が欲しい
LINQにはDic(onary<ItemType,5List<Item>>に似ている
ILookup<ItemType,.Item>が作れるメソッドがある
ToLookupシーケンスからILookupを作る(グループ化したコレクションを生成)
List<Item> itemList = LoadItemList ();
// ILookup<ItemType, Items>は// Dictionary<ItemType,List<Item>>に似ているILookup<ItemType, Item> itemLookup = itemList. ToLookup((Item item) => item.ItemType);
// ディクショナリのように[]とキーでそのグループにアクセスできるIEnumerable<Item> weapons = itemLookup[ItemType.Weapon];
SelectManyリストのリストの平滑化を行う
SelectManyリストのリストの平滑化を行う(その1)
// モンスターのリストのリストList<List<Monster>> listOfMonsterList = LoadListOfMonsterList ();
// モンスターのシーケンスにIEnumerable<Monster> monsters = listOfMonsterList .SelectMany ((List<Monster> list) => list);
SelectManyはリストのリストだけが対象じゃない
結構使える!
こんなクラスがあってpublic class MonsterParty{ public string Name { get; set; } // Monsterのリスト、このプロパティがミソ public List<Monster> MonsterList { get; set; }}
SelectManyリストのリストの平滑化を行う(その2)
// パーティーのリストList<MonsterParty> monsterPartyList = LoadMonsterPartyList ();
// モンスターのシーケンスにIEnumerable<Monster> monsters = monsterPartyList .SelectMany ((MonsterParty party) => party.MonsterList);
Dis$nct重複を除く
Dis$nct重複を除く
// シンプルな例int[] ids = new int[]{0, 1, 1, 2, 2, 2, 3, 3, 4};
// 重複が除かれているIEnumerable<int> distinctedIds = ids.Distinct ();
Dis$nct重複を除く
// パーティーに同じモンスターはいないが、他のパーティーには同じモンスターがいるList<MonsterParty> monsterPartyList = LoadMonsterPartyList ();
// 重複の無しモンスターのIdのシーケンスを作るIEnumerable<int> monsterIds = monsterPartyList // モンスターのシーケンスにして .SelectMany (party => party.MonsterList) // Idのシーケンスにして .Select (monster => monster.Id) // 重複を排除する .Distinct ();
ちなみにLINQには次のような集合演算を行うものもあります
差集合を求める!Except
和集合を求める!Union
積集合を求める!Intersect
Join二つのシーケンスをキーで突合
PlayLogクラスとStageDataクラスから、PlayDataを作りたい
public class PlayLog { // StageのIdのみでStageの情報はない public int StageId { get; set; } public int Score { get; set; } /* 中略 */
}
public class StageData { public int Id { get; set; } public string Name { get; set; } /* 中略 */
}
PlayLogクラスとStageDataクラスから、PlayDataを作りたい
public class PlayData { // StageDataクラスの名前から public string StageName { get; set; } // PlayLogクラスのScoreから public int Score { get; set; } /* 中略 */}
List<PlayLog>なplayLogListと
List<StageData>なstageDataListから
PlayDataのシーケンスを作りたい
foreachな例// foreachな例、長いList<PlayData> playDataList = new List<PlayData> ();foreach(PlayLog playLog in playLogList) { StageData targetStageData = null; foreach(StageData stageData in stageDataList) { if (playLog.StageId == stageData.Id) { targetStageData = stageData; break; // nullなやつはないよねというコード } } playDataList.Add (new PlayData { StageName = targetStageData.Name, Score = playLog.Score, });}
LINQ利用な例// LINQ利用な例、まだ長いList<PlayData> playDataList = new List<PlayData> ();
foreach(PlayLog playLog in playLogList) { StageData stageData = stageDataList.First (stage => stage.Id == playLog.StageId);
playDataList.Add (new PlayData { StageName = stageData.Name, Score = playLog.Score, });}
Joinの例IEnumerable<PlayData> playDatas = playLogList.Join ( stageDataList, (PlayLog playLog) => playLog.StageId, // PlayLogのキー (StageData stageData) => stageData.Id, // StageDataのキー // 二つのキーが揃ったPlayLogとStageDataが来る (PlayLog playLog, StageData stageData) => new PlayData { StageName = stageData.Name, Score = playLog.Score, });
さて
LINQとのつきあい方• エラーにならないライブラリを作るor使う
• エラーになるメソッドor使い方で使わない
• Editor拡張のみで使う
• エラーになる問題が直るまで待つ
何か質問あります?
さて、今日の目標のおさらいです
今日の目標みなさんに• LINQを使うとコードが綺麗になることを知ってもらう
• LINQを使える活用シーンが多いことを知ってもらう
• LINQを使ってみよう・勉強してみようと思ってもらう
• LINQにこんな便利なメソッドあったんだと知ってもらう
いかがだったでしょうか?LINQ使ってみたくなりましたか?
ありがとうございました!@RyotaMurohoshi