我看到我无法从F#编译器解释的行为(Visual F#3.1.1.0) - 表面上出现的只是命名本地和传递临时之间的区别实际上产生了行为差异.
我不了解F#行为,或者这是代码生成错误?(我知道,后者更有可能.)
Repro - 我发现很难在没有使用Reactive Extensions的情况下重新编译,所以这就像我得到它一样简单.请注意,try1和try2几乎是一样的.
open System
open System.Reactive.Linq
open System.Threading
let interval = TimeSpan.FromSeconds(0.5)
let testDuration = TimeSpan.FromSeconds(2.0)
let mkHandler () = // creates a function that closes over state
let count = ref 0
fun _ -> count := !count + 1
printfn "State is now %d" !count
let try1 () =
printfn "try1"
let handler = mkHandler ()
use subscription = Observable.Interval(interval).Subscribe(handler)
Thread.Sleep(testDuration)
let try2 () =
printfn "try2"
// creates handler inline:
use subscription = Observable.Interval(interval).Subscribe(mkHandler ())
Thread.Sleep(testDuration)
[<EntryPoint>]
let main argv =
try1 ()
try2 ()
0
Run Code Online (Sandbox Code Playgroud)
输出 - try1和try2函数分别说明了所需和不良行为.该计划的输出是:
try1
State is now 1
State is now 2
State is now 3
try2
State is now 1
State is now 1
State is now 1
Run Code Online (Sandbox Code Playgroud)
根据我的理解try2应该表现得一样try1.如果没有,请解释这个微小差异应该如何起作用.
通过检查反编译器的输出,我确定了以下内容:
mkHandler运作正常; 它创建了一个关闭唯一状态的函数.当多次调用时,它会改变该状态.
同样超载Subscribe被称为都try1和try2:public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext)
生成的幕后帮助程序代码用于try1关闭处理程序函数并正确调用它:
[CompilationMapping(SourceConstructFlags.Closure)]
[Serializable]
// subscription@16
internal sealed class subscriptionu004016
{
public FSharpFunc<long, Unit> handler;
public subscriptionu004016(FSharpFunc<long, Unit> handler)
{
}
internal void Invoke(long obj)
{
this.handler.Invoke(obj);
}
}
Run Code Online (Sandbox Code Playgroud)
幕后帮助程序代码try2不会关闭处理程序函数,而是在mkHandler每次调用时调用工厂函数; 这解释了输出,但不是所需的行为:
[CompilationMapping(SourceConstructFlags.Closure)]
[Serializable]
// subscription@22-1
internal sealed class subscriptionu004022u002d1
{
public subscriptionu004022u002d1()
{
}
internal void Invoke(long obj)
{
Program.mkHandler<long>().Invoke(obj);
}
}
Run Code Online (Sandbox Code Playgroud)
重申我的问题:为什么这两个功能表现不同?这是代码生成错误吗?以上都不是?
据我所知,您的代码没有任何问题 - 您所做的事情是有道理的.这似乎是F#编译器中的一个微妙的错误.
我怀疑编译器如何解析Subscribe方法有问题.您的代码正在创建一个F#函数值,但编译器会Action<int64>自动将其包装到委托中并使用Rx版本Subscribe.但是,它通常不会自动将部分应用的函数转换为代理 - 它似乎只在这种情况下发生.
最简单的解决方法似乎是更改您的mkHandler函数以显式创建委托,然后一切按预期工作:
let mkHandler () = // creates a function that closes over state
let count = ref 0
Action<int64>(fun _ ->
count := !count + 1
printfn "State is now %d" !count)
Run Code Online (Sandbox Code Playgroud)
编辑:经过一些更多的调查,我会说这是一个特殊的Subscribe方法发生的错误IObservable<T>.由于F#将事件自动视为IObservable<T>值,因此它对它们进行了一些特殊处理并添加了Subscribe方法.如果在Subscribe其他地方声明了扩展名,则会发生冲突并且事情会中断.
我能找到的最简单的repro是创建一个C#项目:
public static class Extensions {
public static void Subscribe(this IObservable<int> c1, Action<int> f) {
f(1);
f(2);
}
}
Run Code Online (Sandbox Code Playgroud)
然后完成你所做的事情:
let partial() =
printfn "called"
fun n -> ()
let a = new Event<int>()
let o = a.Publish.Subscribe(partial())
Run Code Online (Sandbox Code Playgroud)
这打印"被叫"两次,而它应该只被调用一次.我在F#bug跟踪器上为此问题创建了一个错误.