Почему должно быть приведено лямбда-выражение, если оно предоставлено как простой параметр делегата


124

Возьмите метод System.Windows.Forms.Control.Invoke (метод делегата)

Почему это дает ошибку времени компиляции:

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 с лямбда-выражением вам, конечно, не важно, генерируется ли тип делегата автоматически, мы просто хотим, чтобы вызов был выполнен. В какой ситуации метод, принимающий делегат (базовый тип), будет заботиться о конкретном типе? Кроме того, какова цель метода расширения? Нет ничего проще.
Tergiver 07

5
Ах! Я добавил метод расширения и попытался, Invoke(()=>DoStuff)но все равно получил ошибку. Проблема заключалась в том, что я использовал неявное «это». Для того, чтобы заставить его работать внутри элемента управления вы должны быть четко: this.Invoke(()=>DoStuff).
Tergiver 07

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
Это прекрасное использование дженериков.
Peter Wone

2
Должен признаться, мне потребовалось некоторое время, чтобы понять, почему это сработало. Brilliant. Жаль, что это мне сейчас не нужно.
Уильям

1
Не могли бы вы объяснить, как это использовать? Мне сложно это понять? Большое спасибо.
shahkalpesh

Его нужно когда-либо читать, не говоря уже о том, чтобы сказать это, но я думаю, что предпочитаю этот ответ Джону Скиту!
Погриндис

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

Теперь, когда он напечатан, проблема исчезла (ответ Скита), и у нас есть очень сжатый синтаксис:

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
Да, я откровенная пряжка; но в любом случае я понятия не имею, почему он был отклонен; хотя я не использовал реальный код, я подумал, что это хорошее быстрое введение в межпоточные вызовы пользовательского интерфейса, и в нем есть некоторые вещи, о которых я действительно не думал, так что спасибо, определенно +1 за то, что сделал все возможное. :) Я имею в виду, вы дали хороший быстрый метод для вызова делегатов; вы даете возможность звонков, которые нужно дождаться; и вы следуете за этим хорошим быстрым способом для тех, кто застрял в UI Thread Hell, чтобы вернуть немного контроля. Прекрасный ответ, я тоже скажу + <3. :)
shelleybutterfly 09

System.Windows.Threading.Dispatcher.CurrentDispatcherвернет диспетчер ТЕКУЩЕГО потока - т.е. если вы вызовете этот метод из потока, который не является потоком пользовательского интерфейса, код не будет запущен в потоке пользовательского интерфейса.
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-адреса из графического интерфейса DataGridView, проверить их связь, установить для результирующих значков зеленый или красный цвет и повторно включить кнопки в форме. Да, это "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>>того же класса. Для других встроенных типов делегатов и выражений вам придется писать отдельные классы, например 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);

Это еще меньше, но вы теряете определенную безопасность типов, и я думаю, этого не стоит.


1

Немного поздно на вечеринку, но вы также можете использовать это

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


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.");
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.