为什么在绑定中调用绑定值的 get 会抛出 stackoverflow 错误

Why does calling get of a bound value in the binding throw a stackoverflow error

此代码在获取被绑定标签的值时在标签绑定中抛出 Whosebugerror。我希望标签最初是 "test" 然后在第一次按 "test pressed" 然后 "test pressed pressed" 等等。但是,读取该值会引发 Whosebugerror,因为调用 getText() 方法会触发绑定。我希望只有按钮按下事件才能触发绑定。

注意:我注释掉了导致错误的代码并添加了另一个按钮以更好地显示我感到困惑的地方。

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application{

    @Override
    public void start(Stage primaryStage) {

        Label l = new Label("test");
        Button b = new Button("press me");

        l.textProperty().bind(Bindings.createStringBinding(() ->{
            System.out.println("changing label text");
            return "ok";
            //return l.getText() + " pressed"; //Causes a Whosebug error
        },b.pressedProperty()));

        Button b2 = new Button("press me 2");
        b2.pressedProperty().addListener((o) -> {
            l.getText(); //Why does this not triggger the binding?
        });

        VBox root = new VBox();
        root.getChildren().addAll(l,b,b2);

        Scene scene = new Scene(root, 300, 250);
        primaryStage.setTitle("Binding test");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args){
        launch(args);
    }
}

我的objective这里是有一个绑定,在一定条件下,不会改变文本。 Callable Lambda 中的逻辑类似于:

if(condition){
    return "ok";
}else{
    return l.getText(); //if the condition is not met then use the current value.
}

我知道我可以在按下的 属性 上使用监听器,然后以这种方式设置文本标签,所以我有一个解决方案,但我想知道为什么会出现上述情况。

只是在语义上,您的绑定表达了标签的文本是与" pressed" 连接的标签文本的规则。显然这是说标签的文本依赖于标签的文本,所以它是递归的。

无论如何,我认为这不是您要强加的规则。我想你想要 规则是“如果按钮没有被按下,标签的文本是 "test",如果按钮被按下,标签的文本是 "test pressed"。(现在,如果按钮的 pressed 属性 发生变化,但该值实际上并不取决于 属性。)

从技术上讲,发生的事情大致如下:

public class Label {

    private final StringProperty textProperty = new SimpleStringProperty() ;

    public String getText() {
        return textProperty().get();
    }

    // ...
}

public class SimpleStringProperty {

    private StringBinding binding ;
    private boolean bound ;
    private String value ;

    // ...

    public String get() {
        if (bound) {
            value = binding.get();
        }
        return value ;
    }

    public void bind(StringBinding binding) {
        bound = true ;
        this.binding = binding ;
        value = binding.get();
    }
}

最后,字符串绑定的逻辑如下:

public abstract class StringBinding {

    private boolean valid = false;
    private String value ;

    protected void bind(ObservableStringValue dependency) {
        dependency.addListener(o -> invalidate());
    }

    private void invalidate() {
        valid = false ;
        // notify invalidation listeners...
    }

    public String get() {
        if (!valid) {
            value = computeValue();
            valid = true ;
        }
        return value ;
    }

    public abstract String computeValue();
}

在您的示例中,computeValue() 的实现调用标签的 getText() 方法。

因此,当您创建绑定时,标签文本的值 属性 是根据绑定的值设置的。绑定无效(因为尚未计算),因此它是通过您提供的方法计算的。该方法调用 label.getText(),它从 属性 获取值。因为 属性 是绑定的,它检查绑定,它仍然无效(因为它的值的计算还没有完成),所以它计算它的值,调用 label.getText().. .

所以你可能想要这样的东西:

label.textProperty().bind(Bindings.createStringBinding(() -> {
    if (b.isPressed()) {
         return "test pressed";
    } else {
         return "test";
    }
}, b.pressedProperty());

如果你想让底层字符串能够改变,你需要为它创建一个新的属性:

StringProperty text = new SimpleStringProperty("test");
label.textProperty().bind(Bindings.createStringBinding(() -> {
    if (b.isPressed)() {
        return text.get() + " pressed" ;
    } else {
        return text.get();
    }
}, text, b.pressedProperty());

或者,等价地

label.textProperty().bind(text.concat(
    Bindings.when(b.pressedProperty())
    .then(" pressed")
    .otherwise("")));