なぜFunc <T>ではなくExpression <Func <T >>を使用するのですか?


949

私はラムダとを理解しています FuncそしてActionデリゲートます。しかし、表現は私を困らせます。

どのような状況Expression<Func<T>>で、平凡な古いものではなく使用しFunc<T>ますか?


14
Func <>はc#コンパイラレベルのメソッドに変換されます。Expression<Func <>>は、コードを直接コンパイルした後、MSILレベルで実行されます。これが、高速である理由です
Waleed AK

1
回答に加えて、csharp言語仕様「4.6式ツリータイプ」は相互参照に役立ちます
djeikyb

回答:


1133

ラムダ式を式ツリーとして扱い、実行するのではなく内部を調べたい場合。たとえば、LINQ to SQLは式を取得して同等のSQLステートメントに変換し、(ラムダを実行するのではなく)サーバーに送信します。

概念的には、Expression<Func<T>>ある全く違うからFunc<T>Func<T>delegateメソッドへのほとんどのポインタであり、ラムダ式のツリーデータ構造Expression<Func<T>>示します。このツリー構造は、ラムダ式が実際のことを行うのでなく、何を行うかを示します。これは基本的に、式、変数、メソッド呼び出しなどの構成に関するデータを保持します(たとえば、このラムダは定数+パラメータなどの情報を保持します)。この説明を使用して、それを実際のメソッドに変換(または)したり、他の処理(LINQ to SQLの例など)を使用したりできます。ラムダを匿名メソッドおよび式ツリーとして扱う行為は、純粋にコンパイル時のことです。Expression.Compile

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

何も取得せずに10を返すILメソッドに効果的にコンパイルされます。

Expression<Func<int>> myExpression = () => 10;

パラメータを取得せず、値10を返す式を記述するデータ構造に変換されます。

式vs関数 大きな画像

どちらもコンパイル時には同じように見えますが、コンパイラが生成するものはまったく異なります。


96
つまり、an Expressionには、特定のデリゲートに関するメタ情報が含まれています。
bertl 2015

40
@bertl実際には違います。デリゲートはまったく関与していません。デリゲートとの関連付けがまったく存在しない理由は、式デリゲートにコンパイルできます。つまり、より正確に言えば、式メソッドにコンパイルし、そのデリゲートを戻り値として取得します。しかし、式ツリー自体は単なるデータです。Expression<Func<...>>だけではなく使用する場合、デリゲートは存在しませんFunc<...>
Luaan、2015年

5
@Kyle Delaneyの(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }ような式はExpressionTreeであり、Ifステートメントのブランチが作成されます。
マッテオマルチャー

3
@bertl DelegateはCPUが認識するもの(1つのアーキテクチャの実行可能コード)であり、Expressionはコンパイラが認識するものです(単なる別の形式のソースコードですが、ソースコードです)。
codewarrior 2017

5
@bertl:式はfuncに対するものであり、文字列に対するstringbuilderが何であるかを示すことで、より正確に要約できます。文字列/関数ではありませんが、要求されたときにデータを作成するために必要なデータが含まれています。
2018年

337

初心者向けの回答を追加しているのは、これらの回答がどれほど単純であるかを理解するまで、頭に浮かんだためです。時々、複雑であるために「頭を包む」ことができないのはあなたの期待です。

LINQ-to-SQLを一般的に使用しようとする本当に煩わしい「バグ」に踏み込むまで、違いを理解する必要はありませんでした。

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

より大きなデータセットでOutofMemoryExceptionsを取得し始めるまで、これはうまくいきました。ラムダ内にブレークポイントを設定すると、ラムダ条件に一致するものを探すためにテーブルの各行を1つずつ繰り返し処理していることに気付きました。LINQ-to-SQLを想定されているように実行するのではなく、データテーブルを巨大なIEnumerableとして扱うのはなぜですか?また、LINQ-to-MongoDbの対応物でもまったく同じことを行っていました。

修正は単にに変わるFunc<T, bool>ことでしたExpression<Func<T, bool>>ので、なぜのExpression代わりにが必要なのかをググって、Funcここで終わりました。

式は単にデリゲートをそれ自体に関するデータに変換します。したがってa => a + 1、「左側にint a。があります。右側に1を追加します」のようなものになります。それでおしまい。もう帰れます。それは明らかにそれよりも構造化されていますが、それは本質的にすべての式ツリーが本当にすべてであり、頭を包むものは何もありません。

