11

刚从 Qml 中调用重载的 C++ 方法并试图了解其背后的原因时,遇到了 Qt 框架的奇怪行为。假设我有一个QList<QVariant>具有以下方法的 -like 类:

...
Q_SLOT void append(const QVariant &item);
Q_SLOT void append(const QVariantList &items);
Q_SLOT void insert(int index, const QVariant &item);
Q_SLOT void insert(int index, const QVariantList &items);
...

Qml:

onclicked: {
  var itemCount = myListObject.size();
  myListObject.insert(itemCount, "Item " + (itemCount + 1));
}

Qt 以某种方式决定调用参数设置为void insert(int index, const QVariantList &items)重载,而不是使用Wrapped in的重载。itemsnull QVariantQVariantListvoid insert(int index, const QVariant &item)QStringQVariant

现在,如果我按如下方式更改声明的顺序,它会按预期工作:

Q_SLOT void insert(int index, const QVariantList &items);
Q_SLOT void insert(int index, const QVariant &item);

我在 Qt 框架的文档中找不到任何关于声明顺序以及它如何影响调用期间解析方法的方式的信息。

有人可以解释一下吗?谢谢你。

4

2 回答 2

4

这个问题与 JavaScript 中的重载有关。一旦你了解它 - 你就会理解代码“奇怪行为”的原因。只需看看Javascript 中的函数重载 - 最佳实践

简而言之——我建议你接下来做:因为你可以QVariant在两边操作变量(QML 和 Qt/C++)——将变量作为参数传递,并根据需要在 Qt/C++ 端处理它。

你可以使用这样的东西:

您的 C++ 类创建并传递给 QML(例如 as setContextProperty("TestObject", &tc)):

public slots:
    Q_SLOT void insert(const int index, const QVariant &something) {
        qDebug() << __FUNCTION__;
        if (something.canConvert<QString>()) {
            insertOne(index, something.toString());
        } else if (something.canConvert<QStringList>()) {
            insertMany(index, something.toStringList());
        }
    }

private slots:
    void insertOne(int index, const QString &item) {
        qDebug() << __FUNCTION__ << index << item;
    }

    void insertMany(int index, const QStringList &items) {
        qDebug() << __FUNCTION__ << index << items;
    }

在您的QML中的某处:

Button {
    anchors.centerIn: parent
    text: "click me"
    onClicked: {
        // Next line will call insertOne
        TestObject.insert(1, "Only one string")
        // Next line will call insertMany
        TestObject.insert(2, ["Lots", "of", "strings"])
        // Next line will call insertOne
        TestObject.insert(3, "Item " + (3 + 1))
        // Next line will call insertOne with empty string
        TestObject.insert(4, "")
        // Next line will will warn about error on QML side:
        // Error: Insufficient arguments
        TestObject.insert(5)
    }
}
于 2015-02-15T20:21:12.133 回答
1

这个问题与 JavaScript 中的重载有关。一旦你了解它 - 你就会理解代码“奇怪行为”的原因。

据我了解,JS 没有 ECMA 规范中定义的重载支持。在各个地方都建议使用 JS hacks 来模拟重载支持。

关于 Qt 中行为的正确答案如下:

订单相关行为仅作为注释记录在源代码中。请参阅 qtdeclarative 模块中的 qv4qobjectwrapper.cpp。

Resolve the overloaded method to call.  The algorithm works conceptually like this:
    1.  Resolve the set of overloads it is *possible* to call.
        Impossible overloads include those that have too many parameters or have parameters
        of unknown type.
    2.  Filter the set of overloads to only contain those with the closest number of
        parameters.
        For example, if we are called with 3 parameters and there are 2 overloads that
        take 2 parameters and one that takes 3, eliminate the 2 parameter overloads.
    3.  Find the best remaining overload based on its match score.
        If two or more overloads have the same match score, call the last one.  The match
        score is constructed by adding the matchScore() result for each of the parameters.

实施:

static QV4::ReturnedValue CallOverloaded(const QQmlObjectOrGadget &object, const QQmlPropertyData &data,
                                         QV4::ExecutionEngine *engine, QV4::CallData *callArgs, const QQmlPropertyCache *propertyCache,
                                         QMetaObject::Call callType = QMetaObject::InvokeMetaMethod)
{
    int argumentCount = callArgs->argc();

    QQmlPropertyData best;
    int bestParameterScore = INT_MAX;
    int bestMatchScore = INT_MAX;

    QQmlPropertyData dummy;
    const QQmlPropertyData *attempt = &data;

    QV4::Scope scope(engine);
    QV4::ScopedValue v(scope);

    do {
        QQmlMetaObject::ArgTypeStorage storage;
        int methodArgumentCount = 0;
        int *methodArgTypes = nullptr;
        if (attempt->hasArguments()) {
            int *args = object.methodParameterTypes(attempt->coreIndex(), &storage, nullptr);
            if (!args) // Must be an unknown argument
                continue;

            methodArgumentCount = args[0];
            methodArgTypes = args + 1;
        }

        if (methodArgumentCount > argumentCount)
            continue; // We don't have sufficient arguments to call this method

        int methodParameterScore = argumentCount - methodArgumentCount;
        if (methodParameterScore > bestParameterScore)
            continue; // We already have a better option

        int methodMatchScore = 0;
        for (int ii = 0; ii < methodArgumentCount; ++ii) {
            methodMatchScore += MatchScore((v = QV4::Value::fromStaticValue(callArgs->args[ii])),
                                           methodArgTypes[ii]);
        }

        if (bestParameterScore > methodParameterScore || bestMatchScore > methodMatchScore) {
            best = *attempt;
            bestParameterScore = methodParameterScore;
            bestMatchScore = methodMatchScore;
        }

        if (bestParameterScore == 0 && bestMatchScore == 0)
            break; // We can't get better than that

    } while ((attempt = RelatedMethod(object, attempt, dummy, propertyCache)) != nullptr);

    if (best.isValid()) {
        return CallPrecise(object, best, engine, callArgs, callType);
    } else {
        QString error = QLatin1String("Unable to determine callable overload.  Candidates are:");
        const QQmlPropertyData *candidate = &data;
        while (candidate) {
            error += QLatin1String("\n    ") +
                     QString::fromUtf8(object.metaObject()->method(candidate->coreIndex())
                                       .methodSignature());
            candidate = RelatedMethod(object, candidate, dummy, propertyCache);
        }

        return engine->throwError(error);
    }
}

我发现 Qt 4.8 文档 ( https://doc.qt.io/archives/qt-4.8/qtbinding.html )中提到了重载支持。它没有详细说明:

QML 支持调用重载的 C++ 函数。如果有多个同名但参数不同的 C++ 函数,将根据提供的参数的数量和类型调用正确的函数。

请注意,Qt 4.x 系列确实很旧并且已归档。我看到 Qt 5.x 源代码中也存在相同的注释:

src/qml/doc/src/cppintegration/exposecppattributes.qdoc:QML supports the calling of overloaded C++ functions. If there are multiple C++

我同意“按设计”具有这种顺序依赖逻辑真的很奇怪。为什么这是理想的行为?如果您非常仔细地阅读该函数的源代码,就会发现其中有更多的惊喜(至少最初是这样)。当您在调用站点提供的参数太少时,您会收到一条列出可用重载的错误消息,当您提供太多参数时,额外的参数将被忽略。例子:

Q_INVOKABLE void fooBar(int) {
    qDebug() << "fooBar(int)";
};
Q_INVOKABLE void fooBar(int, int) {
    qDebug() << "fooBar(int, int)";
};

Qml 调用站点。

aPieChart.fooBar();

错误消息。

./chapter2-methods 
qrc:/app.qml:72: Error: Unable to determine callable overload.  Candidates are:
    fooBar(int,int)
    fooBar(int)

Qml 调用站点。

aPieChart.fooBar(1,1,1);

运行时输出(最后一个 arg 被忽略,因为选择了两个 arg 重载):

fooBar(int, int)

根据如果我调用具有比定义接受的更多参数的 JS 方法会发生什么?传递太多参数是一种有效的语法。

还值得注意的是,重载机制支持默认参数值。详情如下:

Qt Quick 中的实现依赖于 Qt 元对象系统来枚举函数重载。元对象编译器为每个具有默认值的参数创建一个重载。例如:

void doSomething(int a = 1, int b = 2);
             

变为(CallOverloaded() 将考虑所有这三个):

void doSomething();
void doSomething(a);
void doSomething(a, b);
             
于 2021-01-10T20:21:37.387 回答