在D中实现值对象模式

fre*_*low 6 design-patterns d const immutability value-objects

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

我对D constimmutableD 之间的区别感到困惑.让我用骷髅Rational课来说明我的疑惑:

class Rational
{
    int num;
    int den;
Run Code Online (Sandbox Code Playgroud)

我应该声明numden作为constimmutable?整数有区别吗?

    invariant()
    {
        assert(den > 0);
        assert(gcd(abs(num), den) == 1);
    }
Run Code Online (Sandbox Code Playgroud)

我应该声明invariantconstimmutable?将其标记为immutable导致编译时错误,但这可能是由于其他成员未被标记immutable.

    this(int numerator, int denominator) { ... }
Run Code Online (Sandbox Code Playgroud)

我应该将构造函数声明为constimmutable?那是什么意思?

    string toString()
    {
        return std.string.format("(%s / %s)", num, den);
    }
}
Run Code Online (Sandbox Code Playgroud)

我应该声明toStringconstimmutable

我似乎也可以标记整个班级,而不是标记个别成员:

class Rational
const class Rational
immutable class Rational
Run Code Online (Sandbox Code Playgroud)

哪些对价值对象模式最有意义?

怎么样pure?在值对象模式中,方法应该没有副作用,因此将每个成员声明为有意义pure吗?不幸的是,标记toStringpure不编译,因为std.string.format它不是纯粹的; 这有什么特别的原因吗?

我似乎也可以将类本身声明为pure,但这似乎没有任何影响,因为编译器不再抱怨toString调用不纯函数.

那么宣布一个课是什么意思pure?它被忽略了吗?

jA_*_*cOp 15

D结构

通过简单地使用结构及其内置值语义,最好在D中表示值对象模式.

据我所知,由于Java目前缺乏具有值语义的内置聚合,因此值对象模式通常在Java中使用.

D的结构与C和C#中的结构类似,以及C++中的结构和类.对于后者,这种比较可能是最好的,因为D结构具有构造函数和析构函数,但有一个重要的例外:没有继承和虚函数; 这些特性被委托给,它们的工作方式与Java和C#中的类很相似(它们是隐式引用类型,因此它们从不表现出切片问题).

struct Rational
{
    int num;
    int den;

    /* your methods here */
}
Run Code Online (Sandbox Code Playgroud)

然后,Rational的实例总是按值传递(除非参数显式指定,参见ref和out)到函数并在赋值时复制.

纯度

纯函数无法读取或写入任何全局状态.允许纯函数改变显式参数以及方法的隐式this参数; 因此,Rational上的方法可能总是如此pure.

std.string.format不是pure它当前实现的问题.它将在未来使用不同的实现pure.

Const和不变的

如果你想表达的方法是纯粹的,也没有发生变异自己的状态,你可以把它都pureconst.

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

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

推杆constimmutable上一个structclass声明等效其所有成员(包括方法)标记的constimmutable分别.

不可改变的建设者

如果你的所有构造函数都将num和和den字段分配给它们各自的构造函数参数,那么默认情况下这个功能已存在于结构上:

struct S { int foo, bar; }

auto s = S(1, 2);
assert(s.foo == 1);
assert(s.bar == 2);
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

在上述两个实施例中,foobar为类型immutable(int).

使用班级

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

类是引用类型,不可能像值一样对待它们.它们通常使用实例化new,它总是创建一个GC分配的类实例(new不推荐重载).这两点使D中的类与Java和C#中的类非常相似(另一个值得注意的点是有接口而不是多重继承).但是,类具有隐藏字段的开销(当前size_t.sizeof * 2所有类的字节)并且未指定字段的ABI,但是当需要继承和虚函数时,类也是唯一的选项.

这是为Value Object Pattern实现的Rational:

class Rational
{
    immutable int num;
    immutable int den;

    this(int num, int den)
    {
        this.num = num;
        this.den = den;
    }

    /* methods here */
}
Run Code Online (Sandbox Code Playgroud)

这是最忠实于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 */
}
Run Code Online (Sandbox Code Playgroud)

然后,用户可以选择是否使用Rationalimmutable(Rational).后者可以使用std.concurrency线程接口在线程之间安全地传递,而尝试发送前者将在编译时被拒绝.

但是,后者有一个明显的问题 - 因为Rational隐式地是一个引用类型,没有办法输入对Rational的不可变实例的可变引用.此问题的当前解决方案是使用std.typecons.Rebindable.有一种解决方案可以用语言来解决这个问题.