I've created a minimal example which reproduces a strange Vala behaviour, which I do not understand and would like to have explained.
The constructor of class Test
takes a Func
and uses it to initialize its class member f
:
public class Test
{
public delegate int Func();
public static Func FUNC_0 = () => { return 0; };
public Func f;
public Test( Func f )
{
this.f = f; // line 10
}
}
I instantiate a Test
object using the Func
defined in Test.FUNC_0
, and do a few tests:
public static void main()
{
assert( Test.FUNC_0 != null ); // first assert
var t = new Test( Test.FUNC_0 );
assert( t.f != null ); // second assert
}
Now what's strange about this?
Test.FUNC_0
is null
. How can that be?!valac
gives me a warning that "copying delegates is not supported", but in line 10, which is the this.f = f
assignment, so this warning doesn't regard the Test.FUNC_0
field.assert
and replace the Test.FUNC_0
argument of new Test
by () => { return 0; }
, then the second assert
passes. So what's wrong with this.f = f
in line 10? Is the closure in line 10 copied or not?Test
?I'd really appreciate to see this explained. The valac
version is 0.28.1.
Your problem is actually nothing to do with delegate and everything to do with singly-owned instances. All non-primitive types in Vala are either owned or unowned. Some classes (including the ones that derive from GLib.Object
) can have multiple owners. When a copy of the class is needed, the reference count on he target class is incremented. Other classes (including string
) and structs can only have a single owner but come with a copy function that allows generating a duplicate of that class. Delegates and certain classes (like FileStream
) also only have a single owner, but can't be copied.
The reason delegates can't be copied is that a delegate is three pieces of information: a callback function, some context data, and, maybe, a destructor for the context data. There's no copy function.
Since parameters are unowned by default, this.f = f
, is trying to copy a delegate, which it does not own, into a reference that it does. This would be memory unsafe since it would either hold the reference past the life cycle of the object or the destructor could be called twice.
You have two options:
public class Test
{
public delegate int Func();
public static Func FUNC_0 = () => { return 0; };
public Func f;
public Test( owned Func f )
{
this.f = (owned) f; // you are just moving the delegate, not copying it.
}
}
alternatively:
public class Test
{
public delegate int Func();
public static Func FUNC_0 = () => { return 0; };
public unowned Func f;
public Test( Func f )
{
this.f = f; // You are copying an unowned reference to another
// unowned reference, which is fine. Memory management is now your
// job and not Vala's.
}
}
This second one is a bit dangerous. For instance, the following code will compile and corrupt memory:
Test? t = null;
if (true) {
int x = 5;
Func f = () => { return x; };
t = new Test(f);
}
t.f();