ラムダ式をプレーンなデリゲートパラメーターとして提供するときにキャストする必要がある理由


124

メソッドSystem.Windows.Forms.Control.Invoke(Delegate method)を取る

なぜこれがコンパイル時エラーになるのですか?

string str = "woop";
Invoke(() => this.Text = str);
// Error: Cannot convert lambda expression to type 'System.Delegate'
// because it is not a delegate type

しかし、これはうまくいきます:

string str = "woop";
Invoke((Action)(() => this.Text = str));

メソッドがプレーンなデリゲートを期待するとき?

回答:


125

ラムダ式はデリゲート型または式ツリーに変換できますが、どのデリゲート型を知っている必要があります。署名を知っているだけでは十分ではありません。たとえば、私が持っているとしましょう:

public delegate void Action1();
public delegate void Action2();

...

Delegate x = () => Console.WriteLine("hi");

によって参照されるオブジェクトの具体的なタイプは何になると思いますxか?はい、コンパイラ適切なシグネチャを持つ新しいデリゲート型を生成する可能性がありますが、それが役立つことはめったになく、エラーチェックの機会が少なくなります。

あなたが呼び出すために簡単にそれを作りたい場合Control.InvokeAction最も簡単な方法は、コントロールに拡張メソッドを追加します:

public static void Invoke(this Control control, Action action)
{
    control.Invoke((Delegate) action);
}

1
ありがとう-型なしは使用するのに間違った用語だったと思うので、質問を更新しました。
xyz、

1
これは非常にエレガントで成熟したソリューションです。名前は、(汎用デリゲートの代わりに)実際に呼び出しているものを示すように、おそらく "InvokeAction"と呼びますが、確かに私にとっては機能します:)
Matthias Hryniszak

7
「めったに役に立たない」という意見には同意しません。ラムダを使用してBegin / Invokeを呼び出す場合、デリゲートタイプが自動生成されているかどうかは気にしません。呼び出しを取得するだけです。デリゲート(基本型)を受け入れるメソッドは、どのような状況で具象型が何であるかを気にしますか?また、拡張メソッドの目的は何ですか?それは何も簡単にしません。
Tergiver、2011年

5
ああ!拡張メソッドを追加してみましたInvoke(()=>DoStuff)が、まだエラーが発生しました。問題は、暗黙の「this」を使用したことです。コントロールメンバー内から機能させるには、明示的にする必要がありますthis.Invoke(()=>DoStuff)
Tergiver、2011年

2
これを読んでいる人にとっては、C#への質問と回答:InvokeRequiredコードパターンの自動化が非常に役立つと思います。
Erik Philips

34

ラムダを何度もキャストするのはうんざりですか?

public sealed class Lambda<T>
{
    public static Func<T, T> Cast = x => x;
}

public class Example
{
    public void Run()
    {
        // Declare
        var c = Lambda<Func<int, string>>.Cast;
        // Use
        var f1 = c(x => x.ToString());
        var f2 = c(x => "Hello!");
        var f3 = c(x => (x + x).ToString());
    }
}

3
これはジェネリックの美しい使い方です。
Peter Wone、2011年

2
認めざるを得ませんが、これがなぜうまくいくのかを理解するのにしばらく時間がかかりました。鮮やかさ。残念ですが、今は使い物になりません。
ウィリアム

1
これの使い方を説明してもらえますか?これを理解するのは難しいですか?どうもありがとう。
shahkalpesh 2013年

これを読むことは言うまでもありませんが、ジョンスキートの答えよりもこの答えを好むと思います。
ポグリンディス2013

@shahkalpeshはそれほど複雑ではありません。このように見ると、Lambda<T>クラスにはと呼ばれるID変換メソッドがありCast、渡されたものをすべて返します(Func<T, T>)。これはLambda<T>として宣言されLambda<Func<int, string>>ているFunc<int, string>ため、to Castメソッドを渡した場合、この場合はFunc<int, string>なので、戻ります。TFunc<int, string>
nawfal 2014年

12

ユーザーがUIスレッドにマーシャリングしようとしているため、これが9分の1の時間で発生します。ここに怠惰な方法があります:

static void UI(Action action) 
{ 
  System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(action); 
}