それを理解すると、LINQ-to-SQLがを必要Expressionとし、a Funcが適切でない理由が明らかになります。FuncSQL / MongoDb /その他のクエリに変換する方法の要点を確認するために、それ自体を取得する方法は含まれていません。それが加算、乗算、減算のどちらを実行しているかはわかりません。あなたができることはそれを実行することだけです。Expression一方、では、デリゲートの内部を見て、やりたいことをすべて見ることができます。これにより、デリゲートをSQLクエリのように好きなように変換できます。FuncDbContextがラムダ式の内容を知らないため、機能しませんでした。このため、ラムダ式をSQLに変換できませんでした。しかし、それは次善の策であり、テーブルの各行を条件として繰り返しました。

編集:ジョン・ピーターの要求で私の最後の文章を説明する:

IQueryableはIEnumerableを拡張するため、IEnumerableのメソッドはWhere()、を受け入れるオーバーロードを取得しますExpression。にを渡すExpressionと、結果としてIQueryableが保持されますが、を渡すFuncと、ベースIEnumerableにフォールバックし、結果としてIEnumerableを取得します。言い換えると、気づかずに、データセットをクエリするのではなく、反復するリストに変換しました。実際に署名を確認するまで、違いに気づくことは困難です。


2
チャド; このコメントをもう少し説明してください:「DbContextがラムダ式に含まれているものをブラインドしてSQLに変換するため、Funcは機能しませんでした。次の最良のことを行い、テーブルの各行を条件として繰り返しました。 」
John Peters、

2
>> Func ...実行できるのはそれを実行することだけです。正確ではありませんが、強調すべき点だと思います。関数/アクションを実行し、式を分析します(実行前、または実行する代わりに)。
コンスタンティン

@Chadここでの問題はそれでしたか?:db.Set <T>がすべてのデータベーステーブルを照会しました。 。このコードはテーブル全体をメモリにロードしようとした(そしてもちろんオブジェクトを作成した)ため、OutOfMemoryExceptionが発生すると思います。私は正しいですか?ありがとう:)
ベンスヴェガート2018年

104

Expression vs Funcを選択する際の非常に重要な考慮事項は、LINQ to EntitiesのようなIQueryableプロバイダーは、式で渡すものを「ダイジェスト」できますが、Funcで渡すものは無視するということです。この件に関して2つのブログ投稿があります。

エンティティフレームワークを使用 した式とFuncの詳細とLINQを使用して恋に落ちる-パート7:式と関数(最後のセクション)


+ l説明。ただし、「LINQ式ノードタイプ「呼び出し」はLINQ to Entitiesでサポートされていません。」と表示されます。結果をフェッチした後でForEachを使用する必要がありました。
ティムタム2013

77

私は、違いについていくつかのメモを追加したいFunc<T>Expression<Func<T>>

  • Func<T> 普通の昔ながらのMulticastDelegateです。
  • Expression<Func<T>> 式ツリーの形式でのラムダ式の表現です。
  • 式ツリーは、ラムダ式構文またはAPI構文を使用して構築できます。
  • 式ツリーをデリゲートにコンパイルできます Func<T>
  • 逆変換は理論的には可能ですが、一種の逆コンパイルです。単純なプロセスではないため、そのための組み込み機能はありません。
  • 式ツリーは、を介して観察/翻訳/修正することができます ExpressionVisitor
  • IEnumerableの拡張メソッドは、 Func<T>ます。
  • IQueryableの拡張メソッドはで動作しExpression<Func<T>>ます。

コードサンプルで詳細を説明する記事があります:
LINQ:Func <T>対Expression <Func <T >>

お役に立てれば幸いです。


いいリストですが、小さな変換の1つとして、逆変換は可能ですが、正確な逆変換はできないと述べています。一部のメタデータは、変換プロセス中に失われます。ただし、それを式ツリーに逆コンパイルすると、再度コンパイルしたときに同じ結果が得られます。
アイディアカピ2015年

76

Krzysztof Cwalinaの本(Framework Design Guidelines:Conventions、Idioms、and Patterns for Reusable .NET Libraries)から、より哲学的な説明があります。

リコ・マリアーニ

非イメージバージョンの編集:

ほとんどの場合、コードを実行するだけでFuncまたはActionが必要になります。実行前にコードを分析、シリアル化、または最適化する必要がある場合は、が必要です。はコード、Func / Actionを考えるためのものですはを実行するためのものです。


