Как передать Func <T>с переменным количеством параметров - PullRequest
0 голосов
/ 30 мая 2018

Итак, я пытаюсь передать Func с переменным числом параметров.

Что-то вроде:

public object GetValue<T>(string name, Func<object> func) { 
    var result = func.DynamicInvoke();
}

Приведенная выше функция / подпись отлично работает, когда числоаргументов к func это знать.Но он быстро ломается, когда вы хотите, чтобы число аргументов было неизвестно до времени выполнения.

Я бы хотел изменить сигнатуру метода, чтобы учесть следующие сценарии, без использования перегрузки метода:

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var result = GetValue("Joe", (i) => { return "Joe " + i.ToString(); });

// 2 arguments
var result = GetValue("Henry", (i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

Больше двух аргументов не нужно сейчас ... но может быть в будущем.Синтаксис вызова - самый важный бит для меня.Я бы предпочел, чтобы вызывающий не произнес ничего.

Я посмотрел на этот вопрос и ответы , но все они, кажется, требуют некоторого синтаксиса вызова, который я предпочел бы неиспользовать.

Есть идеи, как этого достичь?

Ответы [ 5 ]

0 голосов
/ 30 мая 2018

Во-первых, мой ответ не о том, следует ли вам это делать или нет;другие ответы на этот пост дали хорошо объясненные советы / мнения по этому поводу.

Мой ответ только о технической части C #: как передать переменное число аргументов в Func.

Это может быть сделано через делегата, которому переменное число аргументов может быть передано через params array и который «совместим» с Func.

. Из-за params array,Func должен быть первым аргументом.Обратите внимание, что я переместил аргумент 'name', как в вашем вопросе, чтобы он передавался через params array, чтобы он был доступен из Func.

Всем нижеприведенным строкам присваивается значение 'Bob Smith', будучи построенным с 0 или 2 аргументами.

public delegate T ParamsDelegate<T>(params Object[] args);

public T GetValue<T>(ParamsDelegate<T> f, params Object[] args)
{
    return f(args);            
}

// 0 passed argument values.  
String s0 = GetValue(args => "Bob Smith");

// 1 argument.
String s1 = GetValue(
    args => String.Format("{0} Smith", args),
    "Bob"
    );

// 2 arguments.            
String s2 = GetValue(
    args => String.Format("{0} {1}", args),
    "Bob", "Smith"
    );

Редактировать:

Начиная с C # 7 вы можете сделать намерение не передавать какие-либо аргументы более понятнымичерез Discards .

Discards - это локальные переменные, которые вы можете назначать, но не можете читать из них.то есть они являются локальными переменными «только для записи».У них нет имен, вместо этого они представлены как _ (подчеркивание.) _ Является контекстным ключевым словом, оно очень похоже на var, и _ не может быть прочитано (т.е. не может отображаться справа от назначения).

Итак, следующее:

String s0 = GetValue(args => "Bob Smith");

можно переписать как:

String s0 = GetValue(_ => "Bob Smith");
0 голосов
/ 30 мая 2018

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

Если у вас есть подробный список переменных, которые могут или не могут понадобиться передаваемому делегату, вы можете предоставить их какодин аргумент, возможно, с именем CallingContext.

class CallingContext
{
    private CallingContext(string name, int index)
    {
        Name = name;
        Index = index;
    }
    public string Name { get; private set; }
    public int Index { get; private set; }
}

public TOut GetValue<TOut>(string name, Func<CallingContext,TOut> func) { 
    var i = LookupIndex(name);
    var context = new CallingContext(name, i);        
    return func(context);
}

Таким образом, функция вызывающего может извлекать все, что ему нужно, и вам не нужно беспокоиться о правильных действиях или общих параметрахFunc, вообще.

var result = x.GetValue("Bob", ctxt => ctxt.Name + ctxt.Id.ToString());

В будущем, если вам придется добавить дополнительные параметры, просто добавьте их в класс CallingContext.Если вы ожидаете, что класс станет довольно богатым, рассмотрите возможность представления его в качестве интерфейса и использования подписи делегата Func<ICallingContext,TOut>, чтобы вы могли прокрутить и заглушить его для модульных тестов.Кроме того, если какой-либо из аргументов является дорогостоящим для вычисления, вы можете выставить их как ленивые свойства и извлекать их только при вызове.

0 голосов
/ 30 мая 2018

Насколько я понимаю, вы хотите иметь возможность игнорировать часть параметров в случае, если вызывающему абоненту они не нужны.Тем не менее, я думаю, вы должны начать с того, чтобы решить, как будет называться делегат.Обратите внимание, что число аргументов DynamicInvoke должно соответствовать количеству параметров фактического метода, поэтому оно не будет работать для всех передаваемых вами делегатов, если вы укажете больше аргументов, чем они принимают.

Если вы не используете рефлексию и emit для вызова делегата, вы должны использовать перегрузки.То, как вы их сделаете, зависит от того, будут ли аргументы «ленивыми» или нет.Если это так, тело каждой перегрузки будет существенно отличаться от других, чтобы оправдать свое присутствие.Если нет, то это выглядит так:

object GetValue(Func<int, object> f)
{
    return GetValue((i,s) => f(i));
}

object GetValue(Func<int, string, object> f)
{
    return f(1, "0");
}

Я не думаю, что это хорошее решение, так как первая перегрузка предполагает, что только первый аргумент выдан, в то время как под капотом все они переданы,В этом случае может быть лучше обернуть всю информацию, которую вы намереваетесь получить из метода в классе, и вместо этого передать его экземпляр.Это применимо и к первому случаю, если вы используете Lazy<T>, но код станет более сложным.

Еще одна вещь, относящаяся к вашей проблеме: существует синтаксис delegate{ ... }, который можно привести к делегатувведите с любыми параметрами, которые затем игнорируются.Это происходит на сайте вызова.

0 голосов
/ 30 мая 2018

Прежде всего, вы побеждаете цель статической типизации: в статической типизации весь смысл в том, что во время компиляции вы можете точно сказать, какой тип имеет определенный объект и как таковой компилятор будетпозволяют вам делать определенные вещи.То, что вы ожидаете здесь, это то, что обычно предлагает динамическая типизация, где вы можете просто передать «что-то», а затем вы можете выяснить во время выполнения, как это использовать.

Вы должны действительно переосмыслить это ваше требованиеи посмотрим, не можете ли вы решить эту проблему лучше, статически типизированным способом.


При этом существует несколько уродливый способ сделать это статически нормальным, используя пользовательский тип и некоторые неявные преобразования типов.(что происходит во время компиляции!):

public object GetValue(string name, DynamicFunc func)
{
    return func.DynamicInvoke("a", "b");
}

public class DynamicFunc
{
    public Func<object> None { get; private set; }
    public Func<object, object> One {get; private set;}
    public Func<object, object, object> Two { get; private set; }

    public object DynamicInvoke(object param1 = null, object param2 = null)
    {
        if (Two != null)
            return Two(param1, param2);
        else if (One != null)
            return One(param1 ?? param2);
        else if (None != null)
            return None();
        return null;
    }

    public static implicit operator DynamicFunc(Func<object> func)
        => new DynamicFunc { None = func };
    public static implicit operator DynamicFunc(Func<object, object> func)
        => new DynamicFunc { One = func };
    public static implicit operator DynamicFunc(Func<object, object, object> func)
        => new DynamicFunc { Two = func };
}

И затем вы можете использовать это так:

var result0 = GetValue("Bob", (Func<object>)(() => { return "Bob Smith"; }));
var result1 = GetValue("Joe", (Func<object, object>)((i) => { return "Joe " + i.ToString(); }));
var result2 = GetValue("Henry", (Func<object, object, object>)((i, e) =>
{
    return $"i: {i.ToString()}, e: {e.ToString()}";
}));

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

Выглядит ли это хорошо или облегчает понимание?Я так не думаю.Если вы хотите правильную статическую типизацию, просто используйте перегрузку метода здесь.Таким образом, вам также не нужно будет вызывать функцию динамически, что также упрощает ее вызов.

0 голосов
/ 30 мая 2018

Ответ не .Прежде всего, вы пытаетесь вызвать метод, который не принимает параметров и возвращает какой-либо объект.Вы не можете просто «сделать» функцию, которая требует параметры этого типа, иначе, как иначе вы будете вызывать ее с необходимыми параметрами.

Вы уже создаете лямбды, вам нужно закрыть«параметры», которые вы хотите таким образом, вы можете эффективно добавлять дополнительные параметры.

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var i = ...;
var result = GetValue("Joe", () => { return "Joe " + i.ToString(); });

// 2 arguments
var i = ...;
var e = ...;
var result = GetValue("Henry", () => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

В противном случае, если вы действительно хотите передать любой делегат с любым количеством параметров, установите параметр Delegate, но вы должен предоставить точный тип делегата, и вы должны предоставить аргументы для вызова.

public object GetValue<T>(string name, Delegate func, params object[] args) { 
    var result = func.DynamicInvoke(args);
}
var result = GetValue("Bob", new Func<object>(() => { return "Bob Smith"; }));

// 1 argument
var result = GetValue("Joe", new Func<T, object>((i) => { return "Joe " + i.ToString(); }), argI);

// 2 arguments
var result = GetValue("Henry", new Func<T1, T2, object>((i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
}), argI, argE);
...