In the previous
post, we introduced a concept about
getter and
setter methods. Basically, they provide safer access to the class members. However, it is absolutely not necessary to have
getters and/or
setters for all class member. The decision to have or not to have
getters and
setters should be driven by the class design logic. Here, we will review two approaches to implement
get/set methods. Let's start with an example where public API available to serve our
getter and
setter purpose. We continue our work on the
MyPoint class expanding it. Let's add a public API to access our
name member. As a reminder, our public structure should be:
1
2
3
4
5
6
| typedef struct {
gint x;
gint y;
gchar *name;
/* stuff */
} MyPointPrivate;
|
Which corresponds to the derivable class from the part 2. For public API, we need to declare our methods in the header file and define them in the source file. The declaration in the header file is pretty straightforward and will be:
1
2
| void my_point_set_name (MyPoint *self, const gchar *name);
const gchar *my_point_get_name (MyPoint *self);
|
The definition in the source file should be:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| void
my_point_set_name (MyPoint *self,
const gchar *name)
{
g_return_if_fail (self);
MyPointPrivate *priv = my_point_get_instance_private (self);
g_free (priv->name);
priv->name = g_stdup (name);
}
const gchar*
my_point_get_name (MyPoint *self)
{
g_return_val_if_fail (self, NULL);
MyPointPrivate *priv = my_point_get_instance_private (self);
return priv->name;
}
|
Did you notice some extra calls on line #5 and #16? They are common checkers for method parameters. Without those checkers, we may pass a NULL as the
self argument and it will cause an error. Since we are trying to dereference it in the function. With these checkers, we know for sure that self is not NULL. GObject provides an alternative and a general approach to accomplish the same task. It is called
GObject properties. How it works? We introduce a property for an object and associate that property to a class member. After that, when we manipulate the property we manipulate our class member. Let's implement a property for the class member
name for our
MyPoint class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| enum {
PROP_0,
PROP_NAME,
N_PROPERTIES
};
static GParamSpec *properties[N_PROPERTIES] = {NULL};
static void
my_point_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
MyPoint *self = MY_POINT(object);
MyPointPrivate *priv = my_point_get_instance_private (self);
switch (property_id)
{
case PROP_NAME:
g_free (priv->name);
priv->name = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
my_point_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
MyPoint *self = MY_POINT(object);
MyPointPrivate *priv = my_point_get_instance_private (self);
switch (property_id)
{
case PROP_NAME:
g_value_set_string (value, priv->name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
my_point_class_init (MyPointClass *klass)
{
GObjecClass *cobject = G_OBJECT(klass);
cobject->get_property = my_point_get_property;
cobject->set_property = my_point_set_property;
properties[PROP_NAME] = g_param_spec_string ("name",
"Name",
"Name of our point",
"Default value",
G_PARAM_READWRITE));
g_object_class_install_properties (cobject,N_PROPERTIES,properties);
}
|
Let's review this code line-by-line. In the beginning, we declare an enumerator that defines all properties. The property with value "0" is reserved for internal
GObject use and should not be used in the user-written code. We can even remove it from our declaration and set the next member to have value "1":
1
2
3
4
| enum {
PROP_NAME = 1,
N_PROPERTIES
};
|
On the line #7 (previous block) we declare an array to hold our properties and set all values to NULL. In the next two blocks, we define static functions to implement a code for our object to work with our properties. At the beginning of the function's body, we get an object to the private structure and then use the switch statement to work with each property based on passed property ID, which in fact enumerator values defined earlier. Pay attention to the object type we pass to our static methods. It is
GValue. Because of that, it is very general and a user only needs to implement how to set/get values to the
GValue variable. Exactly what we have in our switch statement.
GObject class defines two pointers to functions that we need to set using our static functions:
set_property and
get_property (lines #56 and #57). The next step would be setting values to our
properties array. We should do this in our ..._class_init function. One-by-one, we need to set values for every element of our array.
g_param_spec_... family of functions provides a convenient mechanism to create a
GParamSpec instance. The user should use an appropriate function that corresponds to the type of member. In our case it is
const gchar* , therefore, we use
g_param_spec_string method. Please see a full list of the available methods in the official
documentation. In the last step (line #65), we register our array with properties. That's it. Now, if we want to get a value of our
name class member we need to use
g_object_get() method and to set a new value for name, we should use
g_object_set() method:
1
2
3
4
5
| gchar *buffer;
MyPoint *point;
g_object_get (point,"name",&buffer,NULL);
g_object_set (point,"name",buffer,NULL);
|
The code should be self-explained. The first argument for both methods is our
MyPoint instance. The second argument is our property name. To get a value we need to pass a reference to an object where we would like to have a copy. To set a new value we need to pass a regular object instance. We can repeat this step and get as many as we need properties at once. Just add more property-value pairs. In the end, we should terminate with NULL.
As you can see, two approaches are very similar for API user and can be used interchangeably as soon as an appropriate code to define properties was implemented. For library development, the generic approach is favorable since it provides a better binding implementation for other languages.