c# 式木 (expression tree) ~ linqをより深く理解するために ~
DESCRIPTION
Hokuriku.NET C# 勉強会「C# 式木 (Expression Tree) ~ LINQをより深く理解するために ~」 https://atnd.org/events/57085 で使用した資料に加筆 1. LINQ to Objects 復習 2. IQueryable 3. 式木 (Expression Tree) 4. 式木メタ プログラミング 5. LINQ プロバイダーTRANSCRIPT
C# 式木Expression Tree
~ LINQ をより深く理解するために ~
小島 富治雄Hokuriku.NET C# 式木
https://atnd.org/events/57085
2014-10-26
自己紹介
• 小島 富治雄
• @Fujiwo
• http://blog.shos.info
• 福井コンピュータアーキテクト株式会社
• Microsoft MVP C# (2005-2015)
本日の資料
• スライドhttp://slidesha.re/1tA0Tit
• ソースコードhttp://1drv.ms/1zs3n78
※ スライド中の でソースコードを参照 / 実行のこと
3
ソースコード参照
本日のゴール
• .NET の式木を理解することで、 LINQ についてより深く理解する
アジェンダ
1. LINQ to Objects 復習
2. IQueryable<T>
3. 式木 (Expression Tree)
4. 式木メタ プログラミング
5. LINQ プロバイダー
1. LINQ to Objects 復習
LINQ to Objects
• デモ : LinqToObjectsSample
IEnumerable<int> sequence1 = new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34 };
IEnumerable<int> sequence2 = sequence1.Where (x => x % 2 == 0);
IEnumerable<int> sequence3 = sequence2.Select (x => x * x );
foreach (int item in sequence3)
Console.WriteLine(item);
ソースコード参照
LINQ to Objects
• 「 LINQ は IEnumerable<T> なものへのクエリ」
LINQ to Objects
• デモ : LinqToObjectsSample
IEnumerable<int> sequence1 = new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34 };
IEnumerable<int> sequence2 = sequence1.Where (x => x % 2 == 0);
IEnumerable<int> sequence3 = sequence2.Select (x => x * x );
foreach (int item in sequence3)
Console.WriteLine(item);
実際に sequence3 から値が取り出されるまで、 sequence1 から値は
取り出されず、 Where や Select に渡したデリゲートも実行されない
ソースコード参照
2. IQueryable<T>
匿名メソッドとラムダ式の違い
• デモ : 匿名メソッドとラムダ式の違いvar data = new EmployeeDataClassesDataContext();
data.Log = Console.Out;
var sequence1 = data.Employee;
var sequence2 = sequence1.Where (
employee => employee.Name.Contains("田 ") );
var sequence3 = sequence2.Select (
employee => new { 番号 = employee.Id, 名前 = employee.Name });
foreach (var employee in sequence3)
Console.WriteLine("{0}: {1}", employee.番号 , employee.名前 );
ソースコード参照
匿名メソッドとラムダ式の違い
• 匿名メソッドを渡した場合の SQL:
SELECT [t0].[Id] AS [ 番号 ], [t0].[Name] AS [ 名前 ]
FROM [dbo].[Employee] AS [t0]
WHERE [t0].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [% 田 %]
SELECT [t0].[Id], [t0].[Name]
FROM [dbo].[Employee] AS [t0]
• ラムダ式を渡した場合の SQL:
匿名メソッドとラムダ式の違い
• 匿名メソッドを渡した場合の Where:
public static class Queryable
{
public static IQueryable<T> Where<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate);
}
public static class Enumerable
{
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, int, bool> predicate);
}
• ラムダ式を渡した場合の Where:
匿名メソッドとラムダ式の違い
• 二つの LINQ
• IEnumerable<T>.Where( 匿名メソッド )
• IQueryable<T>.Where( ラムダ式 )
匿名メソッドとラムダ式の違い
• デモ : 式として扱えるラムダ式と扱えないラムダ式
class Program
{
static void Main()
{
Func<int, int, int> sequence1 = (x, y) => x + y;
Func<int, int, int> sequence2 = (x, y) => { return x + y; };
Expression<Func<int, int, int>> expression1 = (x, y) => x + y;
//Expression<Func<int, int, int>> expression2 = (x, y) => { return x + y; };
}
}
ブロックが含まれるラムダ式は式として扱えない (IQueryable<T> には使えない )
ソースコード参照
LINQ 構文
• LINQ 構文は、「全てブロックの無いラムダ式」の場合の糖衣構文• なので、 IQueryable<T> の方になる
var sequence4 = from employee in data.Employee
where employee.Name.Contains(" 田 ")
select new { 番号 = employee.Id, 名前 = employee.Name };
二つの LINQ
• 「 LINQ は IEnumerable<T> なものへのクエリ」
• 「 LINQ は IQueryable<T> なものへのクエリ」
IQueryable インターフェイス
• IQueryable インターフェイス - MSDN• http://msdn.microsoft.com/ja-jp/library/system.linq.iqueryable(v=vs.110).aspx
public interface IQueryable : IEnumerable
{
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
public interface IQueryable<T> : IEnumerable<T>, IQueryable
{}
IQueryable<T>
• デモ : IQueryableSampleclass Foo : IQueryable
{
public Type ElementType
{ get { throw new NotImplementedException(); } }
public Expression Expression
{ get { throw new NotImplementedException(); } }
public IQueryProvider Provider
{ get { throw new NotImplementedException(); } }
public IEnumerator GetEnumerator()
{ throw new NotImplementedException(); }
}
ソースコード参照
3. 式木 (Expression Tree)
式木の構造
• デモ : 式の構造を調べてみる
Expression<Func<int, int, int>> expression = (x, y) => x + y;
((Expression)expression).Show();
ソースコード参照
式木の構造
• (x, y) => x + y
式木の構造
• Body: x + y
式木の構造
• Body.Left: x
式木の構造
• Parameters: (x, y)
式木の構造
• Parameters[0]: x
4. 式木メタ プログラミング
前回は「リフレクション」でしたが…
• リフレクション• 実行時に柔軟に対応
• 型が不明なものとバインドする UI 部品など
• インタフェイスに頼らない規約によるプログラミング
• 実行時に型のメタデータを取得
Assembly Module
Type・ Class・ Interface・ Value Type
FieldInfo
PropertyInfo
EventInfo
MethodInfoConstructorInfo ParameterInfo
リフレクションの欠点
遅い
メタプログラミングとは
• 高次なプログラミング• プログラムを対象とするプログラムを書
く
• プログラムを操作 / 出力するプログラムを書く• プログラムでプログラムを出力すると
手でプログラムを書くよりも効率的な場合が
実行時の環境に対応
• ビルド時でなく、実行時にプログラムを書きたい
• 「クラスやメソッドはビルド時までに完成してないといけない。実行時には追加 / 変更できない」• なんで ?
メタプログラミングが有効な例
• コンパイラー / インタープリター• ホスト言語のソースコードから対象言語のプログラムを生成
• O/R マッパー• クラスやオブジェクトから SQL を生成
• モック (mock) オブジェクト• 「モック ( ユニットテストで用いられる代用の
オブジェクト ) を生成」するプログラムを生成
メタプログラミングが有効な例
• XML や JSON の入出力• 「クラスやオブジェクトから XML や JSON を生
成 /XML や JSON からクラスやオブジェクトを生成」するプログラムを生成
• Web アプリケーション• クライアント側で動作するプログラム
(JavaScript 等 ) をサーバー側で動的に生成
参考 : .NET でのメタプログラミングの例
参考 :CodeDOM (Code Document Object Model)
• System.CodeDom 名前空間や System.CodeDom.Compiler 名前空間• C# や Visual Basic 等のコードを生成
• コンパイルしてアセンブリを生成
CodeDOM CodeDOMProvider
ソース コード(C# 、 VB 、 JScript
)
アセンブリ
GenerateCodeFromNamespace
CompileAssemblyFromDom
参考 : リフレクション
• System.Reflection 名前空間• クラスやインスタンスに関する情報 ( メ
タデータ ) を取得し、メンバーを呼び出したりできる
• System.Reflection.Emit 名前空間• CIL (Common Intermediate
Language) からクラス等を動的生成
参考 : CodeDOM によるメタプログラミング
• デモ : CodeDomHelloWorldnamespace CodeDomHelloWorldDemo{ using System;
class Program { static void Main() { Console.WriteLine("Hello world!"); Console.ReadKey(); } }}
ソースコード参照
参考 : Ildasm.exe (IL 逆アセンブラー )
• アセンブリを逆アセンブルして中間言語 (IL: Intermediate Language) を表示
• Visual Studio の「開発者コマンド プロンプト」から Ildasm.exe を起動
• Ildasm.exe (IL 逆アセンブラー ) – MSDN• http://msdn.microsoft.com/ja-jp/library/f7dy01k1(v=vs.110).aspx
式木 によるメタプログラミング
• System.Linq.Expressions 名前空間• 式木を生成し、動的にプログラムを生成
式木
Expression<Func<Employee, bool>> expression = employee => employee.Name.Contains(" 山 ");
Parameters
Body Object
Method
Arguments
Expression
Member
employee => employee.Name.C
ontains(" 山 ")employee.Name
Contains
employee
Name
“ 山”
employee
employee.Name.Contains(" 山 ")
Visual Studio のデバッガーで add 式の構造を見る
例 : 式木による Add メソッドの動的生成
• 簡単なメソッド (Add) の生成1.式木を組み立てる
2.コンパイル
+
x y
=>
(x, y)
式の種類
• Expression の派生クラス一覧 - 継承階層 - Expression クラス - MSDN ライブラリ
(x, y) => x + y の式木
パラメーターの x と y は、 (x, y) 部分と x + y 部分で使われているが、それぞれ 1 インスタンスずつにする
式木によるメタプログラミング
• デモ : ラムダ式を組み立ててデリゲートとして実行する
static Func<int, int, int> AddByExpression(){ // 生成したい式 // (int x, int y) => x + y
// 引数 x の式 var x = Expression.Parameter(type: typeof(int)); // 引数 y の式 var y = Expression.Parameter(type: typeof(int)); // x + y の式 var add = Expression.Add (left: x, right: y); // (x, y) => x + y の式 var lambda = Expression.Lambda (add, x, y ); // ラムダ式をコンパイルしてデリゲートとして返す return (Func<int, int, int>)lambda.Compile();}
ソースコード参照
式木によるメタプログラミング
• デモ : AddSamplestatic Func<int, int, int> AddByExpression(){ // 生成したい式 // (int x, int y) => x + y
// 引数 x の式 var x = Expression.Parameter(type: typeof(int)); // 引数 y の式 var y = Expression.Parameter(type: typeof(int)); // x + y の式 var add = Expression.Add (left: x, right: y); // (x, y) => x + y の式 var lambda = Expression.Lambda (add, x, y ); // ラムダ式をコンパイルしてデリゲートとして返す return (Func<int, int, int>)lambda.Compile();}
ソースコード参照
メタプログラミングの実行速度 : Demo
• 生成速度の比較1. Reflection.Emit による Add
2. 式木による Add
3. Roslyn による Add
• 実行速度の比較1. 静的な Add
2. Reflection.Emit による Add
3. 式木による Add
4. Roslyn による Add
メタプログラミングの実行速度 : 結果
• 生成速度の比較
メタプログラミングの実行速度 : 結果
• 実行速度の比較
式木によるメタプログラミング• デモ : CallSample// Expression ( 式 ) によるメソッド呼び出しメソッドの生成static Func<T, TResult> CallByExpression<T, TResult>(string methodName){ // 生成したい式の例 : // (T item) => item.methodName()
// 引数 item の式 var parameterExpression = Expression.Parameter(type: typeof(T), name: "item"); // item.methodName() の式 var callExpression = Expression.Call( instance: parameterExpression, method : typeof(T).GetMethod(methodName, Type.EmptyTypes) ); // item => item.methodName() の式 var lambda = Expression.Lambda(callExpression, parameterExpression); // ラムダ式をコンパイルしてデリゲートとして返す return (Func<T, TResult>)lambda.Compile();}
ソースコード参照
メタプログラミングの実行速度の改善
• 生成が遅い• 呼び出しの度に生成すると遅い
→ キャッシュで改善
生成したデリゲートのキャッシュ
• デリゲートのキャッシュを用意Dictionary<string, Delegate> methods = new Dictionary<string, Delegate>();
1.キャッシュにない場合は、デリゲートを生成してキャッシュに入れる
2.キャッシュ内のデリゲートを呼ぶ
メタプログラミングの実行速度の改善 : Demo• メソッド呼び出しの速度の比較
1. 静的なメソッド呼び出し
2. 動的なメソッド呼び出し1. リフレクション
2. dynamic
3. 動的にメソッドを生成して呼び出し - キャッシュ無し1. Reflection.Emit
2. 式木
3. Roslyn
4. 動的メソッドを生成して呼び出し - キャッシュ有り1. Reflection.Emit
2. 式木
3. Roslyn
メタプログラミングの実行速度 : 結果
• 生成速度の比較
メタプログラミングの実行速度 : 結果
• 実行速度の比較
メタプログラミングの実行速度の改善 : 結果
5. LINQ プロバイダー
LINQ プロバイダー
• LINQ to Xxx
• LINQ to SQL 、 LINQ to Entities
• LINQ to Outlook 等
• LINQ プロバイダまとめ• http://blog.jhashimoto.net/entry/20120616/1339806360
LINQ プロバイダーの作り方
• チュートリアル : IQueryable LINQ プロバイダーの作成 - MSDN
• http://msdn.microsoft.com/ja-jp/library/bb546158.aspx
• LINQ: Building an IQueryable provider series
• http://blogs.msdn.com/b/mattwar/archive/2008/11/18/linq-links.aspx
• Writing custom LINQ provider
• http://weblogs.asp.net/mehfuzh/writing-custom-linq-provider
LINQ プロバイダーの作り方
LINQ プロバイダー(IQueryProvider)
式
クエリー コンテキスト
クエリー(IQueryable)
式を解釈
LINQ プロバイダーの作り方
• IQueryable<T> を実装したクラスを作る
• IQueryProvider を実装したクラスを作る• IQueryable<T> なクラスの Provider に設定
• Execute の中で渡された式木 (Expression) を解析して、 IQueryable<T> を返す
LINQ プロバイダーの作り方
pubic class QueryProvider : IQueryProvider{ public IQueryable<TCollection> CreateQuery<TCollection>(Expression expression) { return new QueryableData<TCollection>(this, expression); }
IQueryable IQueryProvider.CreateQuery(Expression expression) { return null; }
public TResult Execute<TResult>(Expression expression) { return default(TResult); }
public object Execute(Expression expression) { // ここで式木を解釈して、 IEnumerable を作って返す }}
• デモ : ProviderSample ソースコード参照
LINQ プロバイダーの作り方
• IQueryProvider.Execute で式木を解釈
• ExpressionVisitor クラス• Visitor パターンで式木中の式を探索
LINQ プロバイダーを作ろうとしてみよう• デモ : ProviderSamplestatic void Main(){ IQueryable<int> query1 = new QueryableData<int>(new QueryProvider()); Console.WriteLine(query1.Expression);
IQueryable<int> query2 = query1.Where(x => x % 2 == 0); Console.WriteLine(query2.Expression);
IQueryable<int> query3 = query2.OrderBy(x => x); Console.WriteLine(query3.Expression);
IQueryable<int> query4 = query3.Select(x => x * x); Console.WriteLine(query4.Expression);
foreach (int item in query4) Console.WriteLine(item);}
ソースコード参照
LINQ プロバイダーを作ろうとしてみよう• ProviderSample の実行結果value(ProviderSample.QueryableData`1[System.Int32])
value(ProviderSample.QueryableData`1[System.Int32]).Where(x => ((x % 2) == 0))
value(ProviderSample.QueryableData`1[System.Int32]).Where(x => ((x % 2) == 0)).OrderBy(x => x)
value(ProviderSample.QueryableData`1[System.Int32]).Where(x => ((x % 2) == 0)).OrderBy(x => x).Select(x => (x * x))
1
1
2
3
5
8
13
21
34
LINQ プロバイダーの作り方
• 実際に値を取り出すまでは、 IQueryable<T> なオブジェクトの中に式木が組み上がっていくだけ
• 値を取り出すと、 IQueryProvider.Execute が呼ばれるので、その中で式木を解析
ExpressionVisitor で式を解析
• デモ : ExpressionVisitorSample
public class MyExpressionVisitor : ExpressionVisitor{ protected override Expression VisitBinary(BinaryExpression expression) { return base.VisitBinary(expression); }
protected override Expression VisitConstant(ConstantExpression expression) { return base.VisitConstant(expression); }
protected override Expression VisitMethodCall(MethodCallExpression expression) { return base.VisitMethodCall(expression); }
protected override Expression VisitParameter(ParameterExpression expression) { return base.VisitParameter(expression); }
…… 等々 ……}
ソースコード参照
ExpressionVisitor で式を解析
• ExpressionVisitorSample の実行結果☆ 式 (x, y) => x + y
二項演算 ((x + y)) - 右辺 : x, 左辺 : y, 型 : System.Int32
引数 (x) - 名前 : x, 型 : System.Int32
引数 (y) - 名前 : y, 型 : System.Int32
引数 (x) - 名前 : x, 型 : System.Int32
引数 (y) - 名前 : y, 型 : System.Int32
☆ 式 text => text.Contains(" 福 ")
メソッドコール (text.Contains(" 福 ")) - メソッド名 : Contains, 型 : System.Boolean
引数 (text) - 名前 : text, 型 : System.String
定数 (" 福 ") - 値 : 福 , 型 : System.String
引数 (text) - 名前 : text, 型 : System.String
LINQ to Twitter を作ろうとしてみよう
• デモ : LinqToTwitterSamplepublic class QueryableTimeline<TElement> : IOrderedQueryable<TElement>{ public IQueryProvider Provider { get; private set; } public Expression Expression { get; private set; }
public Type ElementType { get { return typeof(TElement); } }
public QueryableTimeline() { Provider = new TimelineQueryProvider(); Expression = Expression.Constant(this); }
…… 途中省略 ……}
ソースコード参照
まとめ
• .NET の式木を理解することで、LINQ についてより深く理解することができた