Без перегрузки соответствует делегату действие при выборе метода с помощью выражения - PullRequest
0 голосов
/ 20 июня 2019

Я пытаюсь создать свободный API, который позволяет выбирать методы класса. При попытке выбрать метод с параметром я получаю сообщение об ошибке:

No overload for 'MethodB' matches delegate 'Action'.

Похоже, что C # не определяет автоматически универсальный метод. Если я указываю универсальный тип в подписи вызова, он работает, но если опущен, это не так. Обычно, если бы я использовал эту технику для выбора свойств или полей, универсальный тип был бы определен автоматически, но я подозреваю, что C # (или Visual Studio) не может автоматически определить тип, потому что он заключен в Action<> .

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

public class Program
{

  public static void Main( string[] args )
  {
    var builder = new OperationBuilder<SomeClass>();
    builder.AddMethod( x => x.MethodA );
    builder.AddMethod( x => x.MethodB ); // Gives error
    builder.AddMethod<double>( x => x.MethodB ); // Works correctly
  }


  public class OperationBuilder<T>
    where T : class
  {
    private List<MethodInfo> methods;

    public OperationBuilder()
    {
      methods = new List<MethodInfo>();
    }

    public OperationBuilder<T> AddMethod( MethodInfo method )
    {
      methods.Add( method );
      return this;
    }

    public OperationBuilder<T> AddMethod( Expression<Func<T, Action>> expression )
      => AddMethod( expression.GetMethodInfo() );

    public OperationBuilder<T> AddMethod<T1>( Expression<Func<T, Action<T1>>> expression )
      => AddMethod( expression.GetMethodInfo() );

  }

  public class SomeClass
  {

    public void MethodA()
    {
    }

    public void MethodB( double value )
    {
    }

  }

}

public static class ExpressionExtensions
{
  public static MethodInfo GetMethodInfo<TClass>(
    this Expression<Func<TClass, Action>> expression )
    => GetMethodInfoInternal( expression );

  public static MethodInfo GetMethodInfo<TClass, T1>(
    this Expression<Func<TClass, Action<T1>>> expression )
    => GetMethodInfoInternal( expression );

  private static MethodInfo GetMethodInfoInternal( LambdaExpression expression )
  {
    if( !( expression.Body is UnaryExpression unary ) )
      throw new ArgumentException(
        "Expression is not unary.",
        nameof( expression ) );

    if( !( unary.Operand is MethodCallExpression methodCall ) )
      throw new ArgumentException(
        "Expression is not a method call.",
        nameof( expression ) );

    if( !( methodCall.Object is ConstantExpression constant )
      || !( constant.Value is MethodInfo methodInfo ) )
      throw new ArgumentException(
        "Expression does not contain a valid method reference.",
        nameof( expression ) );

    return methodInfo;
  }
}

1 Ответ

1 голос
/ 20 июня 2019

Указывая x.MethodB, вы фактически не предоставляете делегата напрямую, а, скорее, так называемую группу методов .Группы методов могут неявно преобразовываться в данный совместимый тип делегата .

Таким образом, компилятор сталкивается со следующей ситуацией при попытке вывести аргумент универсального типа T1вашего AddMethod<T1> метода.Чтобы иметь возможность вывести T1, компилятор должен знать конкретный тип делегата Action<T1>, который у вас под рукой.Но все, что дано компилятору, это группа методов, а не Action<double> делегат.

Чтобы иметь возможность (неявно) преобразовать группу методов в делегат, компилятору необходимо знать совместимый тип делегатагруппа методов должна быть преобразована в.Но компилятор мог бы знать этот тип делегата, только если бы он мог вывести T1, что опять-таки потребовало бы, чтобы он знал конкретный тип Action<T1>.Catch-22.

Аналогичная, но более простая проблема, иллюстрирующая ту же самую проблему:

static void SomeMethod<T1>(Action<T1> action) { }

var sc = new SomeClass();
SomeMethod(sc.MethodB); // compile error, type argument cannot be inferred

Здесь компилятор также не сможет вывести SomeMethod's введите параметр T1, поскольку ему присваивается группа методов, а не делегат Action<T1>.И он не может преуспеть в преобразовании группы методов в делегат, потому что для определения фактического конкретного типа Action<T1> для преобразования ему потребуется конкретный тип Action<T1> для вывода T1 из.

Ниже небольшой (глупый и непрактичный) пример, показывающий, что проблема не в выводе самого T1, а в отсутствии конкретного конкретного типа делегата Action<T1>, из которого можно вывести T1.Здесь, в лямбда-выражении дается только делегат Action<double>, что, в свою очередь, позволит компилятору выводить T1:

var sc = new SomeClass();
Action<double> d = sc.MethodB;
builder.AddMethod(x => d);    // Gives no error anymore

или, несколько менее глупо:

builder.AddMethod(x => (Action<double>) x.MethodB);    // Gives no error anymore
...