これで型付けされたので、問題はなくなり(qvスキートの答え)、次のような非常に簡潔な構文になります。

int foo = 5;
public void SomeMethod()
{
  var bar = "a string";
  UI(() =>
  {
    //lifting is marvellous, anything in scope where the lambda
    //expression is defined is available to the asynch code
    someTextBlock.Text = string.Format("{0} = {1}", foo, bar);        
  });
}

ボーナスポイントについては、別のヒントをご覧ください。UIの場合はこれを実行しませんが、SomeMethodが完了するまでブロックする必要がある場合(たとえば、要求/応答I / O、応答の待機)は、WaitHandle(qv msdn WaitAll、WaitAny、WaitOne)を使用します。

AutoResetEventはWaitHandleの派生物であることに注意してください。

public void BlockingMethod()
{
  AutoResetEvent are = new AutoResetEvent(false);
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    are.Set();
  });      
  are.WaitOne(); //don't exit till asynch stuff finishes
}

そして、物事が絡まる可能性があるため、最後のヒント:WaitHandlesはスレッドを停止させます。これは彼らがすることになっていることです。ストールしているときに UIスレッドにマーシャリングしようとすると、アプリがハングします。この場合、(a)深刻なリファクタリングが必要です。(b)一時的なハックとして、次のように待つことができます。

  bool wait = true;
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    wait = false;
  });
  while (wait) Thread.Sleep(100);

3
個人的に答えが魅力的ではないという理由だけで答えを投票するという頬があるのは魅力的です。それが間違っていて、これを知っている場合は、何が悪いのかを言います。それができない場合は、反対票の根拠はありません。エピソードが間違っている場合は、「Baloney。See [correct response]」または「おそらく推奨される解決策ではない、[better stuff]を参照してください」
Peter Wone

1
はい、私はfrankenthreadstressです。とにかく、なぜそれが否決されたのか私にはわかりません。実際のコードは使用していませんが、これはUIクロススレッド呼び出しの素早い入門であると思いました。これは、あまり高く評価していなかったいくつかの点があり、間違いなく上を行き来するために+1しました。:)つまり、デリゲートを呼び出すためのすばやい方法を提供しました。待機する必要がある呼び出しのオプションを指定します。そして、UIスレッド地獄で立ち往生している誰かが少し制御を取り戻すための素晴らしい迅速な方法でそれをフォローアップします。正解です、私は+ <3も言うつもりです。:)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherCURRENTスレッドのディスパッチャーを返します。つまり、UIスレッドではないスレッドからこのメソッドを呼び出すと、コードはUIスレッドで実行されません。
BrainSlugs83

@ BrainSlugs83の良い点は、おそらくアプリがUIスレッドディスパッチャーへの参照をキャプチャし、それをグローバルにアクセス可能な場所に配置することです。誰かがそれに気づくのにこれほど長い時間がかかったのには驚きました!
Peter Wone

4

ピーター・ウォン。あなたはダマンです。あなたのコンセプトをもう少し詳しく見て、私はこれら2つの機能を思いつきました。

private void UIA(Action action) {this.Invoke(action);}
private T UIF<T>(Func<T> func) {return (T)this.Invoke(func);}

これら2つの関数をフォームアプリに配置し、バックグラウンドワーカーから次のように呼び出すことができます

int row = 5;
string ip = UIF<string>(() => this.GetIp(row));
bool r = GoPingIt(ip);
UIA(() => this.SetPing(i, r));

たぶん少し怠惰ですが、私はこのような場合に非常に便利になるワーカー完了関数を設定する必要はありません

private void Ping_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
  int count = this.dg.Rows.Count;
  System.Threading.Tasks.Parallel.For(0, count, i => 
  {
    string ip = UIF<string>(() => this.GetIp(i));
    bool r = GoPingIt(ip);
    UIA(() => this.SetPing(i, r));
  });
  UIA(() => SetAllControlsEnabled(true));
}

基本的に、GUI DataGridViewからいくつかのIPアドレスを取得し、pingを実行して、結果のアイコンを緑または赤に設定し、フォームのボタンを再度有効にします。はい、それはバックグラウンドワーカーの「parallel.for」です。はい、それは呼び出しオーバーヘッドの多くですが、短いリストでは無視でき、はるかにコンパクトなコードです。


