运算符,6.1.2 自定义运算符

6.1.2 自定义运算符
定义自定义的运算符的方式类似于函数,使用 let 绑定。它们可以使用任何字符,可以是通常的 F# 数学运算符(+/-*<>),或者是逻辑运算符(& | =),还可以是其他字符 ($%.?@^~!)。声明一个运算符,要把它的名字括在括号中,这是与通常的 let 绑定的唯一区别。使用星号时要小心,因为,(* 用于 F# 多行注释的开始。在这种情况下,解决方案是在星号与括号之间加上空格。清单 6.2 显示了如何声明和使用一个简单的运算符来处理字符串。
Listing 6.2 Working with strings using a custom operator (F# Interactive)
> let (+>) a b = a + "\n>> " + b;;
val ( +> ) : string -> string –> string
> printfn ">> %s" ("Hello world!" +>
"How are you today?" +>
"I'm fine!");;
>> Hello world!
>> How are you today?
>> I'm fine!
使用自定义的运算符而不用函数的好处,是可以使用中缀表示法(infix notation)。这意味着,对于 concat "A" (concat "B" "C"),可以写成 "A"+>"B"+>"C"。这在多次应用运算符时是特别有用的,就像我们前面的示例,因为,不必把每个调用括在括号中。
在清单 6.2 中,我们声明一胩中缀运算符,它取两个参数。F# 也允许定义一元运算符,只取一个参数,用于前缀表示法(prefix notation)。内置前缀运算符的一个示例是一元负,写成为 -1。这种运算符不基于声明中参数的数目,因为,它可能是不明确的。这有点棘手,但是,可以写一元运算符,返回一个函数,那么纯粹基于这个类型签名,它看起来像二元运算符(由于科里化,我们在第 5 章讨论的)。前缀和缀运算符之间的这种区别是基于第一个符号。当定义前缀运算符,必须以 ~ 或 !符号开始。
在 C# 中模拟自定义运算符
在 C# 中,不能声明新的运算符,虽然可以重载现有的运算符。然而,在某种程度上,使用扩展方法,可以实现相同的模式。这是 C# 3.0 中的一个新功能,下面,我们简要介绍一下。
扩展方法
在 C# 中,每个方法必须被包装在一个类中,处理对象的操作是类声明的一部分,可以使用点表示法来调用方法。扩展方法给我们一种添加新方法的方式,来处理对象,而无需修改原始的类声明。以前,这可能要通过写静态方法才行,就像这样:
StringUtils.Reverse(str);
这是很不切实际的,因为,在某个 “Utils” 类中找到一个静态方法是相当困难的。C# 3.0 中,我们可以实现 Reverse,作为扩展方法,以这种方式调用:
str.Reverse();
实现扩展方法是很容易的,因为,它是普通的静态方法,有一个特别的修饰符。唯一的区别,是它可以作为实例方法来调用,使用点表示法。它仍然是一个静态方法,所以,它既不能添加新字段,也不能访问对象的私有状态:
static class StringUtils {
public static string Reverse(this string str) { /* ... */ }
}
所有扩展方法都必须括在非嵌套的静态类中,它们必须是静态方法。修饰符 this 放在第一个参数之前,告诉编译器为它是扩展方法。
如果我们把前面示例中字符串连接实现为扩展方法,得到的语法会非常类似于原始的 F# 版本。清单 6.3 显示了使用标准的静态方法调用和扩展方法,写相同的代码。
Listing 6.3 Working with strings using extension methods (C#)
public static string AddLine(this string str, string next) {
return str + "\n>>" + next;
}
Console.WriteLine(
StringUtils.AddLine(
StringUtils.AddLine("Hello world!", "How are you today"),
"I'm fine!"));
Console.WriteLine("Hello world!"
.AddLine("How are you today")
.AddLine("I'm fine!"));
好处是纯粹的在可读性方面:我们能够 写该方法调用,与我们希望其发生的顺序相同,而不需要指定实现这个方法的类,也不需要额外的大括号。就像这样的情况 ,语法产生一个非常重要的差异。
F# 的流运算符
流水线的运算符(|>) 允许我们在函数的左侧写出第一个参数——即,在该函数名字前。这是非常有用的,如果我们想要调用几个处理函数,用序列中的某个值,我们想先写出要处理的值。下面的示例演示如何在 F# 中反转列表,然后取第一个元素:
List.hd(List.rev [1 .. 5])
这并不是非常优雅的,因为,写的操作顺序与执行顺序相反,将要处理的值在右侧,括在括号中。在 C# 中,使用扩展方法,我们会写成:
list.Reverse().Head();
在 F# 中,可以通过使用流运算符,来得到相同的结果:
[1...5] |> List.rev |> List.hd
尽管这看起来可能非常棘手,但是,运算符是非常简单的。它具有两个参数值:第二个(右侧)是一个函数,第一个(左侧)是一个值。运算符把值作为这个函数的参数值,并返回结果。
在某种意义上,流是类似于调用方法,在对象是使用点表示法,但是,它并不局限于内在的对象的方法。这是类似于扩展方法,所以,当我们写了一个通常用的流运算符的 F# 函数的 C# 替代方案时,我们将它作为扩展方法实现。
现在,我们已经完成了对泛型高阶函数和运算符的简短说明,终于可以看看如何使用它们解决日常函数编程问题。我们将讨论的第一个主题是使用高阶函数元组。
Tags:  位运算符 运算符优先级 java运算符 运算符重载 运算符

延伸阅读

最新评论

发表评论