D opEquals() 和 opCast() 给我一个分段错误

D opEquals() and opCast() give me a Segmentation Fault

我正在尝试用 D 编程语言编写表示全球贸易识别号 (GTIN) 的类型库。有四种 GTIN,每种都以数字本身的长度命名:GTIN8GTIN12GTIN13GTIN14。其中每一个都被建模为 class 继承自抽象 GTIN class.

在摘要 GTIN class 中,我覆盖了 opEquals 运算符来比较任何类型的两个 GTIN。由于所有较小的 GTIN 都可以映射到更宽的 GTIN14,因此比较中的每个 GTIN 首先转换为 GTIN14,然后再进行比较。

在我比较一个或多个 GTIN13 之前一切正常(或者可能是较小的 GTIN 类型),在这种情况下,我会遇到分段错误。

一切都从这个单元测试开始:

GTIN13 gtin1 = new GTIN13("123456789012");
GTIN13 gtin2 = new GTIN13("123456789012");
assert(gtin1 == gtin2);

运算符 opEquals() 的签名为 Object.opEquals(Object o),将签名更改为其他任何内容都不会覆盖 ==。在我的例子中,这变成了对 gtin1.opEquals(cast(Object) gtin2) 的调用。我的 opEquals 覆盖看起来像这样(减去累积的注释掉的代码和调试语句):

public override @trusted
bool opEquals(Object other)
{
    GTIN14 a = cast(GTIN14) this;
    GTIN14 b = cast(GTIN14) other;

    for (int i; i < a.digits.length; i++)
    {
        if (a.digits[i] != b.digits[i]) return false;
    }
    return true;
}

如您所见,每个 GTIN 在函数的开头都被转换为 GTIN14;但是,other 首先转换为 Object.

public
GTIN14 opCast(GTIN14)()
{
    string currentGTINString = this.toString()[0 .. $-1];
    while (currentGTINString.length < 13)
        currentGTINString = ('0' ~ currentGTINString); 
    return new GTIN14(currentGTINString);
}

另外,朋友writeln()告诉我,GTIN14 b = cast(GTIN14) other;行执行后,b就是null。 (该行之前不为空。)

所以,总而言之,问题似乎是将 GTIN14 以外的任何类型的 GTIN 转换为 Object,然后以某种方式返回 GTIN14 完全删除对象。这是一个错误吗?这是我的代码有问题吗?有没有不影响代码质量的解决方法?

如果能得到任何帮助,我将不胜感激。

好的,我有几条来自你的 pastebin 的评论。

