好吧,让我设置场景:
我们在代码中使用了一个函数,该函数接受一个函数并对其进行一些记录,然后返回结果。看起来有点像这样。
TResponse LoggedApiCall<TResponse>(Func<BaseRequest, BaseResponse> apiCall, ...)
where TResponse : BaseResponse;
在使用中,我有以下四个对象
namespace Name.Space.Base {
public class BaseRequest {
...
}
}
namespace Name.Space.Base {
public class BaseResponse {
...
}
}
namespace Some.Other.Name.Space {
public class Request : BaseRequest {
...
}
}
namespace Name.Space {
public class Response<TPayload> : BaseResponse {
...
}
}
因此,为了支持某些单元测试,我正在尝试模拟LoggedApiCall(使用Moq)。我正在编写一个通用方法,该方法使我们可以传递一个满足基本类型约束的函数以及一个类型匹配的响应,以创建一个通用方法来在Mock上执行.Setup()。
看起来像这样:
protected IReturnsResult<IService> SetupLoggedApiCall<TRequest, TResponse>(
Func<TRequest, TResponse> function,
TResponse response
)
where TRequest : BaseRequest
where TResponse : BaseResponse
{
var baseFunction = function as Func<BaseRequest, BaseResponse>;
return _mockService.Setup(service => service.LoggedApiCall<TResponse>(
baseFunction, /*other parameters *
))
.Returns(response);
}
}
我尝试强制转换该函数的原因是,如果不执行此操作,则会收到智能提示错误
Argument type 'System.Func<TRequest, TResponse>' is not assignable to
parameter type 'System.Func<Name.Space.Base.BaseRequest, Name.Space.Base.BaseResponse>'
我感到有些困惑,因为TRequest和TResponse分别受BaseRequest和BaseResponse的约束,但是如果必须解决,我会的。
但是,执行类型转换时
var baseFunction = function as Func<BaseRequest, BaseResponse>
它解析为null。由于前面提到的对传入SetupLoggedApiCall的参数的限制,我也感到困惑。
我在调试代码时做了进一步的挖掘,并得到了以下信息:
function is Func<TRequest, TResponse> | true
function is Func<TRequest, BaseResponse> | true
function is Func<BaseRequest, BaseResponse> | false
如此所示,TResponse继续满足BaseResponse,并且可以毫无问题地转换到它。但是,一旦我们尝试从TRequest转移到BaseRequest,它就会失败。只是为了确保我不会陷入导入任何错误类型或我遵循以下内容的情况:
typeof(TRequest).BaseType == typeof(BaseRequest) | true
所以,谁能告诉我:
假设所有指向TRequest的内容都为BaseRequest,那么在TRequest的问题上该强制转换失败了吗?
我们将开始精简这一过程,并在一个新的代码项目中隔离问题(实际上,我们的代码并没有下面的代码那么简单,我将其简化为它的核心),然后看看它在什么时候失败了,我们如果我们发现任何内容,将不胜感激。
更新1
根据@EugenePodskal的建议,我更新了LoggedApiCall的定义以读取
TResponse LoggedApiCall<TRequest, TResponse>(Func<TRequest, TResponse> apiCall, ...)
where TRequest : BaseRequest where TResponse : BaseResponse
至少在编译时,这使SetupLoggedApiCall感到满意,因为传入的内容将是有效的,但是,对模拟服务的调用仍然返回null。我钻回到代理对象,然后到拦截器进行此调用,发现了这一点:
IService service => service.LoggedApiCall<Request, Response>(, /*other params*/)
那不是错字。拦截器根本没有第一个参数。我猜这将问题更多地转移到了关于模拟而不是功能上,但是看到拦截器是一个兰巴表达,任何人都可以弄清楚可能导致该参数简单丢失的原因是什么?
最佳答案
您需要查看的主题是Covariance and Contravariance in Generics
http://msdn.microsoft.com/en-us/library/dd799517(v=vs.110).aspx
即,“在.NET Framework 4中,Func泛型委托(delegate)(例如Func)具有协变量返回类型和变量参数类型。”
如果您考虑一下,这也是合乎逻辑的。
Func<Apple, AppleProduct> MakeAppleProduct = new Func....;
// Assume the cast is allowed at runtime and doesn't throw.
Func<Fruit, FruitProduct> MakeFruitProduct = (Func<Fruit, FruitProduct>) MakeAppleProduct;
//Returns an instance of AppleProduct
MakeFruitProduct(appleInstance);
//Orange is also a Fruit, and hence we are allowed to pass it? Should it be allowed?
MakeFruitProduct(orangeInstance);
因此,对于函数参数,您不想允许强制转换为基本类型。
另一方面,对于返回值,如果该函数最初声明为返回
AppleProduct
实例,则可以安全地说它返回FuitProduct
实例(AppleProduct
的基类)为100%(类型)