Beginners Guide to Types as Objects
Introduction
This tutorial is aimed at people who want to know more about the new features added to Type, commonly being referred to as 'types as objects', and 'that OOP stuff'. It aims to walk you through these new features, so is aimed at people who don't really understand it yet, but want to learn. A Type in FreeBASIC is an aggregate data type, like a struct in C, or a record in Pascal. Here's just a short sample of typical Type usage.
In this usage it's used as a kind of container for related data; in this example it could be as an entry in an address book. With the new features, however, it can be used more like the class in C++, in that it can do much more than contain just simple fields of data. It becomes a way to express an idea of an object, and this makes object oriented programming much simpler. We will now look at these new features.
Property
We'll start by looking at property. When you add a property to a Type, you access it as if it were an ordinary member, but what happens, is instead of just getting or setting a variable as normal, it calls a function instead. Take a look at this example:
We include in our Type some declarations for a Property; they are very similar to ordinary function declarations. The first one declares a getter, the second a setter. The p_x member is just an ordinary Integer member.
Next we write the code for the properties; again, the syntax is very similar to that of normal functions. Note the way we return a value: instead of Function = value, we do Property = value. You can do Return value as well. Also note that you can refer to the member directly as p_x; you can also use the keyword this, for example this.p_x = n; using this isn't usually needed, but it can help in some ambiguous circumstances.
Then follows some testing code; this shows how we can use the property as if it were any ordinary member. When you run the program it will also print to screen to show that the property get/set code has been called.
Now this code is fairly trivial, but as you get used to the idea you'll see it can be put to some good uses. Imagine as an example you are writing a GUI, and the TYPE represents a button on the screen, you could do button.text = "Hello World!", and make the property code update the screen to show the changes. Or maybe you are using the Type to maintain some kind of list; you could do list.size += 10 and then put some code in your property to make the list larger.
Constructor/Destructor
Constructors are functions that are called when the Type gets created - when you use Dim, for example. A Destructor is a function that gets called when the Type goes out of scope; this could be when the program ends, for a Type in the main code, or when a function ends, for a local Type. Look at the following example, expanded from the last.
Again the syntax is somewhat similar to normal functions. Note that this time I changed p_x to be an Integer ptr. The constructor then Allocates the memory for this when foo is created, and gives it a default value; then it De-Allocates this memory once it is destroyed. So you can use Constructors and Destructors to set things up for you, then clean up once its finished with. Again a trivial example, but bring back the example of some kind of list, and having it set the list up for you, and clean it up when it's finished with can be quite handy.
Methods
You can also have regular Subs and Functions inside your Type; in some terminology, these are referred to as methods. We'll carry on our example:
So this time we added a Sub, that multiplies the integer pointed to by p_x by five, and a function that gets the memory address that the pointer holds.
Private/Public
By default all of the members of the bar type are public; that means that we can read/write or call them. However, sometimes you might want to make them private. Take for example our member p_x; we can currently do Print *foo.p_x, and it will allow us to print the value it points to. We might want to make it private, so that only the members of the bar type (the constructor, destructor, property, and methods) can access it. That way we can make sure we only deal with p_x by the ways we choose. If for example we did 'DeAllocate(foo.p_x)' in our main code, then when the destructor runs, it would try to free it again, known as a 'double free'. Change the Type declaration as follows:
Now try adding Print *foo.p_x to the main code and compile it. You'll get a message from fbc "error 173: Illegal member access, found 'p_x' in 'Print *foo.p_x'", showing that indeed the compiler is now enforcing the fact we made p_x private. When you use private: or public:, any members following that statement follow the rule. Here's a rather pointless example just to show the syntax:
In the above type, the members a, b, and e are private; c and d are public.
Operator overloading
Operator overloading is a way of telling the compiler what to do in the case where we want to perform some kind of operation involving our Type. Take this example:
Now normally the compiler will throw an error when it sees this, as it has no idea how to add together two Types, but we can define what we want to happen. Here's how:
In this code, I use lhs and rhs to refer to the left and right hand side operands of the operator. Note also the expression type(lhs.n + rhs.n); this builds the Type that will be returned. If you had a type like:
Then you would build it like type(xpart, ypart, zpart).
Most or all operators can be overloaded, and most of them are binary ops, meaning they have two operands like the + example above. Some are unary ops having only a right hand side, like Not and unary minus. They would be done like 'Operator Not(ByRef rhs As bar) As bar'.
There are some special cases where they have to be declared inside the Type; these are the assignment operators and casts.
Assignment operators are things like += -= mod= etc, and also Let. Let is used when you do an assignment like:
And casts are kind of the reverse; they are used when you cast to another datatype like:
Heres a short example using Let and Cast:
You need to have separate lets and casts for each data type you want to support. The operators that need declaring within the type are known as non-static, and the ones that don't are known as global. There is a technical reason for this; the non-static ones need to know which instance (in the technical jargon, in our example above, we would say that x is an instance of bar) of the Type they are referring to, and this is accomplished by a hidden 'this' reference. This hidden 'this' reference is how the other members like operators and methods know which instance of the Type the call refers to. Most operators can be overloaded; here's a list of the ones that currently can be:
Assignment ops:
let, +=, -=, *=, /=, \=, mod=, shl=, shr=, and=, or=, xor=, imp=, eqv=, ^=
Unary ops:
-, not, @, *, ->
Binary ops:
+, -, *, /, \, mod, shl, shr, and, or, xor, imp, eqv, ^, =, <>, <, >, <=, >=
Overloaded Constructors/Methods
As with normal functions, our Type's constructor and methods can be overloaded. For constructors, this provides a way to specify details on how the instance should be constructed. Here's a short example:
The first Constructor, that had no arguments, is known as the default constructor. This sets up foo.x to an initial value of 10. However, we have also specified another constructor that will accept an initial value. Note the way we ask for this to be called Dim baz As bar = bar(25). You can also leave out the default constructor, and then you will always have to specify the initial value using the constructor that takes an argument. You can't have an overloaded destructor, because there's no way to manually choose which one would be called.
Overloaded methods are very similar:
They work just they same as normal overloaded functions.
Closing
I hope this tutorial has been useful for you, although there are still a few things left to learn; if you've got this far, it shouldn't be too hard for you to pick them up. There is some more information available in the wiki and on the forums, and also in part 2 of this tutorial, available here - Beginners Guide to Types as Objects (Part 2)
More reading
This tutorial is aimed at people who want to know more about the new features added to Type, commonly being referred to as 'types as objects', and 'that OOP stuff'. It aims to walk you through these new features, so is aimed at people who don't really understand it yet, but want to learn. A Type in FreeBASIC is an aggregate data type, like a struct in C, or a record in Pascal. Here's just a short sample of typical Type usage.
Type person_info
first_name As String
last_name As String
house_number As Integer
street_name As String
town As String
End Type
first_name As String
last_name As String
house_number As Integer
street_name As String
town As String
End Type
In this usage it's used as a kind of container for related data; in this example it could be as an entry in an address book. With the new features, however, it can be used more like the class in C++, in that it can do much more than contain just simple fields of data. It becomes a way to express an idea of an object, and this makes object oriented programming much simpler. We will now look at these new features.
Property
We'll start by looking at property. When you add a property to a Type, you access it as if it were an ordinary member, but what happens, is instead of just getting or setting a variable as normal, it calls a function instead. Take a look at this example:
Type bar
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
p_x As Integer
End Type
Property bar.x() As Integer
Print "bar.x()"
Property = p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
p_x = n
End Property
'---
Dim foo As bar
foo.x = 5
Print foo.x
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
p_x As Integer
End Type
Property bar.x() As Integer
Print "bar.x()"
Property = p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
p_x = n
End Property
'---
Dim foo As bar
foo.x = 5
Print foo.x
We include in our Type some declarations for a Property; they are very similar to ordinary function declarations. The first one declares a getter, the second a setter. The p_x member is just an ordinary Integer member.
Next we write the code for the properties; again, the syntax is very similar to that of normal functions. Note the way we return a value: instead of Function = value, we do Property = value. You can do Return value as well. Also note that you can refer to the member directly as p_x; you can also use the keyword this, for example this.p_x = n; using this isn't usually needed, but it can help in some ambiguous circumstances.
Then follows some testing code; this shows how we can use the property as if it were any ordinary member. When you run the program it will also print to screen to show that the property get/set code has been called.
Now this code is fairly trivial, but as you get used to the idea you'll see it can be put to some good uses. Imagine as an example you are writing a GUI, and the TYPE represents a button on the screen, you could do button.text = "Hello World!", and make the property code update the screen to show the changes. Or maybe you are using the Type to maintain some kind of list; you could do list.size += 10 and then put some code in your property to make the list larger.
Constructor/Destructor
Constructors are functions that are called when the Type gets created - when you use Dim, for example. A Destructor is a function that gets called when the Type goes out of scope; this could be when the program ends, for a Type in the main code, or when a function ends, for a local Type. Look at the following example, expanded from the last.
Type bar
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
p_x As Integer Ptr
End Type
Constructor bar()
Print "Constructor bar()"
p_x = Allocate(SizeOf(Integer))
*p_x = 10
End Constructor
Destructor bar()
Print "Destructor bar()"
Deallocate(p_x)
End Destructor
Property bar.x() As Integer
Print "bar.x()"
Property = *p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
*p_x = n
End Property
'---
Dim foo As bar
Print foo.x
foo.x = 5
Print foo.x
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
p_x As Integer Ptr
End Type
Constructor bar()
Print "Constructor bar()"
p_x = Allocate(SizeOf(Integer))
*p_x = 10
End Constructor
Destructor bar()
Print "Destructor bar()"
Deallocate(p_x)
End Destructor
Property bar.x() As Integer
Print "bar.x()"
Property = *p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
*p_x = n
End Property
'---
Dim foo As bar
Print foo.x
foo.x = 5
Print foo.x
Again the syntax is somewhat similar to normal functions. Note that this time I changed p_x to be an Integer ptr. The constructor then Allocates the memory for this when foo is created, and gives it a default value; then it De-Allocates this memory once it is destroyed. So you can use Constructors and Destructors to set things up for you, then clean up once its finished with. Again a trivial example, but bring back the example of some kind of list, and having it set the list up for you, and clean it up when it's finished with can be quite handy.
Methods
You can also have regular Subs and Functions inside your Type; in some terminology, these are referred to as methods. We'll carry on our example:
Type bar
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
Declare Sub Mul5()
Declare Function Addr() As Integer Ptr
p_x As Integer Ptr
End Type
Constructor bar()
Print "Constructor bar()"
p_x = Allocate(SizeOf(Integer))
*p_x = 10
End Constructor
Destructor bar()
Print "Destructor bar()"
Deallocate(p_x)
End Destructor
Property bar.x() As Integer
Print "bar.x()"
Property = *p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
*p_x = n
End Property
Sub bar.mul5()
*p_x *= 5
End Sub
Function bar.Addr() As Integer Ptr
Function = p_x
End Function
'---
Dim foo As bar
Print foo.x
foo.x = 5
Print foo.x
foo.mul5()
Print foo.x
Print "address p_x points to", foo.Addr()
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
Declare Sub Mul5()
Declare Function Addr() As Integer Ptr
p_x As Integer Ptr
End Type
Constructor bar()
Print "Constructor bar()"
p_x = Allocate(SizeOf(Integer))
*p_x = 10
End Constructor
Destructor bar()
Print "Destructor bar()"
Deallocate(p_x)
End Destructor
Property bar.x() As Integer
Print "bar.x()"
Property = *p_x
End Property
Property bar.x(ByVal n As Integer)
Print "bar.x(ByVal n As Integer)"
*p_x = n
End Property
Sub bar.mul5()
*p_x *= 5
End Sub
Function bar.Addr() As Integer Ptr
Function = p_x
End Function
'---
Dim foo As bar
Print foo.x
foo.x = 5
Print foo.x
foo.mul5()
Print foo.x
Print "address p_x points to", foo.Addr()
So this time we added a Sub, that multiplies the integer pointed to by p_x by five, and a function that gets the memory address that the pointer holds.
Private/Public
By default all of the members of the bar type are public; that means that we can read/write or call them. However, sometimes you might want to make them private. Take for example our member p_x; we can currently do Print *foo.p_x, and it will allow us to print the value it points to. We might want to make it private, so that only the members of the bar type (the constructor, destructor, property, and methods) can access it. That way we can make sure we only deal with p_x by the ways we choose. If for example we did 'DeAllocate(foo.p_x)' in our main code, then when the destructor runs, it would try to free it again, known as a 'double free'. Change the Type declaration as follows:
Type bar
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
Declare Sub Mul5()
Declare Function Addr() As Integer Ptr
Private:
p_x As Integer Ptr
End Type
Declare Constructor()
Declare Destructor()
Declare Property x() As Integer
Declare Property x(ByVal n As Integer)
Declare Sub Mul5()
Declare Function Addr() As Integer Ptr
Private:
p_x As Integer Ptr
End Type
Now try adding Print *foo.p_x to the main code and compile it. You'll get a message from fbc "error 173: Illegal member access, found 'p_x' in 'Print *foo.p_x'", showing that indeed the compiler is now enforcing the fact we made p_x private. When you use private: or public:, any members following that statement follow the rule. Here's a rather pointless example just to show the syntax:
Type bar
Private:
a As Integer
b As Integer
Public:
c As Integer
d As Integer
Private:
e As Integer
End Type
Private:
a As Integer
b As Integer
Public:
c As Integer
d As Integer
Private:
e As Integer
End Type
In the above type, the members a, b, and e are private; c and d are public.
Operator overloading
Operator overloading is a way of telling the compiler what to do in the case where we want to perform some kind of operation involving our Type. Take this example:
Type bar
n As Integer
End Type
Dim As bar x, y, z
z = x + y
n As Integer
End Type
Dim As bar x, y, z
z = x + y
Now normally the compiler will throw an error when it sees this, as it has no idea how to add together two Types, but we can define what we want to happen. Here's how:
Type bar
n As Integer
End Type
Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
Operator = Type(lhs.n + rhs.n)
End Operator
Dim As bar x, y, z
x.n = 5
y.n = 10
z = x + y
Print z.n
n As Integer
End Type
Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
Operator = Type(lhs.n + rhs.n)
End Operator
Dim As bar x, y, z
x.n = 5
y.n = 10
z = x + y
Print z.n
In this code, I use lhs and rhs to refer to the left and right hand side operands of the operator. Note also the expression type(lhs.n + rhs.n); this builds the Type that will be returned. If you had a type like:
Type bar
x As Integer
y As Integer
z As Integer
End Type
x As Integer
y As Integer
z As Integer
End Type
Then you would build it like type(xpart, ypart, zpart).
Most or all operators can be overloaded, and most of them are binary ops, meaning they have two operands like the + example above. Some are unary ops having only a right hand side, like Not and unary minus. They would be done like 'Operator Not(ByRef rhs As bar) As bar'.
There are some special cases where they have to be declared inside the Type; these are the assignment operators and casts.
Assignment operators are things like += -= mod= etc, and also Let. Let is used when you do an assignment like:
Dim As bar foo
Dim As Integer x
foo = x
Dim As Integer x
foo = x
And casts are kind of the reverse; they are used when you cast to another datatype like:
Dim As bar foo
Dim As Integer x
x = foo
Dim As Integer x
x = foo
Heres a short example using Let and Cast:
Type bar
n As Integer
Declare Operator Let(ByRef rhs As Integer)
Declare Operator Let(ByRef rhs As String)
Declare Operator Cast() As String
End Type
Operator bar.Let(ByRef rhs As Integer)
n = rhs
End Operator
Operator bar.Let(ByRef rhs As String)
n = Val(rhs)
End Operator
Operator bar.Cast() As String
Operator = Str(n)
End Operator
Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
Operator = Type(lhs.n + rhs.n)
End Operator
Dim As bar x, y, z
x = 5
y = "10"
z = x + y
Print z
n As Integer
Declare Operator Let(ByRef rhs As Integer)
Declare Operator Let(ByRef rhs As String)
Declare Operator Cast() As String
End Type
Operator bar.Let(ByRef rhs As Integer)
n = rhs
End Operator
Operator bar.Let(ByRef rhs As String)
n = Val(rhs)
End Operator
Operator bar.Cast() As String
Operator = Str(n)
End Operator
Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
Operator = Type(lhs.n + rhs.n)
End Operator
Dim As bar x, y, z
x = 5
y = "10"
z = x + y
Print z
You need to have separate lets and casts for each data type you want to support. The operators that need declaring within the type are known as non-static, and the ones that don't are known as global. There is a technical reason for this; the non-static ones need to know which instance (in the technical jargon, in our example above, we would say that x is an instance of bar) of the Type they are referring to, and this is accomplished by a hidden 'this' reference. This hidden 'this' reference is how the other members like operators and methods know which instance of the Type the call refers to. Most operators can be overloaded; here's a list of the ones that currently can be:
Assignment ops:
let, +=, -=, *=, /=, \=, mod=, shl=, shr=, and=, or=, xor=, imp=, eqv=, ^=
Unary ops:
-, not, @, *, ->
Binary ops:
+, -, *, /, \, mod, shl, shr, and, or, xor, imp, eqv, ^, =, <>, <, >, <=, >=
Overloaded Constructors/Methods
As with normal functions, our Type's constructor and methods can be overloaded. For constructors, this provides a way to specify details on how the instance should be constructed. Here's a short example:
Type bar
Declare Constructor()
Declare Constructor(ByVal initial_val As Integer)
x As Integer
End Type
Constructor bar()
x = 10
End Constructor
Constructor bar(ByVal initial_val As Integer)
x = initial_val
End Constructor
Dim foo As bar
Print foo.x
Dim baz As bar = bar(25)
Print baz.x
Declare Constructor()
Declare Constructor(ByVal initial_val As Integer)
x As Integer
End Type
Constructor bar()
x = 10
End Constructor
Constructor bar(ByVal initial_val As Integer)
x = initial_val
End Constructor
Dim foo As bar
Print foo.x
Dim baz As bar = bar(25)
Print baz.x
The first Constructor, that had no arguments, is known as the default constructor. This sets up foo.x to an initial value of 10. However, we have also specified another constructor that will accept an initial value. Note the way we ask for this to be called Dim baz As bar = bar(25). You can also leave out the default constructor, and then you will always have to specify the initial value using the constructor that takes an argument. You can't have an overloaded destructor, because there's no way to manually choose which one would be called.
Overloaded methods are very similar:
Type bar
Declare Sub foo()
Declare Sub foo(ByVal some_value As Integer)
Declare Sub foo(ByRef some_value As String, ByVal some_other As Integer)
x As Integer
End Type
Declare Sub foo()
Declare Sub foo(ByVal some_value As Integer)
Declare Sub foo(ByRef some_value As String, ByVal some_other As Integer)
x As Integer
End Type
They work just they same as normal overloaded functions.
Closing
I hope this tutorial has been useful for you, although there are still a few things left to learn; if you've got this far, it shouldn't be too hard for you to pick them up. There is some more information available in the wiki and on the forums, and also in part 2 of this tutorial, available here - Beginners Guide to Types as Objects (Part 2)
More reading