在D中实现价值对象模式



我想在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会导致编译时错误,但这可能是由于其他成员未被标记为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

如果你想表达这个方法是纯的,并且不会改变它自己的状态,你可以同时使它成为pureconst

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

通常,不需要对成员字段进行变异的结构方法应该是const。对于类,同样适用,但您也必须考虑任何可能重写该方法的派生方法——它们受相同限制的约束。

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

不可变构造函数

如果构造函数所做的只是将numden字段分配给它们各自的构造函数参数,那么该功能默认情况下已经存在于结构中:

struct S { int foo, bar; }
auto s = S(1, 2);
assert(s.foo == 1);
assert(s.bar == 2);

构造函数上的const没有多大意义,因为任何构造函数无论恒定性如何都可以构造const实例,因为所有东西都可以隐式转换为const。

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

然而,在您的案例中不需要不可变的构造函数,因为Rational没有任何间接性,所以可以使用可变的构造函数并复制结果。换句话说,没有可变间接性的类型可以隐式转换为不可变类型。这包括像intfloat这样的基元类型以及满足相同条件的结构。

没有效果的属性

放在声明中没有任何效果的属性会被所有当前编译器忽略。这是有意义的,因为使用attribute { /* declarations */ }attribute: /*declarations*/语法,属性可以同时应用于多个声明:

struct S
{
immutable
{
int foo;
int bar;
}
}
struct S2
{
immutable:
int foo;
int bar;
}

在上述两个示例中,foobar都属于immutable(int)类型。

使用类

有时不需要值语义,例如出于与频繁复制大型结构相关的性能原因。可以通过引用显式传递结构,例如使用refout函数参数或使用指针,但当值语义是默认值时,很容易出错,语法开销可能会很高。指针还有许多其他陷阱。

类是引用类型,不可能像对待值一样对待它们。它们通常是用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实现的实现。它使用不可变来防止numden的突变,而不管实例本身的可变性如何。方法应该是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。在该语言中有一个解决方案可以解决这个问题。

相关内容

  • 没有找到相关文章

最新更新