9

私が現在取り組んでいるプロジェクトでは、Invoke メソッドを呼び出してラムダ式の引数を渡すときに、変数を使用してローカル スコープに取り込む必要がある多くの静的式があります。

今日は、クエリが期待する型とまったく同じパラメーターを持つ静的メソッドを宣言しました。そのため、同僚と私は、このメソッドをローカル スコープに持ち込まずにオブジェクト全体で呼び出す代わりに、クエリの Select ステートメントでプロジェクトを実行できるかどうかを調べていました。

そしてそれは働いた!しかし、その理由はわかりません。

このようなコードを想像してください

// old way
public static class ManyExpressions {
   public static Expression<Func<SomeDataType, bool> UsefulExpression {
      get {
         // TODO implement more believable lies and logic here
         return (sdt) => sdt.someCondition == true && false || true; 
      }
   }
}

public class ARealController : BaseController {

   /* many declarations of important things */

   public ARealClass( /* many ninjected in things */) {
      /* many assignments */
   }

   public JsonNet<ImportantDataResult> getSomeInfo(/* many useful parameter */) {

      var usefulExpression = ManyExpressions.UsefulExpression;

      // the db context is all taken care of in BaseController
      var result = db.SomeDataType
         .Where(sdt => usefulExpression.Invoke(sdt))
         .Select(sdt => new { /* grab important things*/ })
         .ToList();

      return JsonNet(result);
   }
}

そして、あなたはこれをするようになります!

// new way
public class SomeModelClass {

   /* many properties, no constructor, and very few useful methods */
   // TODO come up with better fake names
   public static SomeModelClass FromDbEntity(DbEntity dbEntity) {
      return new SomeModelClass { /* init all properties here*/ };
   }
}

public class ARealController : BaseController {

   /* many declarations of important things */

   public ARealClass( /* many ninjected in things */) {
      /* many assignments */
   }

   public JsonNet<SomeModelClass> getSomeInfo(/* many useful parameter */) {

      // the db context is all taken care of in BaseController
      var result = db.SomeDataType
         .Select(SomeModelClass.FromDbEntity) // TODO; explain this magic
         .ToList();

      return JsonNet(result);
   }
}

そのため、ReSharper がこれを行うように促すと (デリゲートが期待する型に一致するというこの条件が満たされないことが多いため、そうであるとは限りません)、メソッド グループに変換すると表示されます。メソッド グループはメソッドのセットであり、C# コンパイラは、メソッド グループを明示的に型指定された適切な LINQ プロバイダーのオーバーロードに変換することを処理できることを漠然と理解しています。なぜこれが正確に機能するのか。

何が起きてる?

4

4 に答える 4

32

わからないことを質問するのは素晴らしいことですが、問題は、相手がどの部分を理解していないのかを知るのが難しいことです。あなたが知っていることをたくさん教えて、実際にあなたの質問に答えないのではなく、ここで役に立てば幸いです.

Linq の前、式の前、ラムダの前、匿名デリゲートの前の時代に戻りましょう。

.NET 1.0 では、これらはありませんでした。ジェネリックすらありませんでした。代表者がいましたが。また、デリゲートは、関数ポインター (C、C++、またはそのような言語を知っている場合) または引数/変数としての関数 (Javascript またはそのような言語を知っている場合) に関連しています。

デリゲートを定義できます。

public delegate int MyDelegate(double someValue, double someOtherValue);

そして、それをフィールド、プロパティ、変数、メソッド引数の型として、またはイベントの基礎として使用します。

しかし、当時、デリゲートに実際に値を与える唯一の方法は、実際のメソッドを参照することでした。

public int CompareDoubles(double x, double y)
{
  if (x < y) return -1;
  return y < x ? 1 : 0;
}

MyDelegate dele = CompareDoubles;

dele.Invoke(1.0, 2.0)または省略形でそれを呼び出すことができdele(1.0, 2.0)ます。

現在、.NET にはオーバーロードがあるため、参照するものを複数持つことができますCompareDoubles。これは問題ではありません。たとえば、コンパイラは、もう一方をpublic int CompareDoubles(double x, double y, double z){…}割り当てることを意図していた可能性があることを認識できるため、あいまいではないからです。それでも、コンテキスト内では 2 つの引数を取り、 を返すメソッドを意味しますが、そのコンテキスト外では、その名前を持つすべてのメソッドのグループを意味します。CompareDoublesdeleCompareDoublesdoubleintCompareDoubles

したがって、私たちがそれを呼んでいるのはメソッドグループです。

現在、.NET 2.0 ではデリゲートに役立つジェネリックがあり、同時に C#2 では匿名メソッドもあり、これも便利です。2.0 の時点で、次のことができるようになりました。

