QML: Lambda function works unexpectedly

2019-04-06 12:04发布

I thought QML supported lambda functions because of JavaScript's support of anonymous functions and the fact that functions are first class objects, but they don't work how I expected. Take this code:

Item {
    property var items: []

    function handler( item ) {
        console.log( item );
    }

    Component.onCompleted: {
        for ( var i = 0; i < 3; ++i ) {
            var item = someObj.createObject();
            item.someValueChanged.connect( function() {
                handler( item ); } );

            items.push( item );
            console.log( "Adding:", item );
        }
    }

    Component {
        id: someObj

        Item {
            property bool someValue: false

            Timer {
                running: true
                onTriggered: {
                    parent.someValue = true;
                }
            }
        }
    }
}

I'm trying to use the lambda function() { handler( item ); } so that when the someObj::someValueChanged signal is emitted the emitting item is passed to the handler( item ) function.

I assumed that each loop would create a new instance of the lambda and that the item reference would carry the reference of the someObj instance created in that loop (i.e. item would be captured by the lambda). But that doesn't seem to be the case as the output is:

qml: Adding: QQuickItem_QML_1(0x2442aa0)
qml: Adding: QQuickItem_QML_1(0x2443c00)
qml: Adding: QQuickItem_QML_1(0x2445370)
qml: QQuickItem_QML_1(0x2445370)
qml: QQuickItem_QML_1(0x2445370)
qml: QQuickItem_QML_1(0x2445370)

As you can see, either the whole function is being replaced on each loop or just the item reference, so that ultimately only the last created someObj is referred to. Can someone explain to me why lambdas (if that's even what it is) don't work the way I expect? And is this a QML issue, or a general JavaScript one?

2条回答
Emotional °昔
2楼-- · 2019-04-06 12:50

Try something like this:

item.someValueChanged.connect(function(capture) {
    return function() {
        handler(capture)}
}(item))

Intuitive, right? :D

If JS used "block scope" there would be 3 different items being referenced for each loop iteration, and it would "work as expected". But with "function scope" there is only one item referenced, and it references its final value, thus the need to use that hack to "capture" each value in time.

Just to explain it, in case it isn't immediately obvious, the signal is connected to a handler that is arbitrated by a function which captures the parameter value at the particular time as a discrete object, which is used to feed to the handler.

Hopefully, the incipient Qt 5.12 release will remedy that with the introduction of support for let, a.k.a block scoped variables.

Update: I can confirm that using 5.12, it now works as expected:

let item = someObj.createObject(); // will produce 3 distinct obj refs
查看更多
我命由我不由天
3楼-- · 2019-04-06 12:57

dtech's answer solves the problem (thanks!) but we can simplify it so that it is clearer as to what exactly is going on. The 'inner' function needs to be anonymous but there is no need for the 'outer' one to be. Using a normal function for the outer one makes the code a lot easier to understand and easier for it to be self-documenting since the function has a name. The outer function creates a signal handler so the equivalent code can be:

var signal_handler = create_signal_handler(item);
item.someValueChanged.connect(signal_handler);

...

function create_signal_handler(item)
{
    return function() { return handler(item); }
}
查看更多
登录 后发表回答