从最早的Ceylon版本开始,我们支持简化的类初始化语法,其中类的参数紧随类名之后列出,并且初始化逻辑直接进入类的主体中。
class Color(shared Integer rgba) {
assert (0 <= rgba <= #FFFFFFFF);
function encodedValue(Integer slot)
=> rgba.rightLogicalShift(8*slot).and(#FF);
shared Integer alpha => encodedValue(3);
shared Integer red => encodedValue(2);
shared Integer green => encodedValue(1);
shared Integer blue => encodedValue(0);
function hex(Integer int) => formatInteger(int, 16);
string => "Color { \
alpha=``hex(alpha)``, \
red=``hex(red)``, \
green=``hex(green)``, \
blue=``hex(blue)`` }";
}
我们可以实例化一个这样的类:
Color red = Color(#FFFF0000);
直接从类的成员中引用类的参数的功能确实有助于减少冗长,并且在大多数情况下,这是编写代码的一种非常舒适的方法。
但是,正如我们在过去几年中编写Ceylon代码所看到的那样,有时候我们会非常欣赏能够编写具有多个初始化路径的类的能力,例如Java,C#或C ++中的构造函数。 显然,在绝大多数情况下(我估计超过90%的类),构造函数是不必要且不舒服的。 但是对于其余棘手的案例,我们仍然需要一个好的解决方案。
我们遇到了几个特别引人注目的案例:
-
ceylon.language
Array
类,可以分配一个元素列表,或者分配一个大小和单个元素值,以及 - 克隆复制构造,用于,例如,为了实现
HashMap.clone()
和HashSet.clone()
不幸的是,我总是发现Java和C#继承自C ++的构造函数的设计有些奇怪且缺乏表现力。 因此,在我告诉您在Ceylon 1.2中对构造函数所做的工作之前,让我开始解释我认为Java构造函数存在的问题。
Java中的构造函数有什么问题?
如上所述,在从C ++借用的语言中,构造函数语法的最大问题是,在只有一个构造函数的类的常见情况下,该构造函数的参数在类的主体中不可用,从而导致糟糕的结果。如下代码:
class Point {
public float x;
public float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public String toString() {
return "(" + x + ", " + y + ")";
}
}
这很伤人。 幸运的是,我们已经在锡兰消除了这种痛苦。
class Point(shared Float x, shared Float y) {
string => "(``x``, ``y``)";
}
因此,让我们来看一下Java中的构造函数的其他一些问题。
首先,语法是不规则的。 在类似C的语言中,声明的语法为:
Modifier* (Keyword|Type) Identifier OtherStuff
奇怪的是,构造函数不符合此一般架构,后来被附加。
其次,一个类的构造函数都必须具有相同的名称。 这似乎是一个非常奇怪的限制:
- 如果它们都具有相同的名称,为什么不使用关键字而不是标识符来声明它们? 干,有人吗?
- 这是一个使我失去表现力的限制。 我没有编写
new ColorWithRGBAndAlpha(r,g,b,a)
线索,而是写了new Color(r,g,b,a)
,让读者猜测。 - 因此,构造函数遇到了Java对重载的完全破坏的支持。 我不能有一个使用
List<Float>
的构造函数,而另一个不能使用List<Integer>
的构造函数,因为这两个参数类型具有相同的擦除。 - 构造函数引用(Java中的
Class::new
)可能是不明确的,具体取决于上下文。
第三,构造函数不必强制初始化类的实例变量。 所有Java类型都有一个“默认”值(零或null),并且如果您忘记在Java中初始化实例变量,则会得到NullPointerException
,或更糟糕的是,在运行时会得到不正确的零值。 这些问题最肯定属于我希望静态类型系统能够检测到的一类问题,实际上,在其他情况下,Java 确实会检测未初始化的变量。
还要注意,在Ceylon中这将是一个更大的问题,因为大多数类型没有实例作为null
,因此没有明显的“默认”值。
像往常一样,我的目的不是打击Java,而是证明为什么我们在锡兰采用不同的方式。
命名构造函数和默认构造函数
相比之下,Ceylon中为构造函数新引入的语法是常规的,可表达的,并且不依赖于重载(Ceylon不支持重载,除非与本机Java代码进行互操作)。 这是构造函数的基本语法:
new withFooAndBar(Foo foo, Bar bar)
extends anotherConstructor(foo) {
//do stuff
}
当构造函数所属的类直接扩展Basic
, extends
子句是可选的。
这是一个用法示例:
class Color {
shared Integer rgba;
//default constructor
shared new (Integer rgba) {
assert (0 <= rgba <= #FFFFFFFF);
this.rgba = rgba;
}
//named constructor
shared new withRGB(
Integer red, Integer green, Integer blue,
Integer alpha = #FF) {
assert (0 <= red <= #FF,
0 <= green <= #FF,
0 <= blue <= #FF);
rgba =
alpha.leftLogicalShift(24) +
red.leftLogicalShift(16) +
green.leftLogicalShift(8) +
blue;
}
//another named constructor
shared new withRGBIntensities(
Float red, Float green, Float blue,
Float alpha = 1.0) {
assert (0.0 <= red <= 1.0,
0.0 <= green <= 1.0,
0.0 <= blue <= 1.0);
function int(Float intensity)
=> (intensity*#FF).integer;
rgba =
int(alpha).leftLogicalShift(24) +
int(red).leftLogicalShift(16) +
int(green).leftLogicalShift(8) +
int(blue);
}
function encodedValue(Integer slot)
=> rgba.rightLogicalShift(8*slot).and(#FF);
shared Integer alpha => encodedValue(3);
shared Integer red => encodedValue(2);
shared Integer green => encodedValue(1);
shared Integer blue => encodedValue(0);
function hex(Integer int) => formatInteger(int, 16);
string => "Color { \
alpha=``hex(alpha)``, \
red=``hex(red)``, \
green=``hex(green)``, \
blue=``hex(blue)`` }";
}
构造函数声明用关键字new表示,其名称以小写字母开头。 我们这样调用构造函数:
Color red = Color.withRGBIntensities(1.0, 0.0, 0.0);
或者,使用命名参数,如下所示:
Color red =
Color.withRGBIntensities {
red = 1.0;
green = 0.0;
blue = 0.0;
};
对构造函数的函数引用具有自然的语法:
Color(Float,Float,Float) createColor
= Color.withRGBIntensities;
一个类可能有一个没有名称的构造函数,称为默认构造函数 。 通过默认构造函数进行实例化的方式就像不带构造函数的类的实例化一样:
Color red = Color(#FFFF0000);
一个类不需要具有默认构造函数,但是大多数类将具有一个默认构造函数。
为什么我们需要默认构造函数的概念? 好吧,因为具有构造函数的类可能没有参数list 。 等待,让我们停止并再次强调该警告,因为它是一个重要的警告:
您不能将构造函数添加到带有参数列表的类中! 相反,您必须首先重写该类,以将“默认构造函数”用于其“常规”初始化逻辑。
但是,具有构造函数的类可能仍直接在该类的主体中具有初始化逻辑。 例如,以下内容完全合法:
class Color {
shared Integer rgba;
shared new (Integer rgba) {
this.rgba = rgba;
}
shared new withRGB(
Integer red, Integer green, Integer blue,
Integer alpha = #FF) {
assert (0 <= red <= #FF,
0 <= green <= #FF,
0 <= blue <= #FF);
rgba =
alpha.leftLogicalShift(24) +
red.leftLogicalShift(16) +
green.leftLogicalShift(8) +
blue;
}
shared new withRGBIntensities(
Float red, Float green, Float blue,
Float alpha = 1.0) {
assert (0.0 <= red <= 1.0,
0.0 <= green <= 1.0,
0.0 <= blue <= 1.0);
function int(Float intensity)
=> (intensity*#FF).integer;
rgba =
int(alpha).leftLogicalShift(24) +
int(red).leftLogicalShift(16) +
int(green).leftLogicalShift(8) +
int(blue);
}
//executed for every constructor
assert (0 <= rgba <= #FFFFFFFF);
//other members
...
}
每次实例化该类时,都会执行最后一个assert
语句。
值构造函数
我们刚刚看到的构造函数在语言规范中被称为可调用构造函数 ,因为它们声明了参数。 我们还有值构造函数 ,它们不声明参数,并且在类所属的上下文中首次评估构造函数时执行一次。 对于顶级类,值构造函数为单例。
class Color {
shared Integer rgba;
//default constructor
shared new (Integer rgba) {
this.rgba = rgba;
}
//value constructors
shared new white {
rgba = #FFFFFFFF;
}
shared new red {
rgba = #FFFF0000;
}
shared new green {
rgba = #FF00FF00;
}
shared new blue {
rgba = #FF0000FF;
}
//etc
...
}
我们可以使用这样的值构造函数:
Color red = Color.red;
有时,类的构造函数共享某些初始化逻辑。 如果该逻辑不依赖于类的参数,我们可以将其直接放在类的主体中,如我们所见。 但是,如果确实取决于参数,则我们通常需要采取不同的策略。
构造函数委托
为了便于在类中重用初始化逻辑,允许构造函数委派给同一类的不同构造函数很有用。 为此,我们使用extends
子句:
Integer int(Float intensity)
=> (intensity*#FF).integer;
class Color {
shared Integer rgba;
shared new (Integer rgba) {
this.rgba = rgba;
}
//value constructors delegate to the default constructor
shared new white
extends Color(#FFFFFFFF) {}
shared new red
extends Color(#FFFF0000) {}
shared new green
extends Color(#FF00FF00) {}
shared new blue
extends Color(#FF0000FF) {}
shared new withRGB(
Integer red, Integer green, Integer blue,
Integer alpha = #FF) {
assert (0 <= red <= #FF,
0 <= green <= #FF,
0 <= blue <= #FF);
rgba =
alpha.leftLogicalShift(24) +
red.leftLogicalShift(16) +
green.leftLogicalShift(8) +
blue;
}
shared new withRGBIntensities(
Float red, Float green, Float blue,
Float alpha = 1.0)
//delegate to other named constructor
extends withRGB(int(red),
int(green),
int(blue),
int(alpha)) {}
assert (0 <= rgba <= #FFFFFFFF);
//other members
...
}
构造函数只能委托给在类的主体中定义的构造函数。 注意,我们已经编写了extends Color(#FFFFFFFF)
来委托给Color
的默认构造函数。
确定的初始化和部分构造函数
诸如Color.withRGB()
或Color.withRGBIntensities()
类的普通构造函数负责初始化属于以下类的每个值引用:
-
shared
,或 - 由班级的另一个成员使用(“捕获”)。
Ceylon编译器会在编译时强制执行此职责,除非可以证明每个值引用均已完全初始化,否则它将拒绝代码:
- 每个普通的构造函数,或者
- 在课程本身中
如果不是针对部分构造器的概念,则此规则将使得很难排除构造器中包含的通用逻辑。 对于部分构造函数,放宽了对所有引用进行完全初始化的要求。 但是部分构造函数可能无法用于直接实例化该类。 只能从同一类的另一个构造函数的extends
子句中调用它。 部分构造函数由abstract
注释指示:这是一个人为的示例:
class ColoredPoint {
shared Point point;
shared Color color;
//partial constructor
abstract new withColor(Color color) {
this.color = color;
}
shared new forCartesianCoords(Color color,
Float x, Float y)
//delegate to partial constructor
extends withColor(color) {
point = Point.cartesian(x, y);
}
shared new forPolarCoords(Color color,
Float r, Float theta)
//delegate to partial constructor
extends withColor(color) {
point = Point.polar(r, theta);
}
...
}
到目前为止,我们仅看到了如何委托给同一类的另一个构造函数。 但是,当类扩展超类时,每个构造函数都必须最终(可能是间接地)委托给超类的构造函数。
构造函数与继承
一个类可以使用构造函数扩展一个类,例如:
class ColoredPoint2(color, Float x, Float y)
extends Point.cartesian(x, y) {
shared Color color;
...
}
一个更有趣的情况是扩展类本身具有构造函数时:
class ColoredPoint extends Point {
shared Color color;
shared new forCartesianCoords(Color color,
Float x, Float y)
//delegate to Point.cartesian()
extends cartesian(x, y) {
this.color = color;
}
shared new forPolarCoords(Color color,
Float r, Float theta)
//delegate to Point.polar()
extends polar(r, theta) {
this.color = color;
}
...
}
在此示例中,构造函数直接委托给超类的构造函数。
初始化逻辑的顺序
使用构造函数委托以及直接在类主体中定义的初始化逻辑,您必须想象到初始化会变得非常复杂。
好吧,不。 Ceylon中初始化的一般原理保持不变:初始化总是从上到下流动,允许类型检查器在使用每个value
之前验证每个value
是否已初始化。
考虑此类:
class Class {
print(1);
abstract new partial() {
print(2);
}
print(3);
shared new () extends partial() {
print(4);
}
print(5);
shared new create() extends partial() {
print(6);
}
print(7);
}
调用Class()
导致以下输出:
1
2
3
4
5
7
调用Class.create()
导致以下输出:
1
2
3
5
6
7
一切都井然有序! 在评论中 ,David Hagen提出了一种理解构造函数委派和排序如何工作的方式。
使用值构造函数模拟枚举
您可能已经注意到,如果类仅具有值构造函数,则它与Java enum
非常相似。
shared class Day {
shared actual String string;
abstract new named(String name) {
string = name;
}
shared new sunday extends named("SUNDAY") {}
shared new monday extends named("MONDAY") {}
shared new tuesday extends named("TUESDAY") {}
shared new wednesday extends named("WEDNESDAY") {}
shared new thursday extends named("THURSDAY") {}
shared new friday extends named("FRIDAY") {}
shared new saturday extends named("SATURDAY") {}
}
因此,我们让您在switch
语句中使用值构造函数。 switch
值构造函数的能力可以看作是用于switch
Integer
, Character
和String
等类型的文字值的现有功能的扩展。
Day day = ... ;
String message;
switch (day)
case (Day.friday) {
message = "thank god";
}
case (Day.sunday | Day.saturday) {
message = "we could be having this conversation with beer";
}
else {
message = "need more coffee";
}
print(message);
但是,当我们注意到与Java enum
的相似之处时,我们决定将其想法比Java在此处提供的功能更进一步。 Java的枚举是开放的 ,在这个意义上,一个switch
覆盖了所有枚举值声明enum
还必须包括default
情况下,要考虑通过明确任务,明确回报检查无遗。 但是在Ceylon中,我们也有封闭枚举类型的概念,在这种情况下,可以省略“默认”情况(Ceylon的switch
语句的else
子句),但整个switch
仍将被视为详尽无遗。 注意: API设计人员应注意仅“关闭”枚举,该枚举不会在API的未来版本中增加新的值构造函数。 Day
和Boolean
是封闭枚举的很好的例子。 ErrorType
是开放枚举的示例。 如果我们在Day
添加of
子句,则将其视为封闭枚举。
shared class Day
of sunday | monday | tuesday | wednesday |
thursday | friday | saturday {
shared actual String string;
abstract new named(String name) {
string = name;
}
shared new sunday extends named("SUNDAY") {}
shared new monday extends named("MONDAY") {}
shared new tuesday extends named("TUESDAY") {}
shared new wednesday extends named("WEDNESDAY") {}
shared new thursday extends named("THURSDAY") {}
shared new friday extends named("FRIDAY") {}
shared new saturday extends named("SATURDAY") {}
}
现在,Ceylon将把覆盖所有值构造函数的switch
语句视为详尽的switch
,并且我们可以编写以下代码而无需else
子句:
Day day = ... ;
String message;
switch (day)
case (Day.monday | Day.tuesday |
Day.wednesday | Day.thursday) {
message = "need more coffee";
}
case (Day.friday) {
message = "thank god";
}
case (Day.sunday | Day.saturday) {
message = "we could be having this conversation with beer";
}
print(message);
这是模拟Java样式enum
的现有模式的替代方法。
最后的话
我在这里提出的设计是经过五年的思考过程的最终结果。 我个人认为,从原则上解决这是一个非常困难的问题。 有一段时间,我希望甚至根本不需要使用该语言的构造函数。 但是最终我对最终结果感到非常满意。 在我看来,不仅在原则上与该语言的其余部分保持一致,而且还非常富有表现力和功能。
翻译自: https://www.javacodegeeks.com/2015/06/constructors-in-ceylon.html