我正在为这样的记录类型添加一个静态构建器方法:
type ThingConfig = { url: string; token : string; } with
static member FromSettings (getSetting : (string -> string)) : ThingConfig =
{
url = getSetting "apiUrl";
token = getSetting "apiToken";
}
我可以这样称呼它:
let config = ThingConfig.FromSettings mySettingsAccessor
现在是棘手的部分:我想添加第二个重载的构建器以供C#使用(暂时忽略重复的实现):
static member FromSettings (getSetting : System.Func<string,string>) : ThingConfig =
{
url = getSetting.Invoke "apiUrl";
token = getSetting.Invoke "apiToken";
}
这适用于C#,但打破了我之前的F#调用 错误FS0041:无法根据此程序点之前的类型信息确定方法“FromSettings”的唯一重载。可能需要类型注释。候选人:静态成员ThingConfig.FromSettings:getSetting:(string - &gt; string) - &gt; ThingConfig,静态成员ThingConfig.FromSettings:getSetting:Func - &gt; ThingConfig
为什么F#无法确定要拨打哪一个?
该类型注释会是什么样的? (我可以从呼叫站点注释参数类型吗?)
这种互操作是否有更好的模式? (重载从C#和F#接受lambdas)
答案 0 :(得分:11)
F#中的过载分辨率通常比C#更有限。出于安全考虑,F#编译器通常会拒绝C#编译器认为有效的重载。
然而,这个具体案例是一种真正的模棱两可。为了.NET interop的利益,F#编译器对lambda表达式有一个特殊的规定:定期,lambda表达式将被编译为F#函数,但是如果预期的类型已知为Func<_,_>
,则编译器将转换lambda到.NET委托。这允许我们使用基于高阶函数构建的.NET API,例如IEnumerable<_>
(又名LINQ),而无需手动转换每个lambda。
所以在你的情况下,编译器真的很困惑:你的意思是将lambda表达式保持为F#函数并调用你的F#重载,或者你的意思是将它转换为Func<_,_>
并调用C#重载?
为了帮助编译器,您可以明确地将lambda表达式的类型声明为string -> string
,如下所示:
let cfg = ThingConfig.FromSettings( (fun s -> foo) : string -> string )
稍微好一点的方法是在FromSettings
调用之外定义函数:
let getSetting s = foo
let cfg = ThingConfig.FromSettings( getSetting )
这很好用,因为自动转换为Func<_,_>
仅适用于内联编写的lambda表达式。编译器不会将任何函数转换为.NET委托。因此,在getSetting
调用之外声明FromSettings
会使其类型明确string -> string
,并且重载解析可以正常工作。
编辑:事实证明上述情况不再有效。当前的F#编译器将自动将任何函数转换为.NET委托,因此即使将类型指定为
string -> string
也不会消除歧义。请继续阅读其他选项。
说到类型注释 - 您可以以类似的方式选择其他重载:
let cfg = ThingConfig.FromSettings( (fun s -> foo) : Func<_,_> )
或使用Func
构造函数:
let cfg = ThingConfig.FromSettings( Func<_,_>(fun s -> foo) )
在这两种情况下,编译器都知道参数的类型是Func<_,_>
,因此可以选择重载。
超载通常很糟糕。它们在某种程度上掩盖了正在发生的事情,使得程序更难以调试。我已经丢失了C#重载决策正在挑选IEnumerable
而不是IQueryable
的错误,从而将整个数据库拉到了.NET端。
在这些情况下我通常做的是,我使用不同的名称声明两个方法,然后使用CompiledNameAttribute在从C#查看时为它们提供替代名称。例如:
type ThingConfig = ...
[<CompiledName "FromSettingsFSharp">]
static member FromSettings (getSetting : (string -> string)) = ...
[<CompiledName "FromSettings">]
static member FromSettingsCSharp (getSetting : Func<string, string>) = ...
这样,F#代码将看到两种方法FromSettings
和FromSettingsCSharp
,而C#代码将看到相同的两种方法,但分别命名为FromSettingsFSharp
和FromSettings
。 intellisense体验将有点难看(但很容易理解!),但完成的代码在两种语言中看起来都完全相同。
在F#中,用小写的第一个字符命名函数是惯用的。请参阅标准库以获取示例 - Seq.empty
,String.concat
等。那么我将实际在您的情况下做什么,我会创建两个方法,一个用于F#,名为{{ 1}},另一个用于名为fromSettings
的C#:
FromSettings
(另请注意,第二种方法可以按照第一种方法实施;您不必复制和粘贴实施方案)
答案 1 :(得分:0)
F#中的重载分辨率有问题。
我已经提交了一些案例,就像这样,它显然与规范相矛盾。
作为一种解决方法,您可以将C#重载定义为扩展方法:
module A =
type ThingConfig = { url: string; token : string; } with
static member FromSettings (getSetting : (string -> string)) : ThingConfig =
printfn "F#ish"
{
url = getSetting "apiUrl";
token = getSetting "apiToken";
}
module B =
open A
type ThingConfig with
static member FromSettings (getSetting : System.Func<string,string>) : ThingConfig =
printfn "C#ish"
{
url = getSetting.Invoke "apiUrl";
token = getSetting.Invoke "apiToken";
}
open A
open B
let mySettingsAccessor = fun (x:string) -> x
let mySettingsAccessorAsFunc = System.Func<_,_> (fun (x:string) -> x)
let configA = ThingConfig.FromSettings mySettingsAccessor // prints F#ish
let configB = ThingConfig.FromSettings mySettingsAccessorAsFunc // prints C#ish