通用类型推断,Fluent Api,具有预先声明的类型

时间:2018-02-04 05:26:58

标签: c# generics type-inference

我正在为 Fluent Api 开发一个相当可配置的服务,并且只是尝试为以下问题提供一个简洁的解决方案。

我有类似这样的课程

public class WindowVm : DialogResultBase<MyReturnType>

一切都很好,但任何人都可以想到一种方法来实现以下目的,而不必详细说明第二种通用​​类型,即

public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResult<TViewModel,TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>

我真的只对结果IDialogWithResult<TViewModel, TSomeReturnType>感兴趣,即使我必须在2个陈述中这样做

所以我可以打电话

.DialogWithResult<WindowVm>()

我知道所有信息都在那里并在编译时声明,我也知道这是 Partial Inference 及其全部或全部。但是我只是想知道是否有一些技巧而不必重新声明

.DialogWithResult<WindowVm, ResultType>(); 

此外,我有一个方法需要ResultType作为(你猜对了)结果类型

ResultType MyResult =  ...DialogWithResult<WindowVm, ResultType>()
                         .ShowModal(); 
  

我的意思是,ResultType在这一点上真的是多余的   游戏已由WindowVm宣布。如果,这将是很好的   消费者不必去寻找它(即使它意味着更多   超过一步)

2 个答案:

答案 0 :(得分:9)

是的,当您将TSomeReturnType作为WindowVm传递时,编译器会提供推断TViewModel类型的所有信息。但允许减少泛型(.DialogWithResult<WindowVm>())参数列表的主要障碍是它可能与具有相同名称但只有一个泛型类型参数的重载方法冲突。例如,如果您在类中有以下方法:

public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResult<TViewModel,TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>

public IDialogWithResult<TViewModel> DialogWithResult<TViewModel>(object owner = null)
        where TViewModel : DialogResultBase<MyReturnType>

编码.DialogWithResult<WindowVm>()时编译器应该调用哪一个?

这就是为什么在C#中可能不会引入这种简化语法的原因。

但是,您仍然可以选择使调用像.DialogWithResult<WindowVm>()一样简单。我不是这个解决方案的粉丝,但如果您的Fluent Api电话的简洁性非常重要,那么您可以使用它。该解决方案基于来自TSomeReturnType类型的TViewModel类型的反射和运行时提取:

public class YourClass
{
    public dynamic DialogWithResult<TViewModel>(object owner = null)
    {
        //  Searching for DialogResultBase<TSomeReturnType> in bases classes of TViewModel
        Type currType = typeof(TViewModel);
        while (currType != null && currType != typeof(DialogResultBase<>))
        {
            if (currType.IsGenericType && currType.GetGenericTypeDefinition() == typeof(DialogResultBase<>))
            {
                break;
            }

            currType = currType.BaseType;
        }
        if (currType == null)
        {
            throw new InvalidOperationException($"{typeof(TViewModel)} does not derive from {typeof(DialogResultBase<>)}");
        }

        Type returnValueType = currType.GetGenericArguments()[0];

        //  Now we know TViewModel and TSomeReturnType and can call DialogWithResult<TViewModel, TSomeReturnType>() via reflection.
        MethodInfo genericMethod = GetType().GetMethod(nameof(DialogWithResultGeneric));
        if (genericMethod == null)
        {
            throw new InvalidOperationException($"Failed to find {nameof(DialogWithResultGeneric)} method");
        }

        MethodInfo methodForCall = genericMethod.MakeGenericMethod(typeof(TViewModel), returnValueType);
        return methodForCall.Invoke(this, new [] { owner } );
    }

    public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResultGeneric<TViewModel, TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>
    {
        // ...
    }
}

我们使用DialogWithResult<TViewModel>()的一个泛型类型参数声明了新的TViewModel方法。然后我们搜索基类DialogResultBase<T>类。如果找到,我们会通过TSomeReturnType调用提取Type.GetGenericArguments()的类型。最后通过反射调用原始的DialogWithResultGeneric<TViewModel, TSomeReturnType>方法。请注意,我已将原始方法重命名为DialogWithResultGeneric,以便GetMethod()不会抛出AmbiguousMatchException

现在,在您的程序中,您可以将其命名为:

.DialogWithResult<WindowVm>()

缺点是没有什么能阻止你在错误的类型上调用它(一个不会从DialogResultBase<T>继承):

.DialogWithResult<object>()

在这种情况下,您不会收到编译错误。只有在抛出异常的运行时才会识别该问题。您可以使用此answer中描述的技术解决此问题。简而言之,您应该声明非通用DialogResultBase并将其设置为DialogResultBase<T>的基础:

public abstract class DialogResultBase
{
}

public class DialogResultBase<T> : DialogResultBase
{
    //  ...
}

现在您可以在DialogWithResult<TViewModel>()类型参数上添加约束:

public dynamic DialogWithResult<TViewModel>(object owner = null)
    where TViewModel : DialogResultBase

现在.DialogWithResult<object>()将导致编译错误。

同样,我不是我提出的解决方案的忠实粉丝。但是,只有C#功能才能实现您的要求。

答案 1 :(得分:8)

正如你和@CodeFuller所观察到的那样,在C#中无法进行部分推理。

如果你正在寻找一些不那么邪恶而不是动态的东西,你可以使用扩展方法和自定义类的组合来获得你需要的类型而不必直接引用返回类型。

在下面的示例中,我使用DialogResultBase<T>上的扩展方法来推断返回类型,然后返回一个包含DialogWithResult<WindowVm>的泛型方法的辅助类。

仍然不漂亮,但大致符合你的要求。

关于推理的有趣观点。每个参数只能用于推断单个类型。如果要多次传递相同的参数,可以从中推断出多种类型。即如果您将同一参数传递给(T myList, List<TItem> myListAgain)中的两个参数,则可以推断出列表类型和项目类型。

public class Class2
{
    public static void DoStuff()
    {
        var dialogResult = default(WindowVm).GetReturnType().DialogWithResult<WindowVm>();

    }

}


public class MyReturnType { }
public class DialogResultBase<T> : IDialogWithResult<T> { }
public interface IDialogWithResult<TSomeReturnType> { }

public class WindowVm : DialogResultBase<MyReturnType> { }

public class DialogResultHelper<TSomeReturnType>
{
    public IDialogWithResult<TSomeReturnType> DialogWithResult<TViewModel>() where TViewModel : DialogResultBase<TSomeReturnType>, new()
    {
        return new TViewModel();
    }
}

public static class Extensions
{
    public static DialogResultHelper<T> GetReturnType<T>(this DialogResultBase<T> dialogResultBase)
    {
        return new DialogResultHelper<T>();
    }

}