我想在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
会导致编译时错误,但这可能是由于其他成员未被标记为immutable
。
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
是什么意思?它被简单地忽略了吗?
答案 0 :(得分:15)
值对象模式最好用D表示,只需使用struct及其内置值语义。
据我所知,由于Java目前缺乏具有价值语义的内置聚合,因此价值对象模式通常在Java中使用。
D的结构与C和C#中的结构类似,以及C ++中的结构和类。对于后者,这种比较可能是最好的,因为D结构具有构造函数和析构函数,但有一个重要的例外:没有继承和虚函数;这些功能被委托给classes,它的工作方式与Java和C#中的类很相似(它们是隐式引用类型,因此它们从不展示the slicing problem)。
struct Rational
{
int num;
int den;
/* your methods here */
}
然后Rational的实例总是按值传递(除非参数显式指定,参见ref and out)到函数并在赋值时复制。
纯函数无法读取或写入任何全局状态。允许纯函数改变显式参数以及方法的隐式this
参数;因此,Rational上的方法可能总是pure
。
std.string.format
不是pure
是其当前实施的问题。它将来会使用不同的实现pure
。
如果您想表达该方法是纯粹的并且也不会改变自己的状态,则可以同时使其pure
和const
。
可变(Rational
)和不可变(immutable(Rational)
)实例都可以隐式转换为const(Rational)
,因此const
是您不做的最佳选择需要不可改变的保证,但你仍然不会改变任何成员。
通常,不需要改变成员字段的struct方法应该是const
。对于类,同样适用,但您还必须考虑可能覆盖该方法的任何派生方法 - 它们受相同限制的约束。
将const
或immutable
放在struct
或class
声明上相当于标记其所有成员(包括方法)const
或{{1}分别。
如果您的所有构造函数都将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
引用创建别名,稍后可以通过该引用对该实例进行变异,因此其结果不能总是隐式转换为不可变。
但是,在您的情况下不需要不可变的构造函数,因为Rational没有任何间接,因此可以使用可变构造函数并复制结果。换句话说,没有可变间接的类型可以隐式转换为不可变。这包括原始类型,如this
和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
进行实例化,它始终创建一个GC分配的类实例(不推荐重载new
)。这两点使D中的类与Java和C#中的类非常相似(另一个值得注意的点是有接口而不是多重继承)。但是,类具有隐藏字段的开销(当前所有类的new
字节)并且未指定字段的ABI,但是当需要继承和虚函数时,类也是唯一的选项。
这里为Value Object Pattern实现了Rational:
size_t.sizeof * 2
这是最忠实于Java实现的实现。它使用不可变来防止class Rational
{
immutable int num;
immutable int den;
this(int num, int den)
{
this.num = num;
this.den = den;
}
/* methods here */
}
和num
的变异,而不管实例本身的可变性。与结构一样,方法应为den
,通常为const
。
由于不可变构造函数目前尚未完全实现(阅读:根本不使用它们),上述构造函数实际上将允许您创建类的不可变实例(例如pure
),即使构造函数可以自由地创建new immutable(Rational)(1, 2)
引用的可变别名,从而打破不可变保证。
稍微更像D的方法是将不变性决策留给用户代码,明确地实现它:
this
然后,用户可以选择是使用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
。后者可以使用std.concurrency线程接口在线程之间安全地传递,而尝试发送前者将在编译时被拒绝。
然而,后者有一个明显的问题 - 因为immutable(Rational)
隐式地是一个引用类型,所以没有办法输入对Rational的不可变实例的可变引用。此问题的当前解决方案是使用std.typecons.Rebindable。有proposed solution用于修复此语言。