เหตุใดจึงต้องแคสต์นิพจน์แลมบ์ดาเมื่อระบุเป็นพารามิเตอร์ Delegate ธรรมดา


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

เมื่อเมธอดคาดว่าจะมี Delegate ธรรมดา?

คำตอบ:


125

นิพจน์แลมบ์ดาก็จะสามารถแปลงเป็นประเภทผู้ร่วมประชุมหรือต้นไม้แสดงออก - แต่มันได้ทราบซึ่งประเภทของผู้ร่วมประชุม แค่รู้ลายเซ็นไม่เพียงพอ ตัวอย่างเช่นสมมติว่าฉันมี:

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

...

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

คุณคาดหวังว่าวัตถุที่เป็นรูปธรรมที่อ้างถึงxจะเป็นอย่างไร? ใช่คอมไพลเลอร์สามารถสร้างประเภทผู้รับมอบสิทธิ์ใหม่พร้อมลายเซ็นที่เหมาะสม แต่แทบไม่มีประโยชน์และคุณมีโอกาสน้อยลงในการตรวจสอบข้อผิดพลาด

ถ้าคุณต้องการที่จะทำให้มันง่ายต่อการเรียกร้องControl.InvokeกับActionสิ่งที่ง่ายที่สุดที่จะทำคือการเพิ่มวิธีขยายการควบคุม:

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

1
ขอบคุณ - ฉันอัปเดตคำถามเพราะคิดว่าการไม่พิมพ์เป็นคำที่ใช้ผิด
xyz

1
นั่นเป็นวิธีการแก้ปัญหาที่สง่างามและเป็นผู้ใหญ่มาก ฉันอาจจะเรียกมันว่า "InvokeAction" เพื่อให้ชื่อนั้นบ่งบอกถึงสิ่งที่เรากำลังเรียกใช้จริง ๆ (แทนที่จะเป็นตัวแทนทั่วไป) แต่มันก็ใช้ได้กับฉัน :)
Matthias Hryniszak

7
ฉันไม่เห็นด้วยที่มัน "ไม่ค่อยมีประโยชน์และ ... " ในกรณีของการโทร Begin / Invoke ด้วยแลมบ์ดาแน่นอนว่าคุณไม่สนใจว่าประเภทผู้รับมอบสิทธิ์จะถูกสร้างขึ้นโดยอัตโนมัติหรือไม่เราแค่ต้องการรับสายเท่านั้น วิธีการที่ยอมรับ Delegate (ประเภทฐาน) จะสนใจประเภทคอนกรีตในสถานการณ์ใด นอกจากนี้จุดประสงค์ของวิธีการขยายคืออะไร? มันไม่ได้ทำให้อะไรง่ายขึ้น
Tergiver

5
อา! ฉันเพิ่มวิธีการขยายและลองInvoke(()=>DoStuff)แล้วยังคงได้รับข้อผิดพลาด ปัญหาคือผมใช้ 'this' โดยนัย this.Invoke(()=>DoStuff)จะได้รับมันในการทำงานจากภายในเป็นสมาชิกควบคุมคุณจะต้องมีอย่างชัดเจน:
Tergiver

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

2
ฉันต้องยอมรับว่าฉันต้องใช้เวลาสักพักกว่าจะเข้าใจว่าทำไมมันถึงได้ผล สุกใส เสียดายตอนนี้ไม่มีประโยชน์
William

1
คุณช่วยอธิบายการใช้งานนี้ได้หรือไม่? มันยากสำหรับฉันที่จะเข้าใจสิ่งนี้? ขอบคุณมาก.
shahkalpesh

เคยอ่านเรื่องนี้นับประสาอะไร แต่ฉันคิดว่าฉันชอบคำตอบนี้สำหรับ Jon Skeet!
Pogrindis

@shahkalpesh มันไม่ซับซ้อนมาก ดูวิธีนี้Lambda<T>คลาสมีวิธีการแปลงเอกลักษณ์ที่เรียกว่าCastซึ่งส่งคืนสิ่งที่ส่งผ่าน ( Func<T, T>) ตอนนี้Lambda<T>ถูกประกาศเป็นLambda<Func<int, string>>ซึ่งหมายความว่าถ้าคุณผ่านFunc<int, string>ไปยังCastวิธีการก็จะส่งกลับFunc<int, string>กลับเนื่องจากในกรณีนี้คือT Func<int, string>
ว์ฟาล

12

เก้าในสิบของเวลาที่ผู้คนได้รับสิ่งนี้เนื่องจากพวกเขาพยายามที่จะจัดระเบียบให้กับเธรด UI นี่คือวิธีขี้เกียจ:

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

เมื่อพิมพ์แล้วปัญหาจะหายไป (ผู้อ้างอิงของ qv Skeet) และเรามีไวยากรณ์ที่รวบรัดมาก:

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 ในขณะที่คุณหยุดทำงานแอปของคุณจะหยุดทำงาน ในกรณีนี้ (ก) การปรับโครงสร้างใหม่อย่างจริงจังบางอย่างเป็นไปตามลำดับและ (b) เป็นการแฮ็กชั่วคราวคุณสามารถรอได้ดังนี้:

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

