Чому повинен бути викладений лямбда-вираз, коли він подається як звичайний параметр Delegate


124

Візьміть метод System.Windows.Forms.Control.Invoke (метод delegate)

Чому це дає помилку часу компіляції:

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.Invokeз Actionнайпростіше зробити , це додати метод розширення для контролю:

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

1
Дякую - я оновив питання, тому що думаю, що нетипізований термін був неправильним для використання.
xyz

1
Це дуже елегантне та зріле рішення. Я б, мабуть, назвав це "InvokeAction", так що назва підказує, на що ми насправді звертаємось (замість загального делегата), але це, безумовно, працює для мене :)
Маттіас Гринішак

7
Я не погоджуюся з тим, що це "рідко корисно і ...". У випадку виклику Begin / Invoke з лямбда, вам, звичайно, не байдуже, чи автоматично видається тип делегата, ми просто хочемо зробити виклик. У якій ситуації метод, який приймає делегата (базовий тип), небайдужий, що таке конкретний тип? Також, яка мета методу розширення? Це не робить нічого простішим.
Тергівер

5
Ах! Я додав метод розширення і спробував Invoke(()=>DoStuff)і все-таки отримав помилку. Проблема полягала в тому, що я використав неявне "це". Для того, щоб змусити його працювати всередині елемента керування ви повинні бути чітко: this.Invoke(()=>DoStuff).
Тергівер

2
Для всіх, хто читає це, я думаю, що питання та відповіді на C #: Автоматизація коду InvokeRequired дуже корисні.
Ерік Філіпс

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
Це прекрасне використання дженериків.
Пітер Вон

2
Треба визнати, це знадобило мені деякий час, щоб зрозуміти, чому це працює. Блискуча. Шкода, що я зараз не маю для цього користі.
Вільям

1
Чи можете ви пояснити використання цього? Мені важко це зрозуміти? Дуже дякую.
shahkalpesh

Це коли-небудь читати це, не кажучи вже про це, але я думаю, що я віддаю перевагу цій відповіді Джону Скіту!
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>.
nawfal

12

Дев'яту десяту частину людей люди отримують це, тому що намагаються перейти на нитку інтерфейсу користувача. Ось лінивий шлях:

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

Тепер, коли він набраний, проблема відходить (qwer Skeet anwer), і у нас є цей дуже лаконічний синтаксис:

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

Для бонусних балів ось ще одна порада. Ви б не робили цього для інтерфейсу користувача, але у випадках, коли вам потрібен SomeMethod для блокування до його завершення (наприклад, введення / виведення запиту / відповіді, очікування відповіді), використовуйте 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 затягує нитку. Це те, що вони повинні робити. Якщо ви спробуєте перейти на потік інтерфейсу користувача, поки він зупиниться , ваша програма зависне. У цьому випадку (а) налагоджено деякий серйозний рефакторинг, і (б) як тимчасовий злом можна почекати так:

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

3
Мені здається захоплюючим, що у людей є щока, щоб проголосувати відповідь лише тому, що вони особисто не вважають це привабливим. Якщо це неправильно, і ви це знаєте, то скажіть, що з цим не так. Якщо ви не можете цього зробити, то у вас немає підстав для вступу в голову. Якщо це епічно неправильно, тоді скажіть щось на кшталт "Балоні. Див. [Правильна відповідь]" або, можливо, "Не рекомендується рішення, див. [Кращі речі]"
Пітер Вун

1
Так, я - франк-десятка; але все одно я не маю поняття, чому це було проголосовано; хоча я не використав фактичний код, я подумав, що це приємне швидке вступ до перехресних посилань користувальницького інтерфейсу, і в ньому є деякі речі, які я насправді не думав так kudos, безумовно, +1 для переходу вище та за його межі. :) Я маю на увазі, ви дали хороший швидкий метод для отримання делегатських викликів; ви надаєте можливість для дзвінків, на які потрібно чекати; і ви дотримуєтесь цього приємного швидкого способу, щоб хтось, хто застряг у печатній частині інтерфейсу користувача, отримати трохи контролю. Тонка відповідь, я теж скажу + <3. :)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherповерне диспетчер потоку CURRENT - тобто якщо ви викликаєте цей метод з потоку, який не є потоком UI, код не буде запускатися на потоці інтерфейсу.
BrainSlugs83

@ BrainSlugs83, найкраще, мабуть, найкраще, щоб програма захоплювала посилання на диспетчер потоків інтерфейсу користувача і розміщувала його десь у всьому світі. Я вражений, що пройшло так довго, щоб хтось це помітив!
Пітер Вун

4

Пітер Вон. ти да людина. Зробивши вашу концепцію трохи далі, я придумав ці дві функції.

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

Я розміщую ці дві функції у своєму додатку Form, і я можу телефонувати як фонові працівники, як це

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, пінг їх, встановіть отримані піктограми зеленим або червоним кольором та повторно ввімкніть кнопки форми. Так, це "паралель. Для" в фоновому режимі. Так, це МНОГО виклику накладних витрат, але це мізерно мало для коротких списків та набагато більш компактний код.


1

Я намагався будувати це на відповіді @Andrey Наумова . Можливо, це незначне поліпшення.

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>>того ж класу. Для інших вбудованого делегата і вирази типів, вам доведеться писати окремі класи , як 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).


Ще один спосіб втілити відповідь Андрія - це не так, щоб вийти не загальним

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, цього не варто.


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.");
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.