1

@Andrey Naumovの答えに基づいてこれを構築しようとしました。これはわずかな改善かもしれません。

public sealed class Lambda<S>
{
    public static Func<S, T> CreateFunc<T>(Func<S, T> func)
    {
        return func;
    }

    public static Expression<Func<S, T>> CreateExpression<T>(Expression<Func<S, T>> expression)
    {
        return expression;
    }

    public Func<S, T> Func<T>(Func<S, T> func)
    {
        return func;
    }

    public Expression<Func<S, T>> Expression<T>(Expression<Func<S, T>> expression)
    {
        return expression;
    }
}

ここで、型パラメーターSは仮パラメーター(残りの型を推測するために最低限必要な入力パラメーター)です。これで、次のように呼び出すことができます。

var l = new Lambda<int>();
var d1 = l.Func(x => x.ToString());
var e1 = l.Expression(x => "Hello!");
var d2 = l.Func(x => x + x);

//or if you have only one lambda, consider a static overload
var e2 = Lambda<int>.CreateExpression(x => "Hello!");

同じクラスに対してAction<S>Expression<Action<S>>同じように追加のオーバーロードを持つことができます。以下のために他のデリゲートと表現タイプに建てられ、あなたのような別々のクラス記述する必要がありますLambdaLambda<S, T>Lambda<S, T, U>など

これの利点は、元のアプローチよりも優れています。

  1. 型指定が1つ少なくなります(仮パラメーターのみを指定する必要があります)。

  2. これにより、例に示すように、と言うFunc<int, T>ときだけでなく、任意のに対して自由に使用できます。Tstring

  3. すぐに表現をサポートします。以前のアプローチでは、次のようにタイプを再度指定する必要があります。

    var e = Lambda<Expression<Func<int, string>>>.Cast(x => "Hello!");
    
    //or in case 'Cast' is an instance member on non-generic 'Lambda' class:
    var e = lambda.Cast<Expression<Func<int, string>>>(x => "Hello!");

    式のため。

  4. 他のデリゲート(および式)型のクラスを拡張すると、上記と同様に扱いにくくなります。

    var e = Lambda<Action<int>>.Cast(x => x.ToString());
    
    //or for Expression<Action<T>> if 'Cast' is an instance member on non-generic 'Lambda' class:
    var e = lambda.Cast<Expression<Action<int>>>(x => x.ToString());

私のアプローチでは、型を1回だけ宣言する必要があります(Funcsの場合は1つ少なくなります)。


Andreyの答えを実装するもう1つの方法は、完全に一般的ではないようなものです

public sealed class Lambda<T>
{
    public static Func<Func<T, object>, Func<T, object>> Func = x => x;
    public static Func<Expression<Func<T, object>>, Expression<Func<T, object>>> Expression = x => x;
}

だから物事は次のように減少します:

var l = Lambda<int>.Expression;
var e1 = l(x => x.ToString());
var e2 = l(x => "Hello!");
var e3 = l(x => x + x);

これはタイピングがさらに少なくなりますが、特定のタイプセーフを失うので、これは価値がありません。


1

パーティーに少し遅れますが、このようにキャストすることもできます

this.BeginInvoke((Action)delegate {
    // do awesome stuff
});


0

XUnitとFluentアサーションを操作すると、このインライン機能を本当にクールな方法で使用することができました。

[Fact]
public void Pass_Open_Connection_Without_Provider()
{
    Action action = () => {
        using (var c = DbProviderFactories.GetFactory("MySql.Data.MySqlClient").CreateConnection())
        {
            c.ConnectionString = "<xxx>";
            c.Open();
        }
    };

    action.Should().Throw<Exception>().WithMessage("xxx");
}

[Fact]
public void Pass_Open_Connection_Without_Provider()
{
    ((Action)(() => {
        using (var c = DbProviderFactories.GetFactory("<provider>").CreateConnection())
        {
            c.ConnectionString = "<connection>";
            c.Open();
        }
    })).Should().Throw<Exception>().WithMessage("Unable to find the requested .Net Framework Data Provider.  It may not be installed.");
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.