10
よく置きます。すなわち。Funcが何らかのクエリに変換されることを期待している場合は、式が必要です。つまり。database.data.Where(i => i.Id > 0)として実行する必要がありますSELECT FROM [data] WHERE [id] > 0。Funcを渡すだけの場合、ドライバーにブラインドを配置し、ドライバーが実行できるすべてのことを実行します。SELECT *その後、そのデータがすべてメモリに読み込まれると、それぞれを反復処理し、id> 0のすべてをフィルターで除外しますFuncExpression権限を与えますを分析してFuncSql / MongoDb /その他のクエリに変換するドライバー。
Chad Hedgcock 16年

だから私が休暇を計画しているとき、私は使用しますExpressionが、休暇中はFunc/Action;)
GoldBishop

1
@ChadHedgcockこれは私が必要とした最後の作品でした。ありがとう。私はこれをしばらく見てきましたが、ここでのあなたのコメントはすべての研究をクリックしました。
ジョニー

37

LINQは標準的な例(たとえば、データベースとの対話)ですが、実際には、実際に行うのではなく、をすべきを表現することに常に関心があります。たとえば、protobuf-netの RPCスタックで(コード生成などを回避するために)このアプローチを使用しているため、次のようにメソッドを呼び出します。

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

これにより、解決する式ツリーSomeMethod(および各引数の値)が分解され、RPC呼び出しが実行され、ref/ outargsが更新され、リモート呼び出しの結果が返されます。これは、式ツリーを介してのみ可能です。これについては、ここで詳しく説明します

もう1つの例は、ジェネリックオペレーターコードによって行われるように、ラムダにコンパイルする目的で式ツリーを手動で構築する場合です。


20

関数をコードではなくデータとして扱いたい場合は、式を使用します。コードとして(データとして)操作したい場合は、これを行うことができます。ほとんどの場合、式が必要ない場合は、式を使用する必要はありません。


19

主な理由は、コードを直接実行するのではなく、調べたい場合です。これにはさまざまな理由が考えられます。

  • コードを別の環境にマッピングする(つまり、C#コードをEntity FrameworkのSQLにマッピングする)
  • 実行時のコードの一部の置き換え(動的プログラミング、または単純なDRY手法)
  • コード検証(スクリプトをエミュレートするとき、または分析を行うときに非常に役立ちます)
  • シリアル化-式はかなり簡単かつ安全にシリアル化できますが、デリゲートはできません
  • 本質的に強く型付けされていないものに対する強く型付けされた安全性、およびランタイムで動的呼び出しを実行している場合でもコンパイラーチェックを利用する(Razorを使用したASP.NET MVC 5は良い例です)

あなたは5番にもう少し詳しく説明することができます
uowzd01

@ uowzd01 Razorを見てください。このアプローチは広範囲にわたって使用されています。
Luaan

@Luaan私は式のシリアル化を探していますが、サードパーティによる限定的な使用なしでは何も見つけることができません。.Net 4.5は式ツリーのシリアル化をサポートしていますか?
vabii

@vabii私が知っていることではありません-そして、それは一般的なケースでは本当に良い考えではありません。私の要点は、事前に設計されたインターフェースに対して、サポートしたい特定のケースのためにかなり単純なシリアライゼーションを記述できることについてでした。一般的な場合、Expression任意の式に任意のデリゲート/メソッド参照の呼び出しを含めることができるため、デリゲートと同じようにシリアライズすることは不可能です。もちろん「簡単」は相対的です。
Luaan、2017

15

パフォーマンスについての回答はまだありません。パッシングFunc<>に秒Where()Count()悪いです。本当に悪い。あなたは使用している場合はFunc<>、それが呼び出すIEnumerable代わりにLINQのものIQueryableテーブル全体が中に引っ張られ得ることを意味し、その後、フィルタを。 Expression<Func<>>特に、別のサーバーにあるデータベースにクエリを実行する場合は、非常に高速です。


これはインメモリクエリにも適用されますか?
stt106 2018年

@ stt106おそらく違います。
mhenry1384

これは、リストを列挙する場合にのみ当てはまります。GetEnumeratorまたはforeachを使用する場合、ienumerableを完全にメモリにロードしません。
nelsontruran

1
@ stt106 List <>の.Where()句に渡されると、Expression <Func <>>はそれに呼び出される.Compile()を取得するため、Func <>はほぼ確実に高速です。referencesource.microsoft.com/#System.Core/System/Linq/…
NStuke
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.