Search code examples
vala

How to capture an int value in a closure?


I have an app with an arbitrary number of elements that all call a function that needs to take an argument, defined on creation. This is a simplified example but here I'd be hoping to make 3 buttons that print 0,1,2 but just makes 3 buttons that print 3.

    var application_window = new Gtk.ApplicationWindow (this);
    var grid = new Gtk.Grid ();

    for (int i = 0; i < 3; i++) {
        var button = new Gtk.Button() {expand=true};
        button.clicked.connect (() => {
                print(i.to_string());
        });

        grid.add(button);
    }

    application_window.add(grid);
    application_window.show_all ();

How can I change my app to print 123 instead?


Solution

  • Here is my base code:

    public class MyApplication : Gtk.Application {
        public MyApplication () {
            Object(application_id: "testing.my.application",
                flags : ApplicationFlags.FLAGS_NONE);
        }
    
        protected override void activate () {
            var application_window = new Gtk.ApplicationWindow (this);
            var grid = new Gtk.Grid ();
    
            for (int i = 0; i < 3; i++) {
                var button = new Gtk.Button() {expand=true};
                button.clicked.connect (() => {
                    print(i.to_string());
                });
    
                grid.add(button);
            }
    
            application_window.add(grid);
            application_window.show_all ();
        }
    
        public static int main (string[] args) {
            MyApplication app = new MyApplication ();
            return app.run (args);
        }
    }
    

    If you execute it like that, you get 333 as stdout.

    The problem is in the capturing code:

    for (int i = 0; i < 3; i++) {
        var button = new Gtk.Button() {expand=true};
        button.clicked.connect (() => {
            print(i.to_string());
        });
    

    The closure is capturing the variable i by location. That means when you change the i variable after creating the closures the change will be visibile in the closure as well.

    Other programming languages (e. g. C++) have explicit capture lists to avoid this problem.

    A quick and dirty solution would be using a local variable inside the scope of the loop:

    for (int i = 0; i < 3; i++) {
        var captured_i = i;
        var button = new Gtk.Button() {expand=true};
        button.clicked.connect (() => {
            print(captured_i.to_string());
        });
    

    This prints: 012 as intended.

    A better solution would be using a function that returns the closure as a delegate. I just tried that, but for some reason it does not work:

    public class MyApplication : Gtk.Application {
        public MyApplication () {
            Object(application_id: "testing.my.application",
                flags : ApplicationFlags.FLAGS_NONE);
        }
    
        delegate void ButtonClick();
    
        private ButtonClick make_print_event (int i) {
            return () => print (i.to_string());
        }    
    
        protected override void activate () {
            var application_window = new Gtk.ApplicationWindow (this);
            var grid = new Gtk.Grid ();
    
            for (int i = 0; i < 3; i++) {
                var button = new Gtk.Button() { expand=true };
                var print_event = make_print_event (i);
                button.clicked.connect (print_event);
                grid.add(button);
            }
    
            application_window.add (grid);
            application_window.show_all ();
        }
    
        public static int main (string[] args) {
            MyApplication app = new MyApplication ();
            return app.run (args);
        }
    }
    

    The compiler (valac-0.52) warns:

    three_buttons.vala:20.37-20.47: warning: copying delegates is not supported
    three_buttons.vala:20.37-20.47: warning: Connecting delegates to signals is experimental
                button.clicked.connect (print_event);
                                        ^^^^^^^^^^^