Mengapa ekspresi lambda harus dilemparkan ketika disediakan sebagai parameter Delegate biasa


124

Ambil metode System.Windows.Forms.Control.Invoke (metode Delegasi)

Mengapa ini memberikan kesalahan waktu kompilasi:

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

Namun ini berfungsi dengan baik:

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

Kapan metode mengharapkan Delegasi biasa?

Jawaban:


125

Ekspresi lambda dapat diubah menjadi tipe delegasi atau pohon ekspresi - tetapi harus mengetahui tipe delegasi yang mana . Hanya mengetahui tanda tangannya saja tidak cukup. Misalnya, saya memiliki:

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

...

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

Apa yang Anda harapkan dari jenis konkrit dari benda yang dirujuk x? Ya, kompilator dapat menghasilkan tipe delegasi baru dengan tanda tangan yang sesuai, tetapi itu jarang berguna dan Anda memiliki lebih sedikit kesempatan untuk memeriksa kesalahan.

Jika Anda ingin membuatnya mudah untuk panggilan Control.Invokedengan Actionhal termudah untuk melakukannya adalah menambahkan metode ekstensi ke Control:

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

1
Terima kasih - Saya memperbarui pertanyaan karena menurut saya istilah yang tidak diketik adalah istilah yang salah.
xyz

1
Itu adalah solusi yang sangat elegan dan matang. Saya mungkin akan menyebutnya "InvokeAction" sehingga nama tersebut menyarankan apa yang sebenarnya kami panggil (bukan delegasi umum) tetapi pasti berhasil untuk saya :)
Matthias Hryniszak

7
Saya tidak setuju bahwa ini "jarang berguna dan ...". Dalam kasus memanggil Begin / Invoke dengan lambda, Anda tentu tidak peduli jika tipe delegasi dibuat secara otomatis, kami hanya ingin membuat panggilan tersebut. Dalam situasi apa metode yang menerima seorang Delegasi (tipe dasar) peduli tipe konkretnya? Juga, apa tujuan dari metode penyuluhan? Itu tidak membuat segalanya lebih mudah.
Tergiver

5
Ah! Saya menambahkan metode ekstensi dan mencobaInvoke(()=>DoStuff) dan masih mendapat kesalahan. Masalahnya adalah saya menggunakan 'ini' yang tersirat. Untuk mendapatkannya bekerja dari dalam anggota Kontrol Anda harus eksplisit: this.Invoke(()=>DoStuff).
Tergiver

2
Bagi orang lain yang membaca ini, saya pikir pertanyaan & jawaban untuk C #: Mengotomatiskan pola kode InvokeRequired sangat membantu.
Erik Philips

34

Bosan melempar lambda berulang kali?

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
Itu penggunaan obat generik yang bagus.
Peter Wone

2
Harus saya akui, saya butuh beberapa saat untuk mencari tahu mengapa ini berhasil. Cemerlang. Sayang sekali saya tidak berguna untuk itu sekarang.
William

1
Bisakah Anda menjelaskan penggunaan ini? Sulit bagiku untuk memahami ini? Terima kasih banyak.
shahkalpesh

Ini untuk pernah membaca ini apalagi mengatakannya tetapi saya pikir saya lebih suka jawaban ini untuk Jon Skeet's!
Pogrindis

@shahkalpesh itu tidak terlalu kompleks. Lihat dengan cara ini, Lambda<T>kelas memiliki metode konversi identitas yang dipanggil Cast, yang mengembalikan apa pun yang diteruskan ( Func<T, T>). Sekarang Lambda<T>dideklarasikan sebagai Lambda<Func<int, string>>yang berarti jika Anda meneruskan metode Func<int, string>ke Cast, itu Func<int, string>kembali, karena Tdalam kasus ini adalah Func<int, string>.
nawfal

12

Sembilan persepuluh dari waktu orang mendapatkan ini karena mereka mencoba untuk marshal ke thread UI. Inilah cara malas:

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

Sekarang setelah diketik, masalahnya hilang (qv Skeet's anwer) dan kami memiliki sintaks yang sangat ringkas ini:

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);        
  });
}

Untuk poin bonus, inilah tip lainnya. Anda tidak akan melakukan ini untuk hal-hal UI tetapi dalam kasus di mana Anda memerlukan SomeMethod untuk memblokir hingga selesai (misalnya, permintaan / respons I / O, menunggu respons) gunakan WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Perhatikan bahwa AutoResetEvent adalah turunan 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
}

Dan tip terakhir karena hal-hal bisa menjadi kusut: WaitHandles menghentikan utas. Inilah yang seharusnya mereka lakukan. Jika Anda mencoba untuk melakukan marshal ke thread UI saat Anda menghentikannya , aplikasi Anda akan hang. Dalam kasus ini (a) beberapa pemfaktoran ulang serius sedang dilakukan, dan (b) sebagai peretasan sementara Anda dapat menunggu seperti ini:

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