MyDelegate dele = delegate (double x, double y)
{
  if (x < y) return -1;
  return y < x ? 1 : 0;
};

この部分は C#2 のシンタックス シュガーにすぎず、舞台裏にはまだメソッドが存在しますが、それには「言いようのない名前」 (.NET 名としては有効だが C# 名としては無効な名前) が含まれているため、C#名前が衝突することはありません)。ただし、よくあることですが、特定のデリゲートで一度だけ使用するためだけにメソッドを作成する場合は便利でした。

もう少し先に進むと、.NET 3.5 では、共変性と反変性 (デリゲートに最適)FuncActionデリゲート (多くの場合非常に似ているさまざまなデリゲートの束を持つのではなく、型に基づいて同じ名前を再利用するのに最適) とそれに沿ったものがあります。それに伴い、ラムダ式を持つ C#3 が登場しました。

これらは、ある用途では匿名メソッドに少し似ていますが、別の用途ではそうではありません。

そのため、次のことができません。

var func = (int i) => i * 2;

var割り当てられたものからそれが何を意味するかを理解しますが、ラムダは割り当てられたものからそれらが何であるかを理解するので、これはあいまいです。

次のことを意味します。

Func<int, int> func = i => i * 2;

その場合、次の省略形です。

Func<int, int> func = delegate(int i){return i * 2;};

これは、次のような省略形です。

int <>SomeNameImpossibleInC# (int i)
{
  return i * 2;
}
Func<int, int> func = <>SomeNameImpossibleInC#;

ただし、次のようにも使用できます。

Expression<Func<int, int>> func = i => i * 2;

これは次の省略形です。

Expression<Func<int, int>> func = Expression.Lambda<Func<int, int>>(
  Expression.Multiply(
    param,
    Expression.Constant(2)
  ),
  param
);

また、.NET 3.5 には、これらの両方を多用する Linq があります。実際、Expressions は Linq の一部と見なされ、System.Linq.Expressions名前空間にあります。ここで取得するオブジェクトは、実行方法ではなく、実行したいこと (パラメーターを取得して 2 倍し、結果を返す) の説明であることに注意してください。

現在、Linq は主に 2 つの方法で動作します。そしてIQueryable、そして。IQueryable<T>_ 前者は「プロバイダー」で使用される操作を定義し、「プロバイダーが行うこと」はそのプロバイダー次第であり、後者はメモリ内の値のシーケンスで同じ操作を定義します。IEnumerableIEnumerable<T>

ある場所から別の場所に移動できます。を を にIEnumerable<T>変換するIQueryable<T>AsQueryable、その列挙可能なラッパーが提供さIQueryable<T>IEnumerable<T>ます。IQueryable<T>IEnumerable<T>

列挙可能なフォームはデリゲートを使用します。仕組みの簡略化されたバージョンSelect(このバージョンでは省略されている最適化が多数あり、エラー チェックをスキップし、エラー チェックがすぐに行われるようにするために間接的にしています) は次のようになります。

public static IEnumerable<TResult> Select(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
  foreach(TSource item in source) yield return selector(item);
}

一方、クエリ可能なバージョンはExpression<TSource, TResult>、 への呼び出しを含む式の一部にすることで式ツリーを取得しSelect、クエリ可能なソースを取得して、その式をラップするオブジェクトを返します。つまり、queryable の呼び出しは、queryable の!Selectへの呼び出しを表すオブジェクトを返します。Select

それをどうするかは、プロバイダーによって異なります。データベース プロバイダーはそれらを SQL に変換し、列挙Compile()型は式を呼び出してデリゲートを作成し、上記の最初のバージョンに戻りますSelect

しかし、その歴史を考慮して、もう一度歴史をさかのぼってみましょう。ラムダは、式またはデリゲートのいずれかを表すことができます (式の場合はCompile()、同じデリゲートを取得できます)。デリゲートは、変数を介してメソッドを指す方法であり、メソッドはメソッド グループの一部です。これらはすべて、最初のバージョンではメソッドを作成してそれを渡すことによってのみ呼び出すことができたテクノロジーに基づいて構築されています。

ここで、単一の引数を取り、結果を持つメソッドがあるとしましょう。

public string IntString(int num) { return num.ToString(); }

次に、ラムダ セレクターで参照したとします。

Enumerable.Range(0, 10).Select(i => IntString(i));

デリゲートの匿名メソッドを作成するラムダがあり、その匿名メソッドは同じ引数と戻り値の型を持つメソッドを呼び出します。ある意味では、次のようになります。

public string MyAnonymousMethod(int i){return IntString(i);}

