艾伟:手工打造运算符重载过程

简介: 除了J#外,所有微软支持的.NET开发语言现在均支持运算符重载,因此纯粹为C#简化写法一样特性现已成为一种.NET开发中值得研究的一项重要语言特性。有人认为运算符重载其实就是简化写法,满足模拟基本类型操作的小功能,没有必要深究。

除了J#外,所有微软支持的.NET开发语言现在均支持运算符重载,因此纯粹为C#简化写法一样特性现已成为一种.NET开发中值得研究的一项重要语言特性。有人认为运算符重载其实就是简化写法,满足模拟基本类型操作的小功能,没有必要深究。但我觉得要多思考一层,为什么我们总希望模拟基本类型的操作?因为运算符重载能够将操作中缀化,能够自动推测静态过程的主体。

首先是操作中缀化。函数调用其实是一种前缀操作,函数(代表操作)总是在参数(代表操作数)之前写出。这样执行序列操作时运行的顺序其实和书写的顺序相反:

H(x,y)
G(H(x, y), z)
F(G(H(x, y), z), w)

序列运行的顺序是H->G->F但是却要反过来写,二元参数距离函数名越来越远。我们按照计算机执行的顺序思考,却要反过来写,多少有些不爽。成员函数扩展方法的写法则是将操作数(对象)写在前面:

x.H(y)
x.H(y).G(z)
x.H(y).G(z).F(w)

这样就将书写的顺序正过来了。这是一个甚好的方案,但是在不具备扩展方法的今天,有些事情是成员函数做不了的。比如在我的VBF里,我希望Functor可以进行And, Or等逻辑运算,而Functor之间只能进行算术运算,Functor之间只能进行连接运算,而且规则还不一样……但是成员函数没有根据类型参数选取不同重载的能力,也就是说.NET泛型无法进行特化操作。在.NET中具有编译期类型判定的机制只有两个:函数根据参数类型的重载和用户自定义隐式转换(相当于根据返回类型重载)。我们可以用Functor<,>类型的静态方法来实现根据类型参数不同的不同重载。但是静态方法不但要写全类型的名字,还是前缀操作,使用起来让人甚为不爽,这时就会发现,运算符重载是我们梦寐以求的东西。

Type.op_Operator(x, y) '静态方法
x op y '运算符写法

以上两种是等价的,可以看到运算符重载不仅可以通过x,y的类型推测静态方法的调用主体Type,还可以将操作转化为中缀写法——比后缀更适合表现二元运算。既然这么完美,我们能不能这样写呢?

Class Functor(Of T, U)
    Public Shared Operator And( _
        x As Functor(Of T, Boolean), y As Functor(Of T, Boolean)) _
        As Functor(Of T, Boolean)
    End Operator
End Class

很遗憾,这样会编译错误。作为运算符重载过程,其参数至少有一个必须是定义运算符的类型。在编译器看来,必须是Functor(Of T, U),两个类型参数都必须是该泛型类定义的。就在我对此大感抱怨时,我偶然在C#编译器的源代码(见Rotor)中看到了它识别运算符的规则,其中并没有这些限制,只有两条规则——方法必须是静态的,特定名称的方法;方法必须带有specialname属性。那么我们完全可以骗过编译器,不用它提供的Operator关键字来声明运算符重载过程,而是使用自己编写特定名称的方法,并加以specialname的手法来打造运算符重载过程:

Imports System.Runtime.CompilerServices

Class Functor(Of T, U)
    _
    Public Shared Function op_BitwiseAnd( _
        x As Functor(Of T, Boolean), y As Functor(Of T, Boolean)) _
        As Functor(Of T, Boolean)
    End Function
End Class

System.Runtime.CompilerServices.SpecialNameAttribute是一个指示编译器为声明成员添加specialname的特殊属性,C#和VB编译器都支持。op_BitwiseAnd是VB和C#等语言所识别的与操作运算符过程名称。这样写完以后编译成类库,再以引用DLL的方式引用它,你就会看到编译器将他识别成了我们要的运算符重载过程。当你在Functor这样的类型上使用And操作时,编译器会告诉你不支持该运算符,仅在Functor上才能进行这一操作,编译错误信息准确无误,真是太棒了。

在我们结束前,我们还可以看看如此手工打造还能突破哪些编译器人为的限制:

可重载Protected和Private的运算符(尽管这样做几乎没有意义)
可不成对重载比较运算符(=, >, >=, <=, <, <>)
可以让移位运算符的第二个操作数不是int(>>和<<样子很好看,但是有了这个限制我们就不能拿它来干别的事情,现在好了)
可以在C#中重载仅VB支持的运算符,也可以在VB中重载仅C#支持的运算符(当然要到对方语言中才能生效)
可以让用户自定义显式转换支持泛型类型参数之间更加神奇的写法
用了这种手法,似乎还可以重载诸如operator+(int, int)之类的运算符,但它们并不能生效。

.NET语言编译器中每一项特性,都可能有隐藏在其表面之下的深层次用途。善加研究后常能发现原来所认识不到的功能。我当然不是在推荐大家乱用运算符重载,只是一种思考,一种新的灵感。

目录
相关文章
|
2月前
|
API Windows
MASM32编程完善SysInfo遇到奇怪故障,真切感受全局变量和局部变量之别……
MASM32编程完善SysInfo遇到奇怪故障,真切感受全局变量和局部变量之别……
|
编译器 C语言
【C语言航路外传】如何隐藏代码及声明和定义的在工程中真正的使用场景
【C语言航路外传】如何隐藏代码及声明和定义的在工程中真正的使用场景
119 1
|
6月前
你还在为做原型而担心吗?神器我拿出来了,剩下的就看你的了
你还在为做原型而担心吗?神器我拿出来了,剩下的就看你的了
30 0
|
存储 编译器 C++
c++ 有趣的动态转换之 delete 崩溃探究兼谈基类虚析构的重要性
c++ 有趣的动态转换之 delete 崩溃探究兼谈基类虚析构的重要性
【软工】原型化方法与常用动态分析方法
【软工】原型化方法与常用动态分析方法
57 0
|
自然语言处理 API Python
除庄周梦蝶外,庄子还讲过哪些梦你知道吗?新故事引出新版本——
除庄周梦蝶外,庄子还讲过哪些梦你知道吗?新故事引出新版本——
177 0
|
存储 自然语言处理 Dart
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
详细介绍C语言中程序的预处理、编译、汇编、链接过程。向你展示在计算机内部一个完整的程序是如何诞生的
174 0
C生万物 | 详解程序环境和预处理【展示程序编译+链接全过程】
|
移动开发 JSON JavaScript
华为快应用-怎样写一个全局的变量和方法
华为快应用-怎样写一个全局的变量和方法
190 0
华为快应用-怎样写一个全局的变量和方法
VS使用FFmpeg被声明为已否决的解决方案
VS使用FFmpeg被声明为已否决的解决方案
J3
|
Java
对象只能new吗?傻瓜、海王都是会五六种方式
绝对不是标题党,我真能用五六种方式创建出对象出来,只不过是在 Java 中。 当然,我也绝非海王,这毋庸置疑😛😛😛。
J3
189 0
对象只能new吗?傻瓜、海王都是会五六种方式