刚入职,被安排上了维护一个旧项目 为什么每次入职都是维护旧项目,如标题所写,这是一个使用 Silverlight 开发的旧项目。当这个项目的第一行代码写下的时候,我还在念小学。最近,这个项目中的一个历史遗留问题终于不能被拖延了,需要被马上修复。
问题背景
这个项目有自己的一套业务开发框架,大多数情况下,后续接手的开发人员不需要对 Silerlight 有太多的了解就可以快速的开发新需求。例如,业务框架提供了一个 SendRequestAsync: (reqPath: string, callback: action<T>) → void
的方法,用来异步的调用远程 HTTP 服务,在 callback 中则可以处理请求的响应。再比如,业务框架还提供了一个 IsCompleted: () → bool
的虚方法,用来封装一个表单提交完成后的固定操作。通过子类对 IsCompleted
的重写,来实现控制是否提交表单并触发这个固定操作。
那么现在问题来了,业务要求子类的 IsCompleted
的实现中,必须要先通过 SendRequestAsync
调用一个接口,然后根据接口的返回值来决定 IsCompleted
的返回值。如你所见,IsCompleted
是个同步函数,他的返回值只是一个单纯的 bool
类型而已,所以这个问题本质上是需要在同步函数中等待异步返回结果。
先看看业务框架文档
在接手这个项目的时候,我收到了一份开发文档,里面提到了一个跟 SendRequestAsync
相呼应的 SendRequest: (reqPath: string, seconds: int, callback: action<T>) → void
,从命名上可以看到这应该是用来同步发送请求,文档里面的一句话注释也是这么记录的。
不过如果真的这么简单,这个问题也就不值得我写一篇文章来记录了。把发送请求的方法替换成 SendRequest
后,我发现执行请求的逻辑仍然是异步的,问了交接项目的同事,他也说这个方法一定是同步的,只有 SendRequestAsync
才是异步调用。这样,我只好求助于 dotPeek 将业务框架反编译,对比了 SendRequest
以及 SendRequestAsync
的实现后,我发现他们之间的差异竟然是 SendRequest
仅仅比 SendRequestAsync
多了一个定时器做超时控制。气得我浑身发抖,大热天的全身冷汗手脚冰凉,这算哪门子的同步调用啊!
要不手动实现同步?
既然文档里写的是骗人的,我只好试着来把这个异步调用改造成同步方法了。我首先想到的就是 TaskCompletionSource
,我可以在 IsCompleted
中等待对应的 TaskCompletionSource.Task
执行完成,然后在 SendRequestAsync
的回调中使用 TaskCompletionSource.SetResult
来让等待的线程继续执行。跟交接的同事讨论了这样的想法,他也觉得可行,不过在实现了这个逻辑之后,Silverlight 狠狠地给我们甩了一个脸色。
Silverlight 的 UI 状态只能在 UI 线程中访问,也就是说通过按钮事件触发的 IsCompleted
方法将会在 UI 线程执行。而业务框架为了方便开发,SendRequestAsync
的 callback 也是在 UI 线程执行的。这样一来,如果我们阻塞了 IsCompleted
的调用,那么 SendRequestAsync
的回调将永远不会执行。
将思路逆转过来
上面提到了,业务框架会在一个子类的无法访问的按钮的点击事件回调中调用 IsCompleted
来判断是否需要执行通用的表单提交后的固定操作。将我们的思维逆转过来,不要去纠结如何在同步函数中等待异步结果返回,而是将问题转化为“如何在异步回调中执行通用的固定操作”。因为基类访问性的限制,这个问题最终就变成了“如何调用基类的私有方法”。
遇到这种绕过访问控制的问题,我第一个想到的就是反射,一顿操作就把反射调用基类私有方法的函数(CallBaseCommit
)完成了。不过 Silverlight 又一次将我的方案驳回,并告诉我“方法尝试访问失败”。经过一番面向爆栈搜索后发现,Silverlight 4 是不支持反射访问实例成员的,Silverlight 5 中对反射的使用也有很多限制,这就会导致“尝试访问失败”。
反射调用私有方法不能直接使用,我们还有表达式树。首先通过反射获取到私有方法的 MethodInfo
,然后使用 Expression.Call
构造调用实例方法的表达式,接着用这个表达式构建一个 Lambda 表达式,最终将其编译成一个委托调用。终于,这个阻塞了我接近 6 小时的“同步函数阻塞调用异步函数”的问题,通过表达式树解决了!
最后
-
基础库的开发应当注重命名,比如上面提到的
SendRequest
应为BeginSendRequestWithTimeoutAsync
-
异步流程的模板方法应该提供异步版本,如果业务框架提供了
IsCompletedAsync
就不会有这篇文章了 -
dotPeek 反编译真得劲