3
ฉันคิดว่ามันน่าสนใจที่ผู้คนมีแก้มในการโหวตคำตอบเพียงเพราะโดยส่วนตัวแล้วพวกเขาไม่พบว่ามันน่าสนใจ ถ้ามันผิดและคุณรู้เรื่องนี้ก็บอกว่ามีอะไรผิดพลาด หากคุณไม่สามารถทำได้แสดงว่าคุณไม่มีพื้นฐานในการโหวตลด ถ้ามันผิดมากให้พูดว่า "Baloney ดู [คำตอบที่ถูกต้อง]" หรือบางที "ไม่ใช่วิธีแก้ปัญหาที่แนะนำดู [สิ่งที่ดีกว่า]"
Peter Wone

1
ใช่ฉันเป็นนักแสดงหญิงอาชีพตรงไปตรงมา แต่อย่างไรก็ตามฉันไม่รู้ว่าทำไมมันถึงถูกโหวตลง แม้ว่าฉันจะไม่ได้ใช้โค้ดจริง แต่ฉันคิดว่านี่เป็นคำแนะนำที่รวดเร็วในการเรียกใช้ UI ข้ามเธรดและมันก็มีบางสิ่งที่ฉันไม่เคยนึกถึงความรุ่งโรจน์อย่างแน่นอน +1 สำหรับการก้าวไปข้างหน้า :) ฉันหมายความว่าคุณให้วิธีการที่รวดเร็วดีในการเรียกตัวแทน คุณให้ตัวเลือกสำหรับการโทรที่ต้องรอ และคุณติดตามด้วยวิธีที่รวดเร็วที่ดีสำหรับผู้ที่ติดอยู่ใน UI Thread Hell เพื่อให้สามารถควบคุมกลับมาได้ คำตอบที่ดีฉันจะพูดว่า + <3 เช่นกัน :)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherจะส่งคืนผู้มอบหมายงานของเธรด CURRENT นั่นคือถ้าคุณเรียกใช้เมธอดนี้จากเธรดที่ไม่ใช่เธรด UI โค้ดจะไม่ถูกรันบนเธรด UI
BrainSlugs83

@ BrainSlugs83 จุดที่ดีอาจเป็นสิ่งที่ดีที่สุดสำหรับแอปที่จะจับภาพการอ้างอิงไปยังโปรแกรมจัดการเธรด UI และวางไว้ในที่ที่สามารถเข้าถึงได้ทั่วโลก ฉันรู้สึกประหลาดใจที่มีคนสังเกตเห็นว่าใช้เวลานานขนาดนี้!
Peter Wone

4

Peter Wone คุณเป็นคนดา นำแนวคิดของคุณไปอีกเล็กน้อยฉันได้มาพร้อมกับฟังก์ชันทั้งสองนี้

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

ฉันวางฟังก์ชันทั้งสองนี้ไว้ในแอปแบบฟอร์มของฉันและฉันสามารถโทรจากผู้ทำงานเบื้องหลังได้เช่นนี้

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

โดยพื้นฐานแล้วรับที่อยู่ IP บางส่วนจาก gui DataGridView ping พวกเขาตั้งค่าไอคอนผลลัพธ์เป็นสีเขียวหรือสีแดงและปุ่มเปิดใช้งานใหม่ในแบบฟอร์ม ใช่มันเป็น "parallel.for" ใน backgroundworker ใช่มันเป็นการเรียกใช้ค่าใช้จ่ายจำนวนมาก แต่มีความสำคัญน้อยมากสำหรับรายการสั้น ๆ และโค้ดที่กะทัดรัดกว่ามาก


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

โดยที่พารามิเตอร์ type 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>>ในทำนองเดียวกัน สำหรับอื่น ๆที่สร้างขึ้นในผู้ร่วมประชุมและการแสดงออกประเภทคุณจะต้องเขียนแยกชั้นเรียนเช่นLambda, Lambda<S, T>, Lambda<S, T, U>ฯลฯ

ข้อดีของสิ่งนี้ที่ฉันเห็นจากแนวทางเดิม:

  1. ข้อกำหนดประเภทที่น้อยกว่าหนึ่งรายการ (ต้องระบุเฉพาะพารามิเตอร์ที่เป็นทางการเท่านั้น)

  2. ซึ่งให้อิสระแก่คุณในการใช้กับสิ่งใด ๆFunc<int, T>ไม่ใช่แค่Tพูดstringเมื่อใดดังที่แสดงในตัวอย่าง

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

ในแนวทางของฉันคุณต้องประกาศประเภทเพียงครั้งเดียว (ซึ่งน้อยเกินไปสำหรับFuncs)


อีกวิธีหนึ่งในการนำคำตอบของ Andrey ไปใช้ก็เหมือนกับว่าจะไม่เป็นแบบทั่วไป

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

แม้จะพิมพ์น้อยลง แต่คุณสูญเสียความปลอดภัยบางประเภทและimo ก็ไม่คุ้มค่า




0

เมื่อเล่นกับ XUnit และFluent Assertionsเป็นไปได้ที่จะใช้ความสามารถแบบอินไลน์นี้ในแบบที่ฉันคิดว่าเจ๋งจริงๆ

ก่อน

[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.