public abstract
class GlobalTradeItemNumber
{
    // this could prolly just return _digits.length too.
    public abstract @property
    size_t length();

我在这里的一般评论是,您的代码可以通过从数组中借用长度来稍微简化....或者您也可以使用长度作为参数来做类似模板化 class 的事情.

不过,我稍后会回过头来,首先,让我们修复您的代码。继续 class:

public
/*
    Note #1:

    OK, this line is wrong:
    //GTIN14 opCast(GTIN14)()

    It should probably be:
*/
GTIN14 opCast(T : GTIN14)()
{
    string currentGTINString = this.toString()[0 .. $-1];
    while (currentGTINString.length < 13)
        currentGTINString = ('0' ~ currentGTINString);
    return new GTIN14(currentGTINString);
}

// we also need one for converting to the other sizes
// this could also be done with metaprogramming
GTIN13 opCast(T : GTIN13)()
{
    string currentGTINString = this.toString()[0 .. $-1];
    while (currentGTINString.length < 12)
        currentGTINString = ('0' ~ currentGTINString);
    return new GTIN13(currentGTINString);
}

该行错误的原因是它实际上定义了一个名为 GTIN14 的本地模板参数,它可以是 anything,并隐藏了外部名称!

因此,我在这里的更改创建了一个新名称 T,然后专门用于仅匹配来自外部的 GTIN14 名称。参见:http://dlang.org/spec/template.html#parameters_specialization

但是,它只适用于 GTIN14,所以我还添加了第二个函数,它也适用于 GTIN13。

如果您使用的是模板化 class,专业化也可以提取长度,您将拥有一个函数来完成所有这些。或者,如果长度作为每个子项中的编译时常量(枚举)class,您也可以从那里提取它。

无论如何,只需使用专业化语法 (T : class_name) 并为其他子classes 添加函数即可修复当前代码。

public
ulong toNumber()
{
    ulong result;
    // a note here: I would probably just have
    // int exponent = 1;
    // then exponent *= 10; in each loop instead of pow each time.
    for (size_t i = this.length-1; i > 0; i--)
    {
        result += (this._digits[i] * (10^^(this._digits.length-i)));
    }
    return result;
}

好吧,没什么可说的,你的代码有效,我只是写得有点不同。累加器变量模式会稍微优化它。当然,这里不是很重要,只是要记住一些。

/*
    Note #2: this is where things get broken
*/
public override @trusted
bool opEquals(Object other)
{
    /*
    these are actually two different kinds of casts
    GTIN14 b = cast(GTIN14) other; // does a generic dynamic cast!
    GTIN14 a = cast(GTIN14) this; // actually calls the opCast
    */

    GTIN obj = cast(GTIN) other;
    if(obj is null) // which might return null because Object is not necessarily an instance of your class
        return false; // definitely not a match

    GTIN14 b = cast(GTIN14) obj;
    GTIN14 a = cast(GTIN14) this;

    for (int i; i < a.digits.length; i++)
    {
        if (a.digits[i] != b.digits[i]) return false;
    }
    return true;
}

public override @trusted
int opCmp(Object other)
{
    // GTIN14 that = cast(GTIN14) other; // a generic dynamic cast!
    GTIN obj = cast(GTIN) other;
    if(obj is null) // which might return null because Object is not necessarily an instance of your class
        return -1; // so return something indicating not a match
    GTIN14 that = cast(GTIN14) obj; // now you can use your custom cast
    const ulong thisNumber = this.toNumber();
    const ulong thatNumber = that.toNumber();
    if (thisNumber == thatNumber) return 0;
    return ((thisNumber / 10u) > (thatNumber / 10u) ? 1 : -1);
}

子class中的其他opCmp犯了同样的错误,可以用同样的方法修复。

但这是造成您麻烦的主要原因 - 相同的 cast 语法实际上做了两件不同的事情!

看,other 的静态类型是泛型基础 class Object,因此它还不知道您的自定义转换函数。 (opCast,作为模板,不能是虚拟的,因此不像其他修改泛型函数行为的函数那样 override

相反,它使用通用的 dynamic_cast(这是 C++ 名称,D 只是将它们全部称为 cast,但它是相同的概念,因此您可以通过搜索C++ 术语,如果你愿意的话)。它尝试使用运行时类型标记将基 class/interface 转换回子 class。如果它确实是那个 class 的一个实例(或者它自己的子 classes 之一),那么转换成功并且你得到了引用。否则,它 returns null。这是您的段错误的原因。

另一方面,cast(xxx) this 已经知道它是 GTIN(或其子classes 之一)的实例,因此它能够使用您的自定义opCast 转换。因此,您的 a 变量调用了正确的函数并正确填充,但您的 b 变量将是 null... 除非您实际上正在比较两个 GTIN14 实例.然后动态转换会成功,但其他 classes.

因此,解决方法是首先将通用 Object other 转换回您的 GTIN 基础 class,检查 null(如果用户写像 GTIN14 a = new GTIN14("xxx"); Object b = new Object(); assert(a == b); /* uh oh, b is an Object, so it should just return null */.

顺便说一句,在与 null 进行比较时,通常应该使用 a is null 而不是 a == b,因为如果 a 本身为 null,则在尝试访问虚拟 opEquals 函数时会崩溃!

无论如何,在将其转换回 GTIN 之后,您可以再次转换 并调用转换函数。

或者,您也可以在基 class 中使用不同的命名函数,例如 toGTIN14,它通常会进行转换,您只需从基 class 的每个实例中调用它=] 并以这种方式转换它们,而不是使用 cast 关键字。这实际上是我写的方式 - 这是我的偏好,但两种方式都有效。

opEqualsopCmp,在实现它们的任何 classes 中,都需要遵循相同的模式。在 opEquals 中你可以看到 I return false 当它为 null 时,因为如果它们甚至不能转换为普通类型,它们显然是不相等的!

但在 opCmp 中,你不想 return 0 因为这意味着相等,但实际上 return 对我来说是个谜....我只是did -1 这样该数组中的所有其他对象都会更早排序,但也许您有更好的主意。我不知道什么是最好的。

无论如何,进行这些更改应该可以修复您的代码。

最后,作为奖励,这里是通用模板化的实现 class:

alias GTIN14 = GlobalTradeItemNumberImpl!14;
alias GTIN13 = GlobalTradeItemNumberImpl!13;
public
class GlobalTradeItemNumberImpl(int size) : GlobalTradeItemNumber
{
    public override @property
    size_t length()
    {
        return size;
    }

    this(string digits)
    {
        super(digits);
    }
}

如果您曾经看过 Phobos 的一些内部结构,您会在 std.base64std.digest.

中看到类似的模式

现在,所有功能实际上都在基础 class 中。您可以在基础 class:

中重写 opCast
T opCast(T : GlobalTradeItemNumberImpl!N, int N)()
{
    string currentGTINString = this.toString()[0 .. $-1];
    while (currentGTINString.length < (N-1))
        currentGTINString = ('0' ~ currentGTINString);
    return new T(currentGTINString);
}

那里的专业化使用"pattern matching" 形式 #7 中描述的:http://dlang.org/spec/expression.html#IsExpression 来捕获任何随机 N,并提取它在函数内部使用的内容。

如果您有兴趣,我们还可以进行其他优化,例如使用 ~ 运算符,或者甚至可以将其从 classes 更改为 struct 使用alias this 结合常用功能..但如果你愿意,我会让你玩:)