MyAnonymousMethodここでは少し無意味です。呼び出しIntString(i)て結果を返すだけなのでIntString、最初に呼び出してそのメソッドを切り取ってはいけません。

Enumerable.Range(0, 10).Select(IntString);

ラムダベースのデリゲートを取得してメソッド グループに変換することで、不必要な (ただし、デリゲートのキャッシュについては以下の注を参照してください) レベルの間接化を削除しました。したがって、ReSharper のアドバイスは「メソッド グループに変換する」ですが、それは言葉遣いです (私自身は ReSharper を使用していません)。

ただし、ここで注意することがあります。IQueryable<T>の Select は式のみを受け取るため、プロバイダーはそれを処理方法 (データベースに対する SQL など) に変換する方法を考え出すことができます。IEnumerable<T>の Select はデリゲートのみを受け取るため、デリゲートは .NET アプリケーション自体で実行できます。で前者から後者に移動できます (クエリ可能なものが実際にラップされた列挙可能である場合) がCompile()、後者から前者に移動することはできません: デリゲートを取得してそれをに変換する方法がありません。 SQL に変換できるものではない「このデリゲートを呼び出す」以外を意味する式。

ラムダ式を使用すると、一緒に使用するとi => i * 2式になり、一緒に使用するIQueryable<T>とデリゲートになりIEnumerable<T>ます。クエリ可能な式を優先するオーバーロード解決ルールのためです (型として両方を処理できますが、式の形式は最も派生したもので機能します)。タイプ)。明示的にデリゲートを指定したとしても、それをどこかに入力したためかFunc<>、メソッド グループから取得したかどうかにかかわらず、式を受け取るオーバーロードは使用できず、デリゲートを受け取るオーバーロードが使用されます。つまり、データベースに渡されるのではなく、その時点までの linq 式が「データベース部分」になり、それが呼び出され、残りの作業がメモリ内で行われます。

回避するのが最善の時間の 95% です。したがって、95% の確率で、データベースに基づくクエリで「メソッド グループに変換する」というアドバイスを受け取った場合、「うーん、これは実際にはデリゲートだ。なぜデリゲートなのか? 式に変更できますか?」と考える必要があります。 "。残りの 5% の時間だけ、「メソッド名を渡すだけで少し短くなる」と考えてください。(また、デリゲートの代わりにメソッド グループを使用すると、コンパイラが別の方法で実行できるデリゲートのキャッシュが妨げられるため、効率が低下する可能性があります)。

そこで、あなたがその過程で理解できなかった部分をカバーしたことを願っています。または、少なくともここには、指して「そこの部分、それは私が理解していない部分です」と言うことができる部分があることを願っています.

于 2016-02-16T01:54:36.187 に答える
1
Select(SomeModelClass.FromDbEntity)

これはEnumerable.Select、あなたが望むものではないものを使用します。これは、「クエリ可能な LINQ」から LINQ to オブジェクトに移行します。これは、データベースがこのコードを実行できないことを意味します。

.Where(sdt => usefulExpression.Invoke(sdt))

ここで、私はあなたが意味したと仮定します.Where(usefulExpression). これにより、クエリの基になる式ツリーに式が渡されます。LINQ プロバイダーは、この式を変換できます。

このような実験を行う場合は、SQL プロファイラーを使用して、どのような SQL がネットワーク上を通過するかを確認してください。クエリのすべての関連部分が翻訳可能であることを確認してください。

于 2016-02-15T23:00:10.417 に答える
1

私はあなたを失望させたくありませんが、魔法はまったくありません。そして、この「新しい方法」には細心の注意を払うことをお勧めします。

VS でホバリングして、常に関数の結果を確認してください。IQueryable<T>「継承」には と同じ名前の拡張メソッドIEnumerable<T>も含まれていることに注意してください。唯一の違いは、前者は で動作し、後者は. QueryableEnumerableExpression<Func<...>>Func<..>

Funcしたがって、 or method groupoverを使用するときはいつでもIQueryable<T>、コンパイラはEnumerableオーバーロードを選択し、暗黙のうちLINQ to EntitiesLINQ to Objectsコンテキストに切り替えます。しかし、この 2 つには大きな違いがあります。前者はデータベースで実行され、後者はメモリで実行されます。

重要なポイントは、コンテキストにできるだけ長くとどまることですIQueryable<T>。そのため、「古い方法」を優先する必要があります。たとえば、あなたの例から

.Where(sdt => sdt.someCondition == true && false || true)

また

.Where(ManyExpressions.UsefulExpression)

また

.Where(usefulExpression)

だがしかし

.Where(sdt => usefulExpression.Invoke(sdt))

そして決して

.Select(SomeModelClass.FromDbEntity)
于 2016-02-16T02:16:30.400 に答える