这是介绍 Ceylon 语言的系列文章的第9篇。请注意,在最终发布之前,语言的一些特性可能会发生变化。
命名参数
考虑以下方法
void printf(OutputStream to, String format, Object... values) { ... }
(记住,最后一个参数是一个序列参数,可以接受多个参数,就像 Java 的 varargs
参数一样。)
我们已经看到了许多使用熟悉的 C 风格语法调用方法或实例化类的例子,其中参数由括号分隔,并用逗号分隔。参数通过在列表中的位置进行匹配。让我们再看一个例子,以防万一
printf(process, "Thanks, %s. You have been charged %.2f. Your confirmation number is %d.", user.name, order.total, order.confimationNumber);
这可以正常工作,我想。然而,Ceylon 提供了一种替代的方法调用协议,当有多个或两个以上的参数时,通常更容易阅读
printf { to = process; format = "Thanks, %s. You have been charged %.2f. Your confirmation number is %d."; user.name, order.total, order.confimationNumber };
这种调用协议称为命名参数列表。我们可以通过使用大括号而不是括号作为分隔符来识别命名参数列表。请注意,参数之间用分号分隔,除了序列参数的参数,它们用逗号分隔。我们明确指定每个参数的名称,除了序列参数,其参数始终出现在命名参数列表的末尾。请注意,也可以这样调用此方法,将序列传递给命名值参数值参数
printf { to = process; format = "Thanks, %s. You have been charged %.2f. Your confirmation number is %d."; values = { user.name, order.total, order.confimationNumber }; };
我们通常将命名参数调用格式化在多行中。
声明性对象实例化语法
命名参数通常用于构建对象图。因此,Ceylon 提供了一种特殊的缩写语法,通过指定类初始化器中的命名参数来简化属性获取器、命名参数或方法的声明。
我们可以缩写以下形式的属性定义
Payment payment = Payment { method = user.paymentMethod; currency = order.currency; amount = order.total; };
或以下形式的命名参数指定
payment = Payment { method = user.paymentMethod; currency = order.currency; amount = order.total; };
到以下更声明性(且不那么冗余)的风格
Payment payment { method = user.paymentMethod; currency = order.currency; amount = order.total; }
我们可以编写如下形式的方法
Payment createPayment(Order order) { return Payment { method = user.paymentMethod; currency = order.currency; amount = order.total; }; }
使用以下简化的语法
Payment createPayment(Order order) { method = user.paymentMethod; currency = order.currency; amount = order.total; }
也许您担心这看起来像为声明类的三个属性赋值的方法,而不是为Payment类的命名参数实例化提供快捷语法。这是一个非常合理的观点。对于一个Java开发者来说,这确实是这样。有两个因素应该提醒您真正发生的事情。上述方法
- 没有返回语句,但它没有被声明void,并且
- 包含一组=规范语句而不是:=赋值表达式。
一旦您习惯了Ceylon更灵活的语法,这些差异通常会立即显现。
关于命名参数的更多内容
以下类定义了用于构建表格的数据结构
class Table(String title, Natural rows, Border border, Column... columns) { ... } class Column(String heading, Natural width, String content(Natural row)) { ... } class Border(Natural padding, Natural weight) { ... }
当然,我们可以使用位置参数列表来构建一个Table。然而,使用命名参数构建复杂对象图的情况更为常见。在本节中,我们将介绍一些新的命名参数列表功能,这些功能特别便于构建对象图。
String x(Natural row) { return row.string; } String xSquared(Natural row) { return (row**2).string; } Table table = Table("Squares", 5, Border(2,1), Column("x",10, x), Column("x**2",12, xSquared));
首先,请注意,我们已看到的指定命名参数值的语法与细化
形式属性的语法完全相同。如果您考虑一下,考虑到方法参数可能接受其他方法的引用,指定命名参数值的问题开始看起来与细化抽象成员的问题非常相似。因此,Ceylon将允许我们在命名参数列表内部重用大量的成员声明语法。(但请注意,这尚未在编译器中实现。)在命名参数列表中包含以下结构是合法的
方法声明 - 指定接受函数的参数的
- 值
- 对象(匿名类)声明 - 对于指定类型为接口或抽象类的参数值非常有用,并且
- 获取器声明 - 允许我们内联计算参数的值。
这有助于解释为什么命名参数列表用大括号括起来:命名参数列表的完全通用语法与类、方法或属性体的语法非常接近。再次注意,语言的规律性如何产生灵活性。
因此,我们可以将构建Table的代码重写如下
Table table = Table { title="Squares"; rows=5; border = Border { padding=2; weight=1; }; Column { heading="x"; width=10; String content(Natural row) { return row.string; } }, Column { heading="x**2"; width=12; String content(Natural row) { return (row**2).string; } } };
请注意,我们已使用声明方法的常规语法指定了名为content的参数的值。
更好的是,我们的示例可以缩写如下
Table table { title="Squares"; rows=5; Border border { padding=2; weight=1; } Column { heading="x"; width=10; String content(Natural row) { return row.string; } }, Column { heading="x**2"; width=10; String content(Natural row) { return (row**2).string; } } }
请注意,我们如何将代码从强调调用的形式转换为强调声明层次结构的格式。从语义上讲,这两种格式是等效的。但在可读性方面,它们却大不相同。
我们可以将上述完全声明性的代码放入一个单独的文件中,它看起来就像一种定义表格的某种类型的“迷你语言”。实际上,它是可执行的Ceylon代码,可以通过Ceylon编译器进行语法正确性验证,然后编译为Java字节码。更好的是,Ceylon IDE(当存在时)将为我们提供迷你语言的创作支持。与某些动态语言中的DSL支持形成鲜明对比,任何Ceylon DSL都是完全类型安全的!您可以认为Table, Column和Border类的定义是定义迷你语言的“
模式”或“
语法”。(实际上,它们真正定义的是迷你语言的语法树。)
现在让我们看一个具有内联获取器声明的命名参数列表的示例
shared class Payment(PaymentMethod method, Currency currency, Float amount) { ... }
Payment payment { method = user.paymentMethod; currency = order.currency; Float amount { variable Float total := 0.0; for (Item item in order.items) { total += item.quantity * item.product.unitPrice; } return total; } }
最后,这是一个具有内联声明的命名参数列表示例对象声明
shared interface Observable { shared void addObserver(Observer<Bottom> observer) { ... } }
shared interface Observer<in Event> { shared formal on(Event event); }
observable.addObserver { object observer satisfies Observer<UpdateEvent> { shared actual void on(UpdateEvent e) { writeLine("Update:" + e.string); } } };
注意,Observer
当然,正如我们在第8部分中看到的,解决这个问题的更好方法可能是消除Observer接口并直接传递方法
shared interface Observable { shared void addObserver<Event>(void on(Event event)) { ... } }
observable.addObserver { void on(UpdateEvent e) { writeLine("Update:" + e.string); } };
这里快速岔开一下:注意,我们在这里只需要一个方法T的类型参数addObserver()因为Ceylon继承了Java的限制,即函数类型在参数类型上是非变异性。这实际上很不自然。我们可能最终需要想出一个解决方案来使函数类型在参数类型上变异性,这样我们就可以编写
shared interface Observable { shared void addObserver(void on(Bottom event)) { ... } }
定义用户界面
我们将要为Ceylon编写的第一个模块之一是一个用于编写HTML模板的库。一段静态HTML可能看起来像这样
Html { Head head { title = "Hello World"; cssStyleSheet = 'hello.css'; } Body body { Div { cssClass = "greeting"; "Hello World" }, Div { cssClass = "footer"; "Powered by Ceylon" } } }
一个完整的HTML模板可能看起来像这样
import ceylon.html { ... } doc "A web page that displays a greeting" page '/hello.html' Html hello(Request request) { Head head { title = "Hello World"; cssStyleSheet = 'hello.css'; } Body body { Div { cssClass = "greeting"; Hello( request.parameters["name"] ).greeting }, Div { cssClass = "footer"; "Powered by Ceylon" } } };
还有更多...
这个语法除了用户界面定义之外还有许多潜在的应用。例如,Ceylon允许我们使用命名参数列表来指定程序元素注解的参数。但我们将不得不在未来的一期中回到注解的话题。在第10部分中,我们将讨论语言模块的一些基本类型,特别是数值类型,并介绍操作多态性的概念。