我想在D中实现值对象模式。也就是说,我想拥有对不可变对象的可变引用变量。T
变量应该是可赋值的,但T
对象永远不应该改变它们的状态。
我对D中const
和immutable
之间的区别感到困惑。让我用一个框架Rational
类来说明我的疑虑:
class Rational
{
int num;
int den;
我应该将num
和den
声明为const
还是immutable
?整数有区别吗?
invariant()
{
assert(den > 0);
assert(gcd(abs(num), den) == 1);
}
我应该将invariant
声明为const
还是immutable
?将其标记为immutable
会导致编译时错误,但这可能是由于其他成员未被标记为CCD14。
this(int numerator, int denominator) { ... }
我应该将构造函数声明为const
还是immutable
?这意味着什么?
string toString()
{
return std.string.format("(%s / %s)", num, den);
}
}
我应该将toString
声明为const
还是immutable
?
我似乎也可以标记整个班级,而不是标记单个成员:
class Rational
const class Rational
immutable class Rational
以下哪一项对值对象模式最有意义?
pure
呢?在值对象模式中,方法应该没有副作用,那么将每个成员声明为pure
有意义吗?不幸的是,将toString
标记为pure
不会编译,因为std.string.format
不是纯的;这有什么特别的原因吗?
我似乎也可以将类本身声明为pure
,但这似乎没有任何效果,因为编译器不再抱怨toString
调用了一个不纯净的函数。
那么,将一个类声明为pure
意味着什么呢?它只是被忽略了吗?
D结构
值对象模式最好在D中通过简单地使用结构及其内置的值语义来表示。
据我所知,Java中通常使用值对象模式,因为Java目前缺乏具有值语义的内置聚合。
D的结构类似于C和C#中的结构,以及C++中的结构和类。这种比较可能最适合后者,因为D结构有构造函数和析构函数,但有一个重要的例外:没有继承和虚拟函数;这些特性被委托给类,这些类的工作方式与Java和C#中的类非常相似(它们是隐式引用类型,因此从来没有出现切片问题)。
struct Rational
{
int num;
int den;
/* your methods here */
}
然后,Rational的实例总是通过值传递给函数(除非参数明确指定,请参阅ref和out),并在赋值时复制。
纯度
纯函数不能读取或写入任何全局状态。允许纯函数对方法的显式参数和隐式this
参数进行变异;因此Rational上的方法可能总是CCD_ 29。
std.string.format
不是pure
是其当前实现的问题。它将来将使用一种不同的实现,即pure
。
Const和Immutable
如果你想表达这个方法是纯的,并且不会改变它自己的状态,你可以同时使它成为pure
和const
。
可变(Rational
)和不可变(immutable(Rational)
)实例都可以隐式转换为const(Rational)
,因此当您不需要不可变的保证,但仍然不会更改任何成员时,const
是最佳选择。
通常,不需要对成员字段进行变异的结构方法应该是const
。对于类,同样适用,但您也必须考虑任何可能重写该方法的派生方法——它们受相同限制的约束。
将const
或immutable
放在struct
或class
声明上相当于分别标记其所有成员(包括方法)const
或immutable
。
不可变构造函数
如果构造函数所做的只是将num
和den
字段分配给它们各自的构造函数参数,那么该功能默认情况下已经存在于结构中:
struct S { int foo, bar; }
auto s = S(1, 2);
assert(s.foo == 1);
assert(s.bar == 2);
构造函数上的const
没有多大意义,因为任何构造函数无论恒定性如何都可以构造const实例,因为所有东西都可以隐式转换为const。
构造函数上的immutable
确实有意义,有时是构造结构或类的不可变实例的唯一方法。可变构造函数可以为this
引用创建别名,稍后可以通过该别名对实例进行变异,因此其结果不能总是隐式转换为不可变。
然而,在您的案例中不需要不可变的构造函数,因为Rational没有任何间接性,所以可以使用可变的构造函数并复制结果。换句话说,没有可变间接性的类型可以隐式转换为不可变类型。这包括像int
和float
这样的基元类型以及满足相同条件的结构。
没有效果的属性
放在声明中没有任何效果的属性会被所有当前编译器忽略。这是有意义的,因为使用attribute { /* declarations */ }
和attribute: /*declarations*/
语法,属性可以同时应用于多个声明:
struct S
{
immutable
{
int foo;
int bar;
}
}
struct S2
{
immutable:
int foo;
int bar;
}
在上述两个示例中,foo
和bar
都属于immutable(int)
类型。
使用类
有时不需要值语义,例如出于与频繁复制大型结构相关的性能原因。可以通过引用显式传递结构,例如使用ref
和out
函数参数或使用指针,但当值语义是默认值时,很容易出错,语法开销可能会很高。指针还有许多其他陷阱。
类是引用类型,不可能像对待值一样对待它们。它们通常是用new
实例化的,CCD_60总是创建一个GC分配的类实例(不赞成重载new
)。这两点使D中的类与Java和C#中的类非常相似(另一个值得注意的点是有接口而不是多重继承)。然而,类有隐藏字段的开销(目前所有类都有size_t.sizeof * 2
字节),并且没有指定字段的ABI,但当需要继承和虚拟函数时,类也是唯一的选项。
以下是为价值对象模式实现的Rational:
class Rational
{
immutable int num;
immutable int den;
this(int num, int den)
{
this.num = num;
this.den = den;
}
/* methods here */
}
这是最忠实于Java实现的实现。它使用不可变来防止num
和den
的突变,而不管实例本身的可变性如何。方法应该是const
,通常是pure
,就像结构一样。
由于不可变构造函数目前还没有完全实现(读作:根本不要使用它们),因此上面的构造函数实际上允许您创建类的不可变实例(例如new immutable(Rational)(1, 2)
),即使构造函数可以自由地创建this
引用的可变别名,从而打破了不可变的保证。
一种稍微类似D的方式是将不变性决策留给用户代码,实现方式如下:
class Rational
{
int num;
int den;
this(int num, int den)
{
this.num = num;
this.den = den;
}
/* immutable constructor overload would be here */
/* methods here */
}
然后,用户可以选择是使用Rational
还是使用immutable(Rational)
。后者可以使用std.cocurrency线程接口在线程之间安全地传递,而在编译时尝试发送前者将被拒绝。
然而,后者有一个明显的问题——因为Rational
隐含地是一个引用类型,所以无法键入对Rational的不可变实例的可变引用。目前这个问题的解决方案是使用std.typecons.Rebindable。在该语言中有一个解决方案可以解决这个问题。