3
Saya merasa sangat menarik bahwa orang memiliki pipi untuk memilih jawaban hanya karena mereka secara pribadi tidak menganggapnya menarik. Jika itu salah dan Anda mengetahuinya, katakan apa yang salah dengannya. Jika Anda tidak dapat melakukannya, Anda tidak memiliki dasar untuk memberi suara negatif. Jika benar-benar salah, katakan sesuatu seperti "Baloney. Lihat [tanggapan yang benar]" atau mungkin "Bukan solusi yang disarankan, lihat [hal yang lebih baik]"
Peter Wone

1
Ya, saya adalah frankenthreadstress; tapi bagaimanapun saya tidak tahu mengapa itu ditolak; meskipun saya belum pernah menggunakan kode yang sebenarnya, saya pikir ini adalah pengantar singkat yang bagus untuk pemanggilan lintas utas UI, dan ada beberapa hal yang tidak benar-benar saya pikirkan sebagai pujian, pasti +1 untuk melangkah lebih jauh. :) Maksud saya, Anda memberikan metode cepat yang bagus untuk membuat panggilan delegasi; Anda memberikan opsi untuk panggilan yang harus ditunggu; dan Anda menindaklanjutinya dengan cara cepat yang bagus bagi seseorang yang terjebak di UI Thread Hell untuk mendapatkan sedikit kendali kembali. Jawaban yang bagus, saya akan mengatakan + <3 juga. :)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherakan mengembalikan operator utas CURRENT - yaitu jika Anda memanggil metode ini dari utas yang bukan utas UI, kode tidak akan dijalankan pada utas UI.
BrainSlugs83

@ BrainSlugs83 poin yang bagus, mungkin hal terbaik adalah aplikasi menangkap referensi ke UI thread dispatcher dan meletakkannya di suatu tempat yang dapat diakses secara global. Saya kagum butuh waktu lama bagi seseorang untuk memperhatikan itu!
Peter Wone

4

Peter Wone. kamu adalah seorang pria. Mengambil konsep Anda sedikit lebih jauh, saya menemukan dua fungsi ini.

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

Saya menempatkan dua fungsi ini ke dalam aplikasi Formulir saya, dan saya dapat melakukan panggilan dari pekerja latar belakang seperti ini

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

Mungkin agak malas, tetapi saya tidak perlu menyiapkan fungsi yang dikerjakan oleh pekerja, yang sangat berguna dalam kasus seperti ini

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));
}

Pada dasarnya, dapatkan beberapa alamat ip dari Gui DataGridView, ping mereka, atur ikon yang dihasilkan menjadi hijau atau merah, dan aktifkan kembali tombol pada formulir. Ya, ini adalah "parallel.for" pada pekerja latar belakang. Ya, ini BANYAK meminta overhead, tetapi dapat diabaikan untuk daftar pendek, dan kode yang jauh lebih ringkas.


1

Saya mencoba membangun ini berdasarkan jawaban @Andrey Naumov . Mungkin ini sedikit perbaikan.

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;
    }
}

Di mana parameter tipe Sadalah parameter formal (parameter input, yang minimum diperlukan untuk menyimpulkan tipe lainnya). Sekarang Anda bisa menyebutnya seperti:

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!");

Anda dapat memiliki kelebihan beban tambahan untuk Action<S>dan Expression<Action<S>>serupa di kelas yang sama. Untuk lainnya dibangun dalam mendelegasikan dan ekspresi jenis, Anda akan harus menulis kelas terpisah seperti Lambda, Lambda<S, T>, Lambda<S, T, U>dll

Keuntungan dari ini saya lihat dari pendekatan aslinya:

  1. Satu spesifikasi tipe yang lebih sedikit (hanya parameter formal yang perlu ditentukan).

  2. Yang memberi Anda kebebasan untuk menggunakannya terhadap apa pun Func<int, T>, tidak hanya ketika Tdikatakan string, seperti yang ditunjukkan dalam contoh.

  3. Mendukung ekspresi langsung. Dalam pendekatan sebelumnya Anda harus menentukan tipe lagi, seperti:

    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!");

    untuk ekspresi.

  4. Memperluas kelas untuk tipe delegasi (dan ekspresi) lain juga merepotkan seperti di atas.

    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());

Dalam pendekatan saya, Anda harus mendeklarasikan tipe hanya sekali (itu terlalu satu kurang untuk Funcs).


Satu cara lain untuk mengimplementasikan jawaban Andrey adalah seperti tidak sepenuhnya generik

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;
}

Jadi hal-hal berkurang menjadi:

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

Itu bahkan lebih sedikit mengetik, tetapi Anda kehilangan keamanan jenis tertentu, dan imo, ini tidak sepadan.


1

Agak terlambat ke pesta tetapi Anda juga bisa melakukan cast seperti ini

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


0

Bermain dengan XUnit dan Fluent Assertions adalah mungkin untuk menggunakan kemampuan inline ini dengan cara yang menurut saya sangat keren.

Sebelum

[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");
}

Setelah

[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.");
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.