Qore supports three types of container types (see also Basic Data Types and Code Data Types):
These container types can be combined to make arbitrarily complex data structures. The data type of any element can be any basic type or another aggregate type. The types do not have to be uniform in one container structure.
List
- Description:
- Lists (or arrays) are simply ordered containers of values. A list element can be any Qore type (even another list, hash, or object).
Lists are specified by giving expressions separated by commas as follows:
list = (expression, expression [, expression ...]);
Here is a concrete example:
list list = (
1,
2,
"three",
4.0,
5e20n,
6,
2001-01-15Z,
);
Trailing commas can be left on the end of a list (or a hash), making it easier to insert and remove elements at the end of a multi-line list.
List elements are dereferenced using square brackets: "["
and "]"
. The first element in a list has index zero.
Example:
The following operators perform special processing on lists:
- Immediate Value Example:
(1, "two", 3.0)
Gives an empty list (note that {}
gives an empty hash): ()
- Pseudo Class for Type List:
- <list>
- Type Code:
- Qore::NT_LIST
- Type Name:
"list"
The list type supports a complex element type specification as well, however "list" lvalues without a complex element type specification will strip the complex type when assigned as in the following example:
list l0 = (2, 3);
printf(
"%y\n", l0.fullType());
list<auto> l1 = (2, 3);
printf(
"%y\n", l1.fullType());
- List slicing:
- Lists can be "sliced" by dereferencing them using the [] operator with a range or a list, as in the following examples:
list<int> l = (1, 2, 3, 4);
list l1a = l[0..1];
list l1b = l[0,1];
list l1c = l[..1];
printf(
"slice 1a: %y\n", l1a);
printf(
"slice 1b: %y\n", l1b);
printf(
"slice 1c: %y\n", l1c);
list l2a = l[3..2];
list l2b = l[3,2];
printf(
"slice 2a: %y\n", l2a);
printf(
"slice 2b: %y\n", l2b);
list l3 = l[3,2..1,0];
slice 1a: [1, 2]
slice 1b: [1, 2]
slice 1c: [1, 2]
slice 2a: [4, 3]
slice 2b: [4, 3]
slice 3: [4, 3, 2, 1];
When making a slice of a list with a list of elements and referring to an element index that does not exist, NOTHING is returned for that list element in the resulting list as in the following example: list<int> l = (1, 2, 3, 4);
[3, null, null]
- See also
-
- Complex Type Support for Lists
- The
"list"
type also supports an optional type argument which allows the value type of list elements to be declared; the following example demonstrates a list declaration with a specific value type:
This type is supported at parse-time and at runtime; to convert such values to an untyped list, assign it to a list lvalue, use cast<list>(...) on the value, or call the list() function on the value. Each of these options can be used to convert a type-safe list to an untyped list.
A special type argument, "auto"
, allows for the lvalue to maintain the complex list type as in the following example:
list l0 = (2, 3);
printf(
"%y\n", l0.fullType());
list<auto> l1 = (2, 3);
printf(
"%y\n", l1.fullType());
- See also
- List With Declared Value Type
- Note
- Trailing commas can be left on the end of a list (or a hash), making it easier to insert and remove elements at the end of a multi-line list.
- List elements are dereferenced using square brackets:
"["
and "]"
. The first element in a list has index zero.
- Dereferencing an invalid element (past the end of the list or with a negative number) will return NOTHING
- Use <list>::iterator() as an easy way to get a list iterator object for the current list
Hash
- Description:
- Hashes are containers that associate values to a string key and also preserve key order for consistent data serialization/deserialization. If anything else than a string is used as a key, the value is one-way converted to a string and the string value is used as a key. This can sometimes have unexpected results and therefore only string keys are recommended. Hash key lookups are case-sensitive and use a hashing algorithm that in the worst case should provide O(ln(n)) complexity to execute; in the best case finding a particular key in a hash is executed in constant time (ie O(1) complexity).
Hashes are specified using the following syntax:
hash h = {
key_expression: value_expression,
key_expression: value_expression,
...,
};
with a typed hash, immediate values can look like: auto th0 = <MyHash>{
key_expression: value_expression,
key_expression: value_expression,
...,
};
auto th1 = hash<MyHash>{
key_expression: value_expression,
key_expression: value_expression,
...,
};
auto th2 = cast<hash<MyHash>>({
key_expression: value_expression,
key_expression: value_expression,
...,
});
auto th3 = new <hash<MyHash>>({
key_expression: value_expression,
key_expression: value_expression,
...,
});
or alternatively, a simple non-typed immediate hash can be declared with parentheses: hash h2 = (
key_expression: value_expression,
key_expression: value_expression,
...,
);
Here is a concrete example:
hash hash = {
"apple": 1 + 1,
"pear": "string",
};
Trailing commas are ignored (as with lists) to allow for easier insertion and deletion of elements in source code.
Hashes are dereferenced in one of two ways, either using curly brackets: "{"
and "}"
, where any valid Qore expression can be used, or using the dot "." hash member dereferencing operator, where literal strings can be used.
element3 = hash{"pe" + "ar"};
Is equivalent to:
and:
and:
element3 = hash.("pe" + "ar");
Hash members can have the names of keywords or names that are not valid identifiers, but in this case to dereference them you cannot use the dot operator with a literal string, otherwise a parse error will be raised. Use quotes around the member name when dereferencing hash members with the same name as a qore keyword or other name that is not a valid identifier as follows:
element3 = hash."keys";
element3 = hash{"this-element-1!"};
A literal string after the dot "." hash member dereferencing operator must be a valid Qore identifier; therefore if you want to use a hash key that's not a valid identifier, enclose the string in quotes.
If you want to use the result of an expression to dereference the hash, then either the curly bracket syntax must be used or the expression must be enclosed in parentheses.
In the case of using a literal string with the dot operator, keep in mind that the string is always interpreted as a literal string name of the member, even if there is a constant with the same name. To use the value of a constant to dereference a hash, use curly brackets with the constant: ex: Note that hash keys can also be given by constants (as long as the constant resolves to a string) when using curly brackets.
- Note
- using curly-bracket delimiters with an immediate hash with
map
results in the hash version of the map operator)
- Hash slicing:
- Hashes can be "sliced" by dereferencing them using parenthesis syntax with more than one key, as in the following example:
hash h = {
"a": 3,
"b": 5,
"x": "test",
"y": 0,
"text": "lorem ipsum dolor sit amet",
"float": 2.71828
};
hash nh = h.("a", "b");
printf(
"slice 2: %y\n", h.(
"x",
"y",
"float"));
printf(
"slice 3: %y\n", h.(
"x",
"random"));
printf(
"slice 4: %y\n", h.(
"abc",
"xyz"));
slice 1: {a: 3, b: 5}
slice 2: {x: "test", y: 0, float: 2.71828}
slice 3: {x: "test"}
slice 4: {}
All keys matching with the original hash's key-value pairs will be included in the newly created hash. Keys which are not in the original hash will be silently ignored.
- Immediate Value Examples:
{"key1": 1, "key2": "two", get_key_3(): 3.141592653589793238462643383279502884195n}
<Container>{"i": 2}
hash<Container>{}
Hashes can be declared with curly brackets (preferred) or parentheses: ("key1": 1, "key2": "two", get_key_3(): 3.141592653589793238462643383279502884195n)
Gives an empty hash (note that ()
gives an empty list): hash h = {};
- Typed Hash Declarations
- The
"hash"
type supports a variant with pre-defined type-safe keys which can be specified with a single argument giving a type-safe hashdecl identifier in angle brackets after the "hash"
type. Furthermore, type-safe hashes can be declared using the hashdecl keyword; the following example demonstrates a type-safe hash declaration and then a variable restricted to this type:
hashdecl Container {
int i = 1;
string str = "default";
}
auto ah1 = hash<Container>{};
auto ah2 = <Container>{};
auto ah3 = <Container>{"i": 2};
hash<Container> c1();
hash<Container> h2(("i": 10, "str": "other"));
This type is supported at parse-time and at runtime; to convert such values to an untyped hash, assign it to a hash lvalue, use cast<hash>(...) on the value, or call the hash() function on the value. Each of these options can be used to convert a type-safe hash to an untyped hash.
A special single argument, "auto"
, allows for the lvalue to maintain the complex hash type as in the following example:
hash h0 = ("a": 2, "b": 3);
printf(
"%y\n", h0.fullType());
hash<auto> h1 = ("a": 2, "b": 3);
printf(
"%y\n", h1.fullType());
- Note
- a
hashdecl
may not have the name "auto"
, this name has a special meaning in complex types
- See also
-
- Complex Type Support for Hashes
- The
"hash"
type also supports two type arguments which allow the value type of the hash to be declared. The key type is also included in the declaration, but is currently restricted to type string; the following example demonstrates a hash declaration with a specific value type:
hash<string, int> h(("key1": 2, "key2": 3));
This type is supported at parse-time and at runtime; to convert such values to an untyped hash, assign it to a hash lvalue, use cast<hash>(...) on the value, or call the hash() function on the value. Each of these options can be used to convert a type-safe hash to an untyped hash.
- See also
- Hash With Declared Value Type
- Pseudo Class for Type Hash:
- <hash>
- Type Code:
- Qore::NT_HASH
- Type Name:
"hash"
- See also
- hash
- Note
- Trailing commas are ignored in immediate hash declarations (as with lists) to allow for easier insertion and deletion of elements in source code.
- Hashes are dereferenced in one of two ways, either using curly brackets:
"{"
and "}"
, where any valid Qore expression can be used, or using the dot "." hash member dereferencing operator, where literal strings can be used. In the case of using a literal string with the dot operator, keep in mind that the string is always interpreted as a literal string name of the member, even if there is a constant with the same name. To use the value of a constant to dereference a hash, use curly brackets with the constant: ex:
- Use quotes around the member name when dereferencing hash members with the same name as a Qore keyword or other name that is not a valid identifier: ex:
- Qore hashes preserve key insertion order to support consistent serialization and deserialization to strings (such as XML, JSON, or YAML strings) or data encoding formats, therefore the keys operator and the <hash>::keys() pseudo-method will always return the hash keys in insertion/creation order
- Dereferencing hash values that are not present in the hash will return NOTHING; to catch typos in hash member names when dereferencing a hash, you can use an object and declare a list of allowed public members in the class definition (in which case dereferencing a non-existant member will cause a parse exception to be thrown, if the object's class is known at parse time, otherwise it will cause a runtime exception), also the Qore::ListHashIterator class allows for hash members to be dereferenced transparently and will throw an exception if a non-existant member name is given (to catch typos).
- There are several pseudo-methods implemented in both the <hash> and the <nothing> pseudo-classes that provide asy access to special hash iterators:
- the hash version of the map operator is used when a literal single member hash expression with curly brackets is given to the map operator; this version of the map operator can be used to build a hash dynamically from a list or iterator expression
Object
- Description:
- Qore objects are instantiations of a class. They have members (like a hash; values associated to string keys), and methods. The class definition specifies the methods that run on objects of that class, public and private members, static methods and variables, etc associated with the class (however note that static methods do not run in the scope of an object). See Classes for information on how to create a class in Qore.
- Immediate Value Example:
- Pseudo Class for Type Object:
- <object>
- Type Code:
- Qore::NT_OBJECT
- Type Name:
"object"
- See also
-
- Note
- each Qore type has a "pseudo-class" associated with it (the default is <value>); methods from the data type's "pseudo-class" can be run on any value of that type; see "Pseudo Class for Type" headings in Basic Data Types for more information. Pseudo-methods can be overridden in classes; if a class implements a method with the same name and signature as an object pseudo-method, then the class' method will be executed instead of the pseudo-method.
Object Overview
The recommended way to instantiate an object is to declare its type and give constructor arguments after the variable name in parentheses as follows:
class_name_or_path var_name([argument list])
For example (for a constructor taking no arguments or having only default values for the aguments, the list is empty):
Mutex m();
object<Mutex> m1();
Objects can also be instantiated using the new operator as follows.
new class_identifier([argument list])
For example:
Objects have named data members that are referenced like hash elements, although this behavior can be modified for objects using the memberGate() method. Object members are accessed by appending a dot '.' and the member name to the object reference as follows:
object_reference.member_name
For more information, see Class Members.
Object methods are called by appending a dot '.' and a method name to the object reference as follows:
object_reference.method_name([argument_list])
Or, from within the class code itself to call another method from inside the same class hierarchy:
method_name([argument_list])
For more information, see Object Method Calls.
The object references above are normally variable references holding an object, but could be any expression that returns an object, such as a new expression or even a function call.
- Note
- Objects are treated differently than other Qore data types; they are only explicitly copied (see Object References for more information). Any object instantiated with the new operator will remain unique until deleted or explicitly copied. An explicit copy is made with the copy method, and does not always guarantee an exact copy of the source object (it depends on the definition of the copy method for the class in question).
Objects exist until they go out of scope, are explicitly deleted, or their last thread exits. For detailed information, see Classes.
- See also
- object
Object References
In Qore objects are treated differently from all other data types in that they are by default passed as arguments to functions and methods by passing a copy of a reference to the object (similar to Java's handling of objects). That means that passing an object to a function that modifies the object will by default modify the original object and not a copy, however reassigning a local parameter variable assigned an object passed as an argument (that is only assigned to a local variable in the calling function) will not result in deleting the object, but rather decrement its scope reference count (note that if the object were created as a part of the call and reassigning the variable would cause the object's scope reference count to reach zero, then the object would be deleted in this case).
Assigning an object to a variable has the same effect; a copy of a reference to the object is assigned to the variable. This results in prolonging the object's scope (by owning a new copy of a reference to the object).
An example:
sub test2(any x) {
x.member = "tree";
x = 1;
}
sub test() {
TestObject o();
test2(o);
if (o.member == "tree")
}
If, however, an object is passed by reference, then the local variable of the called function that accepts the object owns the scope reference of the calling functions's variable.
An example:
sub test2(any x) {
x.member = "tree";
x = 1;
}
sub test() {
TestObject o();
test2(\o);
if (o.member == "tree")
}
- Note
- that when parse option %allow-bare-refs is set, then variable references as in the above examples are made without the
"$"
character.
Object Scope and Garbage Collection
Garbage Collection and Resource Management
All complex data structures in Qore use atomic references to support copy-on-write semantics, but objects are unique since they are always passed with a copy of a reference to the object (similar to Java's object handling), therefore it's possible to make recursive references to objects, a fact which complicates object scope management and garbage collection.
The RAII idiom for resource management is a fundamental feature of Qore's design; the language should always guarantee that objects are destroyed and therefore their associated resources are managed and memory freed when objects go out of scope. This is tricky when objects have recursive references; other languages use a variety of approaches to deal with this (for example, a mark-and-sweep algorithm in a dedicated garbage collection thread) which can have various drawbacks (such as non-deterministic performance impacts, such as when the garbage-collection thread grabs global locks to verify references, etc).
Qore supports deterministic garbage collection of objects even when objects participate in recursive directed graphs. In this case the deterministic aspect of Qore's garbage collection algorithm means that objects are collected immediately when they have no more valid references to them. In case of recursive references, it means that no valid references are pointing to any object in the recursive graph; in other words, the scope references for every object participating in the recursive graph are only due to references related to recursive references. In this case, the affected objects are collected immediately by having their destructors run.
The consequence of this is that destructors are run immediately when the objects are no longer valid, and therefore the RAII idiom for resource management is supported in Qore even when objects participate in recursive directed graphs.
Some examples of RAII in builtin Qore classes are (a subset of possible examples):
- the Autolock class releases the Mutex in the destructor (this class is designed to be used with scope-bound exception-safe resource management; see also the AutoGate, AutoReadLock, and AutoWriteLock classes)
- the Datasource class closes any open connection in the destructor, and, if a transaction is still in progress, the transaction is rolled back automatically and an exception is thrown before the connection is closed
- the DatasourcePool class closes all open connections in the destructor, and, if a transaction is still in progress, the transaction is rolled back automatically and an exception is thrown before the connection is closed
- the ReadOnlyFile and File classes close the file descriptor in the destructor if it's open
- the RWLock class throws an exception if destroyed while any thread is still holding the lock; note that in this case the underlying object is only destroyed when all threads holding locks have released their locks; this is handled with Qore's thread resource handling and strong references to the underlying RWLock object while thread resources are held
- the Socket class first shuts down any TLS/SSL connection and then closes the connection in the destructor if it's open
- the ThreadPool class detaches all currently in-progress worker threads, cancels pending tasks not yet executed (by calling their cancellation closure, if any), terminates the worker thread and destroys the thread pool
- Note
- Qore also supports scope-related resource management support in the form of the on_exit, on_success, and on_error statements
Object Scope
As mentioned in the previous section, objects are automatically collected and have their destructors run when their scope-relevant reference count reaches zero or when the scope-relevant reference count is equal to the number of recursive references for every object in a recursive directed graph (note that objects can be deleted manually at any time by using the delete operator). Whenever an object is collected, the object's destructor method as defined in the object's class is run on the object (multiple destructor methods can be run on objects instantiated from a class hierarchy).
The following affect objects' scope:
- external references to the object: an object's automatic scope is prolonged as long as the object has valid external references
- object method thread launched within the object: if a member function thread was launched from within the object using the background operator, the object's automatic scope is prolonged to the life of the new thread. Object threads started externally to the object (i.e. not directly from an expression with the background operator within a method) will not prolong the scope of the object.
- Note
- If an object with running threads is explicitly deleted, and this case is not handled in the object's destructor() method (by ensuring that all other running threads terminate gracefully), exceptions will be thrown in other threads at any attempt to access the already-deleted object. The fact that object threads and closures can prolong object scope means, for example, that objects assigned to local variables can exist for longer than the scope of their host variable if they have one or more methods running in other threads or if closures created from within the object still exist at the time the local variable goes out of scope. For more information about threading, please see Threading
- Since
- As of Qore 0.8.10 the existence of a closure created within the object no longer prolongs the scope of the object; the closure encapsulate the object's state (along with any local variables referenced within the closure), but if the object goes out of scope while the closure still exists, then any references to the object after the object has been destroyed will cause
OBJECT-ALREADY-DELETED
exceptions to be thrown. This addresses memory and reference leaks caused by recursive references when closures encapsulating an object's scope are assigned to or accessible from members of the object.
Copying Objects
To explicitly generate a copy of an object, the copy()
constructor must be called. This is a special method that exists implicitly for every class even if it is not explicitly defined (like constructor()
and destructor()
methods). The implicit behavior of the copy()
constructor is to create a new object with new members that are copies of the original members (except objects are created as copies of references to the object). Then, if any copy()
method exists, it will be executed in the new object, passing a copy of a reference to the old object as the first argument to the copy()
method.
- Note
- In a class hierarchy copy() methods are called in the same order as constructor() methods.
Not all built-in classes can be copied. Classes not supporting copying will throw an exception when the copy()
methods are called. See the documentation for each class for more information.