在D中实现值对象模式

时间:2012-08-08 10:29:16

标签: design-patterns const d immutability value-objects

我想在D中实现值对象模式。也就是说,我希望将可变引用变量赋予不可变对象。 T变量应该是可分配的,但T个对象永远不应该改变它们的状态。

我对D中constimmutable之间的区别感到困惑。让我用骷髅Rational类来说明我的疑惑:

class Rational
{
    int num;
    int den;

我应该将numden声明为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是什么意思?它被简单地忽略了吗?

1 个答案:

答案 0 :(得分:15)

D Struct

值对象模式最好用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

Const和Immutable

如果您想表达该方法是纯粹的并且也不会改变自己的状态,则可以同时使其pureconst

可变(Rational)和不可变(immutable(Rational))实例都可以隐式转换为const(Rational),因此const是您不做的最佳选择需要不可改变的保证,但你仍然不会改变任何成员。

通常,不需要改变成员字段的struct方法应该是const。对于类,同样适用,但您还必须考虑可能覆盖该方法的任何派生方法 - 它们受相同限制的约束。

constimmutable放在structclass声明上相当于标记其所有成员(包括方法)const或{{1}分别。

不可变构造函数

如果您的所有构造函数都将immutablenum字段分配给它们各自的构造函数参数,则默认情况下该结构上已存在此功能:

den
构造函数上的

struct S { int foo, bar; } auto s = S(1, 2); assert(s.foo == 1); assert(s.bar == 2); 没有多大意义,因为任何构造函数都可以构造一个const实例,因为所有构造函数都可以隐式转换为const。

构造函数上的

const确实有意义,有时是构造结构或类的不可变实例的唯一方法。可变构造函数可以为immutable引用创建别名,稍后可以通过该引用对该实例进行变异,因此其结果不能总是隐式转换为不可变。

但是,在您的情况下不需要不可变的构造函数,因为Rational没有任何间接,因此可以使用可变构造函数并复制结果。换句话说,没有可变间接的类型可以隐式转换为不可变。这包括原始类型,如thisint以及满足相同条件的结构。

无效的属性

所有当前编译器都会忽略声明中没有任何效果的声明属性。这是有道理的,因为属性可以使用floatattribute { /* 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用于修复此语言。