F#中的事件(下)

简介:

上一篇随笔简单介绍了在F#中事件的有趣特性,即可组合的一等公民,在我们处理事件时就拥有了更好的灵活性。在里面的例子中可以看到如何订阅一个事件,包括在C#/VB.NET中定义的标准.NET事件。本文将介绍如何在F#中发布(创建)和订阅自定义事件。

在C#中发布和订阅事件

在此之前,先来看看如何在C#中发布一个事件,这里将只考虑标准的.NET事件(即微软推荐的事件定义方式)。这个过程涉及两个重要的类型:EventArgs,作为包含事件数据的类的基类;EventHandler<TEventArgs>,表示将处理事件的方法。比如下面的例子:

复制代码
C# Code - 发布和订阅事件
public class KeyEventArgs : EventArgs
{
    
private readonly char keyChar;
    
public KeyEventArgs(char keyChar)
    {
        
this.keyChar = keyChar;
    }

    
public char KeyChar
    {
        
get { return keyChar; }
    }
}

public class KeyInputMonitor
{
    
public event EventHandler<KeyEventArgs> KeyDown;

    
protected virtual void OnKeyDown(KeyEventArgs e)
    {
        EventHandler
<KeyEventArgs> temp = KeyDown;

        
if (temp != null)
        {
            temp(
this, e);
        }
    }

    
public void SimulateKeyDown(char key)
    {
        KeyEventArgs e 
= new KeyEventArgs(key);
        OnKeyDown(e);
    }
}

internal class UsingCustomEvent
{
    
private static void Main()
    {
        KeyInputMonitor monitor 
= new KeyInputMonitor();
        monitor.KeyDown 
+= new EventHandler<KeyEventArgs>(monitor_KeyDown);

        monitor.SimulateKeyDown(
'A');
        monitor.SimulateKeyDown(
'V');
    }

    
static void monitor_KeyDown(object sender, KeyEventArgs e)
    {
        Console.WriteLine(e.KeyChar);
    }
}
复制代码


这里通过KeyEventArgs类来封装事件所包含的数据,然后以泛型EventHandler委托发布KeyDown事件,最后定义SimulateKeyDown方法来触发事件。在类型外部(这里是UsingCustomEvent类)就可以订阅该事件。

使用Event.create方法创建事件

事件的发布-订阅过程大致如此,理解了这个过程就比较容易理解F#的定义方式了。这里还是要用到那个Event模块,它的create方法将帮助我们创建事件,其签名信息为:

F# Signature Info
val it : (unit -> ('a -> unit) * IEvent<'a>)


该方法的返回值是一个tuple,一个用于触发事件的方法,另一个则是事件本身,就像前面例子中的SimulateKeyDown方法和KeyDown事件。比如:

复制代码
F# Code - 使用Event.create方法创建事件
let fire, event = Event.create()
event.Add(printfn 
"Fired %d")
fire(
25) // 
Fired 25

val fire : (int -> unit)
val event : IEvent<int>
复制代码


这里我们可以再一次见识类型推导的威力。不过问题是,以这样的方式创建事件后,事件是局部的,如何将事件封装在类中呢?

在F#中发布和订阅事件

现在将尝试在F#重写上面的C#示例。

复制代码
F# Code - 发布和订阅事件 
open System

type KeyEventArgs(keyChar: char) =
    
inherit EventArgs()
    
member this.KeyChar = keyChar
   
type KeyInputMonitor() =
    
let (keyDownFire, keyDownEvent: IEvent<KeyEventArgs>) = Event.create()
   
    
member this.KeyDown = keyDownEvent
    
member this.SimulateKeyDown key =
        
let args = new KeyEventArgs(key)
        keyDownFire args
   
let monitor = new KeyInputMonitor()
monitor.KeyDown.Add(
fun args -> printfn "%A" args.KeyChar)
monitor.SimulateKeyDown 
'A' 
monitor.SimulateKeyDown 
'V'  
复制代码


这里,在KeyInputMonitor发布KeyDown事件后,之后订阅和触发过程跟C#的例子很接近了。现在考虑在C#中使用F#中发布的事件。

发布标准的.NET的事件

现在我们在C#项目中引用上面创建的F#类,并尝试以常规的方式使用它的事件,就会发现难以使用。原因是它的KeyDown事件是IEvent类型的,而不是常规的委托类型。这个问题解决起来不难:

复制代码
F# Code - 发布标准的.NET事件
open System

type KeyEventArgs(keyChar: char) =
    
inherit EventArgs()
    
member this.KeyChar = keyChar
    
type KeyDownEventHandler = delegate of obj * KeyEventArgs -> unit
    
type KeyInputMonitor() =
    
let keyDownEvent = new Event<KeyDownEventHandler, KeyEventArgs>()
    
    [<CLIEvent
>]
    
member this.KeyDown = keyDownEvent.Publish
    
    
member this.SimulateKeyDown(key) = 
        keyDownEvent.Trigger(this, 
new KeyEventArgs(key))
        
let monitor = new KeyInputMonitor()
monitor.KeyDown.Add(
fun args -> printfn "%A" args.KeyChar)
monitor.SimulateKeyDown 
'A'
monitor.SimulateKeyDown 
'V'
复制代码


这里定义了一个委托KeyDownEventHandler,并且在创建事件的时候创建了一个Event类型的实例,而不是Event.create方法,更重要的是对事件属性添加了attribute:CLIEvent,这个attribute将使得这个事件属性编译为标准的.NET事件。现在来看,一方面在F#中使用该事件时与前面相同,另一方面在C#中使用时:

复制代码
C# Code - 使用在F#中发布的事件
internal class UsingFsEvent
{
    
private static void Main()
    {
        FsLib.KeyInputMonitor monitor 
= new FsLib.KeyInputMonitor();
        monitor.KeyDown 
+= new FsLib.KeyDownEventHandler(monitor_KeyDown);
        monitor.SimulateKeyDown(
'A');
        monitor.SimulateKeyDown(
'V');
    }

    
static void monitor_KeyDown(object sender, FsLib.KeyEventArgs e)
    {
        Console.WriteLine(e.KeyChar);
    }
}
复制代码

这个也与我们在C#中的使用习惯相同,这样在F#发布的事件就可以兼容于其它.NET语言了。

小结

本文首先介绍了如何使用Event.create方法创建新的事件,然后在此基础上讨论了如何发布和订阅事件,这样可以更符合我们的编码习惯。不过这种方式发布的事件在C#等其它.NET语言中却难以使用,所以最后介绍了如何发布标准的.NET事件,这样就可以兼容于F#和其它的.NET语言了。

参考

F# First Class Events – Creating Events
How to create .NET-compatible events in F#
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2009/08/21/composable-events-in-fsharp-part2.html,如需转载请自行联系原作者。

目录
相关文章
|
存储 NoSQL 关系型数据库
PostgreSQL列存扩展hydra简单测试
Hydra是一款PostgreSQL的扩展,为PostgreSQL增加了列存引擎,使得PostgreSQL的olap性能大幅提升,本文介绍Hydra基本的使用方法。
|
9月前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
319 8
|
8月前
|
弹性计算 运维 安全
云上DevOps自动化的最佳实践
本文介绍了云上DevOps自动化最佳实践,重点探讨了企业在上云过程中面临的成本管理、运维效率和弹性等问题。通过阿里云的产品和服务,企业可以实现自动化的资源管理、成本优化和高效运维。文章详细阐述了如何利用标签进行成本分析、选择合适的付费类型和实例规格、以及通过弹性伸缩降低成本。此外,还介绍了新功能发布,如统一的实例运维通道界面、AI辅助的运维工具等,帮助企业提升云上业务的管理和运营效率。
|
NoSQL 安全 调度
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
Redisson 的 MultiLock 是一种分布式锁实现,支持对多个独立的 RLock 同时加锁或解锁。它通过“整锁整放”机制确保所有锁要么全部加锁成功,要么完全回滚,避免状态不一致。适用于跨多个 Redis 实例或节点的场景,如分布式任务调度。其核心逻辑基于遍历加锁列表,失败时自动释放已获取的锁,保证原子性。解锁时亦逐一操作,降低死锁风险。MultiLock 不依赖 Lua 脚本,而是封装多锁协调,满足高一致性需求的业务场景。
155 0
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
|
算法 计算机视觉
基于Chan-Vese算法的图像边缘提取matlab仿真
**算法预览展示了4幅图像,从边缘检测到最终分割,体现了在matlab2022a中应用的Chan-Vese水平集迭代过程。核心代码段用于更新水平集并显示迭代效果,最后生成分割结果及误差曲线。Chan-Vese模型(2001)是图像分割的经典方法,通过最小化能量函数自动检测平滑区域和清晰边界的图像分割,适用于复杂环境,广泛应用于医学影像和机器视觉。**
|
存储 关系型数据库 MySQL
MySQL表空间结构与页、区、段的定义
一、概念引入 1、页 InnoDB是以页为单位管理存储空间的,在InnoDB中针对不同的目的设计了各种不同类型的页面。如下(省略了FIL_PAGE或FiL_PAGE_TYPE的前缀):
|
安全 机器人 程序员
Windows 计划任务每天隔 3 小时运行一次批处理文件详细配置步骤
Windows 计划任务每天隔 3 小时运行一次批处理文件详细配置步骤
1760 2
|
存储
【STM32基础 CubeMX】PWM输出
【STM32基础 CubeMX】PWM输出
1682 0
|
安全 Go
gRPC(七)进阶:自定义身份验证
gRPC为每个gRPC方法调用提供了Token认证支持,可以基于用户传入的Token判断用户是否登陆、以及权限等,实现Token认证的前提是,需要定义一个结构体,并实现credentials.PerRPCCredentials接口。
1548 1
gRPC(七)进阶:自定义身份验证
|
前端开发 数据库
mybatisplus---接口返回的数据为null
mybatisplus---接口返回的数据为null
mybatisplus---接口返回的数据为null