Chapter 9
Arrays, Lists and Dictionaries
This is an important chapter for (at least) two reasons. First, arrays and especially (generic) lists are important data structures. And second, there is hardly another topic where X# that has its roots in the 80s differs more from a "modern" language like C#. The good news is, as always, there is no need to choose between "old" and "new". VO style arrays, codeblocks, and LINQ with lambdas and generic lists can be combined in a single application if the developer wishes to do so.
Arrays overview
Arrays are a well known entity in any programming language since the invention of Fortran in the mid-50s of the last century (I am looking forward to the 100th birthday party for Fortran, Cobol, and LISP although I am not sure that I might be able to attend;) so I won't go into any details.
Arrays have been an important part of VO from the beginning, the VO library contains many powerful functions like AAdd, ADel, AScan, and so on.
This section is about working with Arrays the ".Net way" because this is what C# developers who want to learn X# are mostly interested in. Using Arrays the .Net way does not bring any significant advantages compared to the "VO way" except for more consistency which is always good. I recommend using Lists anyway because they are more flexible but if someone is used to Arrays, especially when they are multi-dimensioned, that's no problem of course (at least when the structure of the multidimensional array is documented somewhere;).
There is no data type Array in .Net. Well, there is an Array class but it's an abstract base class with a set of approximately two dozen methods. So it's possible to declare a variable of Array and use the static methods, but it's not possible to instantiate an Array.
To quote the Microsoft documentation:
The Array class provides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime.
Just in case: System.Array documentation
Although it's not possible to create new instances of the Array class, its methods like BinarySearch, Copy, Find, Resize, Reverse, or Sort can be used with an existing array as an argument.
Declaring an array the VO way
In the good, old days of Windows application development using arrays was made as easy and comfortable as possible by languages like VO because all data outside a database were kept in arrays. There were no "modern" alternatives like lists or dictionaries available.
The VO syntax for defining an initialized array is still possible when compiling with the VO dialect:
var zahlen[10]
zahlen[1] := 1000
? zahlen[1]
In the year 2023 (still hard to believe sometimes) this syntax is of historical interest only. A "modern" version for the VO dialect is to initialize an array like this:
Local zahlen := {11,22,33,44,55,66}
? zahlen[1]
or with the formal Array declaration and without initialization values and adding them later:
Local zahlen := Array{}
AAdd(zahlen, 11)
AAdd(zahlen, 22)
AAdd(zahlen, 33)
? zahlen[1]
Since no data types have been used so far, all values are usuals including the array itself.
Declaring an array the .Net way
Whereas VO arrays are declared with Array as the data type, .Net arrays are declared by appending a pair of brackets to the data type.
The following command declares a Int32 array:
var numbers As Int32[]
The array is not yet initialized so it's not possible to store anything into that array. More on this later. The following example creates and initializes a .Net array with three elements and prints out each element:
Local numbers := <Int32>{11,22,33} As Int32[]
? numbers[1]
? numbers[2]
? numbers[3]
11
22
33
Special topics about using arrays
This section contains some useful information for using arrays. Always with consideration that (generic) Lists and Dictionaries offer much more flexibility.
0 or 1 based?
To make a long story short: Both VO and .Net Arrays are 1-based in X# until the compiler switch /az is used which switches to 0-based arrays.
A short example will hopefully shed some light on the consequences.
**Example 9.1: (XS_ArrayLength.prg) **
// Getting the length of an array
// compile with xsc .\XS_ArrayLength.prg /dialect:vo /r:XSharp.rt.dll /r:XSharp.Core.dll
Function Start() as void
Local Numbers1 := {1,3,5,7,11} As Array
For Local i := 1 UpTo ALen(Numbers1)
? i"Index {i} => {Numbers1[i]}"
Next
Local Numbers2 := <Int>{1,3,5,7,11} As Int32[]
For Local i := 1 UpTo Numbers2:Length
? i"Index {i} => {Numbers2[i]}"
Next
When compiled without the /az switch X# assumes a 1-based array type both for the VO and the .Net array and all numbers are printed out.
When compiling with the /az switch two things have changed:
- The first number is skipped because the index is 0
- An ArgumentException is thrown because there is no element with the index = Length of Array
Which to choose?
I think it depends if a developer starts a new project or is porting a VO project. Since "the whole world" is used to 0-based arrays I recommend using them in X# too which means setting the /az switch in the project settings. The best solution is to avoid arrays if possible. Not because they are old or "bad" but because generic Lists and the ArrayList class are more flexible.
Another argument is "pro List": Lists and Collections are always 0-based. This is one reason that I recommend using the ForEach command because it "knows" that an array is 1-based and the others are 0-based.
Declaring an empty array
An array variable uses an empty pair of [] with the data type like
Local names As String[]
Local values As Decimal[]
Local flags as Logic[]
Remember: [] always follows the type not the name of the variable.
These arrays are uninitialized so there is no way of putting a value in. They can only be used by assigning an array to them.
Getting the length of an array
Since ALen() won't work with a .Net array, it has to be either the Length property or the GetUpperBound() method. Later method expects a dimension value that starts with 0.
Local Numbers := Int[]{2} As Int[]
Numbers[1] := 100
Numbers[2] := 200
? Numbers:Length
2
Test if an array is empty
There is no IsEmpty() method for an array and we would have to define first what empty means. An array with 0 values, empty strings null values, or all of them? For VO arrays, the AScan() function is usually a good solution. The function expects an array as the first and a code block (which is another term for a lambda expression) as the second parameter. Please look in the X# help file for all the details.
The following example tests if an array with integers contains a value that is not 0.
Local a1 := {0,0,0,0,0} As Array
AScan(a1, {|z| z > 0}) > 0
Example 9.2: (XS_EmptyArrayTest1.prg) ** You will find the source code in the repository
For a .Net array, there is always LINQ as a flexible alternative.
The following example tests if a .Net array with integers contains a value that is not 0.
Local a1 := <Int32>{0,0,0,0,0} As Int32[]
a:Where({z => z <> 0}):ToList():Count == 0
Example 9.3: (XS_EmptyArrayTest2.prg) ** You will find the source code in the repository
NOTE: If while trying out the different options for dealing with VO and .Net arrays an error XS9008: Untyped arrays are not available in the selected dialect 'Core' occurs it probably has something to do with a wrong way of declaring or initializing the array.
Dynamic arrays
VO had an excellent "support" for arrays from the beginning. Much better than Visual Basic for example. The syntax for defining a dynamic array is still available in the VO dialect.
var aItem := { { 1,"OOP", "Soup"},;
{ 2, "AOT", "Cherrypie"};
}
AAdd(aItem, { 3, "SOLID", "Enchiladas"})
?i"I like {aItem[3][3]}"
Example 9.4: (XS_DynamicArraysVO.prg) ** You will find the source code in the repository
In the core dialect initializing an array with values is not that simple. For a good reason, because there are much better options for a nested data structure.
The following example declares and initializes a two-dimensional array using the core dialect:
var aItem := <Int32[]>{ {1,2,3},;
{ 11,22,33};
}
One more reason not to do it is way is the fact, that it's not possible to use Object as the data type so all the values have to be of the same type.
Example 9.5: (XS_DynamicArraysCore.prg) ** You will find the source code in the repository
It can be done with the generic list class List but this is also not a programming style I would recommend because it makes code hard to read and maintain. And why use Object if there is a better way like using a typed Struct?
**Example 9.6: (XS_DynamicArrayWithList.prg) **
// A small example for using a "dynamic Array" the .Net way - but I would consider "bad practice"
using System.Collections.Generic
Function Start() As Void
var aItem := List<object>{}{List<Object>{}{1,"Spagetti",3},;
List<Object>{}{11,"Rigatoni",33};
}
aItem:Add(List<Object>{}{111,"Pizza",333})
// not possible because the list element type is object
// error: Cannot apply indexing with [] to an expression of type `object'
// ?aItem[0][1]
// Works, but too complicated
?i"I really like {((List<Object>)aItem[2])[1]}"
Initializing a dynamic array
The following command won't compile.
Local values := {1,2,3} As Decimal[]
The error message says it all: "error XS0029: Cannot implicitly convert type 'array' to 'decimal[]'". So something is wrong with the initialization value which is a VO Array.
It's necessary to add the type explicitly.
Local values := <Decimal>{1,2,3} As Decimal[]
note: As stated before, the index in X# for the first elements starts with 1 not with 0 like in C#.
The following loop will result in an IndexOutOfRangeException:
For Local i := 0 UpTo Values:Length - 1
? Values[i]
Next
It has to be this way.
For Local i := 1 UpTo Values:Length
?Values[i]
Next
As already mentioned, I prefer the ForEach command, although it might be a little slower, because it always "knows" how to enumerate an array or a list.
ForEach var v in Values
?v
Next
Initializing a fixed sized array
The syntax for initializing a fixed-size array is a little different.
Local values := Decimal[]{3} As Decimal[]
This has already been addressed but just in case: Is it possible to use VO functions like AAdd or ASort with a .Net array? Answer: No.
Getting the last elements of an array (or list)
Getting the last elements of an array or list could be a little easier than it is with X#. On the other hand, the ^ operator that was introduced with C# 9.0 lacks consistency (it used a 1-based index numbering). The X# team discussed implementing the operator but decided not to do so. Maybe, somebody comes up with a really good syntax for addressing list elements from behind.
In the meantime, there is some simple arithmetic needed for getting the elements of an array or list from the end.
The following functions return the last n elements of an array as another array.
```visual basic
Function GetLastNumbers(a as Int[], n As Int) As Int[]
var tmpList := List
**Example 9.7: (XS_ArrayLastElements1.prg) **
*You will find the source code in the repository*
It gets really easy with LINQ and the combination of a *reverse()* and a *take()* method of the *Enumerable* class and a `using System.Linq`:
```visual basic
Function GetLastNumbersEx(a As Int[], n As Int) As Int[]
Return Enumerable.Reverse(a):Take(n):Reverse():ToArray()
LINQ will explained in Chapter 12 in more detail.
Example 9.8: (XS_ArrayLastElements2.prg) ** You will find the source code in the repository
Fig 9.1: Getting the last elements of an array or list
Multidimensional arrays
The syntax for declaring a multidimensional array the .Net way is a little bit "special" but OK.
**Example 9.9: (XS_ArrayMultidimensional.prg) **
// Creating a two dimensional .Net array
Function Start() as void
Local Numbers := Int[,]{2,2} As Int[,]
Numbers [1,1] := 100
Numbers [1,2] := 111
Numbers [2,1] := 200
Numbers [2,2] := 222
For Local i := 1 UpTo Numbers:GetUpperBound(0) + 1
For Local j := 1 UpTo Numbers:GetUpperBound(1) + 1
? Numbers[i,j]
Next
Next
GetUpperBound() always gets the zero-based length of the array in the given dimension (I somewhere read that a .Net array can have up to 63 dimensions which should be enough for any string theory science).)
There is also a GetLowerBound() method but I am not sure what it's good for since the lower dimension is supposed to be always 0 and the method does not work with a VO array.
Well, in the .Net world, you can always suspect a surprise. In this case, it is a rarely (if at all) used method to create an Array through the factory method CreateInstance (thanks to a StackOverflow user who reminded me of this forgotten "jewel").
The following examples prove that in really rare circumstances the lower bound of a .Net array can be non-zero.
The example defines a 3x3 int 2D array but the upper bound of the second dimension starts with 1 and not 0 (I hope I got that right).
**Example 9.10: (XS_ArrayUpperbound.prg) **
// Getting the dimensions of a multi dimensional array
// The upper bound of an Array can be non-zero in rare circumstances
Function Start() As Void
// Array with int and two dimensions with 3 elements
// The first dimension starts with 0, the second with 1
var Numbers := Array.CreateInstance(typeof(Int), <Int>{3,3}, <Int>{0,1})
For Local dim := 0 UpTo Numbers:Rank - 1
// PM: Not sure why Numbers:GetLowerBound() does not work
? i"LowerBound of Dimension {dim}: {Numbers.GetLowerBound(dim)}"
? i"UpperBound of Dimension {dim}: {Numbers.GetUpperBound(dim)}"
Next
And while we are at it: The rank property gives the number of dimensions of a .Net Array.
Here is an example you have been probably waiting for: An Array with 10 dimensions (I was too lazy to type more and push the compiler to its limit).
Local StrangeArray := Int[,,,,,,,,,]{1,2,3,4,5,6,7,8,9,10} As Int[,,,,,,,,,]
? i"Dimensions={StrangeArray:Rank}"
Dimensions:10
Typed arrays
X# offers a nice language feature that does not exist in both C# and Visual Basic: Typed Arrays. As the name implies, it's possible to define an array where all elements have to have a specific type, like Int32 or Person. Although one could argue that is what generic lists are for, since many developers are used to arrays, especially those with a VO background, a typed array might appear more familiar and is one step towards more type safety at runtime (which is always good).
The declaration for a typed Array of T has the typical developer-friendly syntax that VO developers have known for many years:
Local inventors := {} As Array Of Inventor
Adding elements is done with AAdd() for example:
var inv1 := Inventor{}{Name:="John Backus",Birthday:=DateTime.Parse("3.12.1924"),Inventions:=<String>{"Fortran","an obscure Notation"}}
AAdd(inventors, inv1)
The example is nearly identical to the example in the X# help file (except for the little fact that it does not produce an error;)
Example 9.11: (XS_TypedArray.prg) ** You will find the source code in the repository
Searching arrays
The legendary Donald E. Knuth once (in 1973) wrote a whole book with about 800 pages about searching and sorting algorithms. Luckily, we don't have to worry about the choice and implementation of a search algorithm anymore. This is all handled by the .Net runtime and the VO libraries.
How to search an array of course depends on whether it is a VO or a .Net array. For the first category, there is the universal AScan() function, for the latter there is LINQ (which can be used for VO arrays too).
Searching a VO-Array with AScan
VO has an excellent and rich handling of arrays - especially if an array contains several dimensions. The only requirement is to change the dialect to VO when compiling the code.
VO developers had a powerful search function from the beginning that Visual Basic developers could only have dreamed of, I am talking about the AScan() function of course. It scans an array for a value and returns either its position within the array or 0 (and not -1;).
The following examples are really simple and assume that you have not used this function before but want to use it for VO arrays.
The first example searches for a string value and returns the position.
**Example 9.12: (XS_AscanExample1.prg) **
// Searching a String array with AScan
// compile with xsc .\XS_AScanExample1.prg /dialect:VO /r:XSharp.Core.dll /r:XSharp.RT.dll
Function Start() As Void
Local aTopics := {"SG01","SG02 ", "SG03"} As Array
// SG02 will be found despite the blank at the end
Local strTopic := "SG02" As String
Local Pos := (Int) AScan(aTopics, {|aVal|aVal=strTopic}) As Int
? Pos
The next example does the same with integers.
**Example 9.13: (XS_AscanExample2.prg) **
// Searching a int array with AScan
// compile with /dialect:VO /r:RuntimeLibs/XSharp.Core.dll /r:RuntimeLibs/XSharp.RT.dll
Function Start() As Void
Local a1 := {} As Array
AAdd(a1, 11)
AAdd(a1, 22)
AAdd(a1, 33)
AAdd(a1, 44)
// Does the value 33 exists in the Array?
?AScan(a1, {|x|x = 33})
// Does the value 55 exists in the Array?
?AScan(a1, {|x|x = 55})
The next example shows a little more of the versatility of AScan() and searches a two-dimensional array.
**Example 9.14: (XS_AscanExample3.prg) **
// Scanning a 2d array with AScan until all values found
// compile with /dialect:VO /r:RuntimeLibs/XSharp.Core.dll /r:RuntimeLibs/XSharp.RT.dll
Function Start() as Void
Local AValues := {} As Array
AAdd(AValues, {"A", 1000})
AAdd(AValues, {"A", 2000})
AAdd(AValues, {"B", 100})
AAdd(AValues, {"B", 200})
AAdd(AValues, {"A", 3000})
Local pos := 0 As DWord
While (pos := AScan(AValues, {|f|f[1]=="A"}, pos+1)) > 0
? AValues[pos][2]
End While
The last example again searches an array with an array as one of its values, but this time all arrays are searched.
**Example 9.15: (XS_AscanExample4.prg) **
// Seaching a two dimensional array with AScan
// compile with /dialect:VO /r:runtimeLibs/XSharp.Rt.dll /r:runtimeLibs/XSharp.Core.dll
Function Start() As Void
Local a1 := Array{} As Array
AAdd(a1, {0,0,0,0})
AAdd(a1, {0,11,22,33,44})
AAdd(a1, {0,111,222,333,444})
AAdd(a1, {0,0,0,0})
// Scan each array in a1 for containing non 0 values
// ax is Usual, a cast would look like (Array)ax
ForEach var ax in a1
?AScan(ax, {|z|z > 0})
Next
// prints 2 because this index in the second array is not 0
? AScan(a1[2], {|z|z > 0})
Searching a .Net array
The .Net runtime offers no direct equivalent to AScan(). One reason that X# developers probably won't miss such an equivalent for .Net array is that LINQ offers much more flexibility. Not only for searching but also for sorting arrays and several other things. LINQ will be discussed in chapter 12.
First, a "proof" that LINQ queries are possible with VO-Arrays too.
**Example 9.16: (XS_ArraySearchWithLinq.prg) **
// Searching a VO array with LINQ
// compile with xsc .\XS_ArraySearchWithLinq.prg /r:XSharp.Core.dll /r:XSharp.Rt.dll
using System.Linq
Function Start() As Void
Local aValues := {} As Array
AAdd(aValues, {"A", 1000})
AAdd(aValues, {"A", 2000})
AAdd(aValues, {"B", 100})
AAdd(aValues, {"B", 200})
Var Values := (From w In aValues Where w[1] == "A" Select w[2]):ToList()
For Local i := 0 Upto Values:Count - 1
?Values[i]
Next
For a .Net-Array there are other ways for searching an array like the (extension) method Where. Since X# does not have a Like operator like Visual Basic, using the IsMatch() method of the Regex class is a good alternative. Don't worry about regular expressions (which are not discussed in this book anyway), the simplest form of a regular expression is just the search term without any ingredients like * or ? (don't ever use * as a placeholder in a regular expression;)).
**Example 9.17: (XS_DotNetArraySearchWithWhere.prg) **
// Searching a .Net array with LINQ
using System.Linq
using System.Text.RegularExpressions
Function Start() As Void
var cities := <String>{"New York","Rio","Tokyo"}
var searchTerm := "Tokio"
var result := cities:Where({c => regex.IsMatch(c, searchTerm)}):ToList()
? result:Count
searchTerm := "Tokyo"
result := cities:Where({c => regex.IsMatch(c, searchTerm)}):ToList()
? result:Count
TIP: If you prefer a like operator in X#, the following article shows how to implement a IsLike() extension method: http://www.blackbeltcoder.com/Articles/net/implementing-vbs-like-operator-in-c. But I recommend using regular expressions because they are so much more flexible, powerful and used in all major programming languages (and not difficult to understand as well).
More operations with arrays
This section contains a few more operations that can be done with arrays.
Sorting a VO Array
If AScan() searches a VO array, what function does the sorting of a VO array? It's ASort() of course. Like AScan it can be used with a Codeblock (Lambda) which makes it very flexible. Since the X# Help contains many examples, I will keep this section as short as possible (but omitting it completely without any example is not an option).
**Example 9.18: (XS_VOArraySorting1.prg) **
// Simple sorting of a VO array with ASort()
// compile with /dialect:VO /r:runtimeLibs/XSharp.Core.dll /r:runtimeLibs/XSharp.Rt.dll
Function Start() As Void
var cities := {"Plochingen", "Göppingen", "Esslingen", "Reutlingen", "Eislingen"}
var sortedCities := ASort(cities)
ForEach var city in sortedCities
?city
Next
The next examples show a few of the muscles the ASort() function has when it comes to sorting a multidimensional array.
**Example 9.19: (XS_VOArraySorting2.prg) **
// Using a lambda expression for sorting a VO array with ASort()
// compile with /dialect:VO /r:runtimeLibs/XSharp.Core.dll /r:runtimeLibs/XSharp.Rt.dll
Function Start() As Void
var cities :=
var sortedCities := ASort(cities,,, {|c1,c2| c1[1] > c2[1]})
ForEach var city in sortedCities
?i"{city[2]} has a population of {((Int)city[1]).ToString(""n0"")}"
Next
Sorting a .Net Array
Sorting a .Net Array is possible of course but each member of the array has to implement the interface IComparable or IComparable
If each value is "comparable" sorting an array just means to use the static Sort method of the Array class.
**Example 9.20: (XS_ArraySorting1.prg) **
// Sorting a .Net Array - Part 1
Function Start() as void
Local values := Decimal[]{3} As Decimal[]
values[1] := (decimal)233.434
values[2] := (decimal)22 / 7
values[3] := 567.89M
Array.Sort(values)
ForEach var v in values
? v
Next
Sorting is done by the numerical order of each number. This simple scheme won't work of course if the values in the array are based on a custom class like Document. To sort by the value of a property there must be a designated property or a mechanism for providing a lambda (function) that does the sorting. Neither is the case with the Array-Sort-Method.
**Example 9.21: (XS_ArraySorting2.prg) **
// Sorting a .Net Array - Part 2
// Does not work, it raises a InvalidOperationException exception
Class Document
Property Id As Int Auto
Property Title As String Auto
End Class
function Start() as void
Local values := Document[]{3} As Document[]
values[1] := Document{}{Id := 1000, Title := "X# for ever"}
values[2] := Document{}{Id := 1001, Title := "C# for ever"}
values[3] := Document{}{Id := 1002, Title := "F# for ever"}
Array.Sort(values)
ForEach var v in values
? v:Title
Next
The error message of the resulting InvalidOperationException says it all: "At least one object has to implement IComparable". As already explained, there has to be a mechanism for defining the sort order.
So, let's implement IComparable (or IComparable< T>) which is much easier than it might appear. There is only one method to implement: CompareTo which expects a single argument.
**Example 9.22: (XS_ArraySorting3.prg) **
// Sorting a .Net Array - Part 3
using System
Class Document Implements IComparable
Property Id As Int Auto
Property Title As String Auto
Method CompareTo(o As Object) As Int
Return String.Compare(Self:Title, ((Document)o).Title)
End Class
Function Start() as void
Local values := Document[]{3} As Document[]
values[1] := Document{}{Id := 1000, Title := "X# for ever"}
values[2] := Document{}{Id := 1001, Title := "C# for ever"}
values[3] := Document{}{Id := 1002, Title := "F# for ever"}
Array.Sort(values)
ForEach var v in values
? v:Title
Next
For every comparison the Sort method has to make it just calls the implemented CompareTo method which does any kind of comparison necessary and either returns 0 (both values equal), 1 (Self:Title > o: Title), or -1 (Self:Title < o:Title).
Since the object type stays usually the same with every call it's recommended to implement IComparable<T> instead so that the parameter is of type T.
Another glimpse of LINQ
Too lazy to implement interfaces needed for sorting and when using this technique it feels like programming in the 90s again? LINQ comes to your rescue which makes sorting anything that is somehow enumerable easy.
Disclaimer: LINQ, which was introduced with .Net 3.51 in the year 2008, is already 15 years old but it still feels new and shiny. And it's not an acronym (I think, although something like 'Language Integrated (Language) Neutral Queries' would fit).
**Example 9.23: (XS_ArraySortingLINQ.prg) **
// Sorting a .Net Array with LINQ
using System
using System.Linq
Class Document
Property Id As Int Auto
Property Title As String Auto
End Class
function Start() as void
Local Documents := Document[]{3} As Document[]
Documents[1] := Document{}{Id := 1001, Title := "C# for ever"}
Documents[2] := Document{}{Id := 1002, Title := "F# for ever"}
Documents[3] := Document{}{Id := 1000, Title := "X# for ever"}
var sortedDocuments := From d in Documents OrderBy d.Id Select d
ForEach var d in sortedDocuments
? i"{d.Title} (Id={d.Id})"
Next
LINQ will be covered in more detail in chapter 12 of this book so I will omit any explanations at this point. Except for this one: Sorting with LINQ always works with plain objects that only contain some properties. These objects can be part of an array, a list, a dictionary, or something else. The X# help has some very good LINQ examples.
Deleting elements in a VO array
Deleting something in an array would be a little bit challenging if this had been done without a function. For VO arrays ADel() is the function to use.
Example 9.24: (XS_ADelExample1.prg) ** You will find the source code in the repository
Interestingly deleting an element from a .Net array is much more complicated. Since the Array class offers no Remove method, deleting an element would mean excluding the elements to remove while copying all other elements into a new array. I will not provide an example because it makes more sense to use a list class like List
Lists
Like an array, a list can hold any number of elements, but instead of having to rely on runtime functions or operators, the class of the list is based on offers methods like Add or Remove. Another name for lists in the Microsoft universe is collections although a "collection" class with this particular name does not exist. There are two important namespaces in the .Net runtime with list classes: System.Collections and System.Collections.Generic. The most common non-generic list class is the ArrayList class which is simple to use. The most common generic list class is List
The following command creates a new list that can hold any kind of value:
var names := ArrayList{}
Like Arrays lists can be initialized too. The syntax for X# closely resembles the C# syntax except for an extra pair of curly braces:
```var actors := List
There are many operations possible with lists like inserting, deleting, updating, searching elements, etc. I won't discuss them in this book because there are literally "myriads" places on the Internet that explain these data structures and algorithms in great detail. Any technique and code sample for C# can be applied 1:1 to X# as well.
### Generic Lists
A generic list is a list that only accepts values of a specific kind. The general syntax *List<T>*, where T stands for a type name, is the typical syntax for any generic list or dictionary. I usually prefer generic lists over non-generic lists.
The following command creates a new generic list that can hold only strings:
````
var names := List<String>{}
````
Initializing a generic list is no different from initializing a non-generic counterpart:
```var actors := List<String>{}{"Robin Williams", "Michael J. Fox", "Bruce Willis", "William Shatner"}
var actors := List<String>{}{"Robin Williams", "Michael J. Fox", "Bruce Willis", "William Shatner"}
Each element of a generic list is accessed with an index that starts with 0:
actors[1]
Since a list can contain other lists, the index syntax is indistinguishable from accessing a two-dimensional array:
?i"In Back to the Future part 2 Jennifer Parker is played by {movies[1][2]}"
Example 9.25: (XS_GenericListOfLists.prg) ** You will find the source code in the repository
NOTE: Its worth mentioning that the X# array type internally uses a List\
Interfaces as list elements
It might seem a little unusual at first that interfaces can used as a data type for generic lists.
For example.
Local vehicles := List<IVehicle>{} As List<IVehicle>
It's important to understand that an interface type is a (data) type like any other type although it just contains the method signatures but not any implementation.
A variable with an interface type can be assigned to any object that implements that interface.
Grouping list classes by their interfaces
The .Net runtime offers about two dozen different kinds of list classes. So many choices. Whoever might need a PriorityQueue class? A good way of getting a better overview is by grouping the classes by the interfaces they implement. Most collection classes implement several interfaces. The often-used List
- IList`1
- IList
- ICollection`1
- ICollection
- IEnumerable`1
- IEnumerable
- IReadOnlyList`1
- IReadOnlyCollection`1
The `1 stands for one generic type.
The implemented interfaces define the capabilities of a list. An interface that every list needs is IEnumerable or IEnumerable
TIP: Implementing your list classes that behave like regular list classes by implementing the needed interfaces is a very good learning exercise. The official .Net documentation contains a very good example of this. Have a look here https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerable.
Thanks to .Net reflection it's really simple to list the names of the interfaces any class implements in X# too. You only have to call the GetInterfaces() method of the type object that contains all the type metadata of an object.
Example 9.26: (XS_GenericListInterfaces.prg) ** You will find the source code in the repository
Fig 9.2: Each list class implements several interfaces
Dictionaries
A dictionary is a special kind of list where every value is accompanied by another value - the key. The key (value) is used to access value instead of a numeric index. The key can be any kind of value, an int, a string, an object, or even another dictionary.
Dictionaries offer several advantages over two-dimensional arrays:
- More flexibility
- Better performance (with a large number of pairs)
- Make code better readable
There may be even more;) I would prefer a dictionary over a 2D array most of the time because of its performance and readability.
The .Net runtime offers simple dictionary classes like Hashtable and generic dictionaries like Dictionary<S,T> where S denotes the type of the key and T the type of the value.
The following examples use a dictionary based on a Hashtable class by storing CityInfo objects and making each accessible by their "Kfz-Kennzeichen".
The dictionary is instantiated without any further details about the key-value-pairs:
var cities := Hashtable{}
A key-value-pair is either added by the Add() method or by assigning the value through the pair directly
cities:Add("ES", CityInfo{}{Name:="Esslingen", Population:=89000})
cities["S"] := CityInfo{}{Name:="Stuttgart", Population:=700000}
In the first example, the key is "ES", and in the second example "S". It's important to understand that the key can be any type of value, not only string or int. It can be an object or even another hashtable.
Enumerating a dictionary means enumerating the keys and accessing each value through the key.
Example 9.27: (XS_Dictionary.prg) ** You will find the source code in the repository
And now: generic Dictionaries
X# uses (generic) dictionaries much like C# does. Instead of one the definition of a generic dictionary uses two type placeholders like S for the key and T for the value.
var cities := Dictionary<S, T>{}
Even the initialization of a Dictionary<S,T> with values is possible.
The following example resembles the last example except that this time a generic _Dictionary<String,CityInfo>_is used with a key of type String and a value of type CityInfo. All values are then printed again in a loop by using the key.
Example 9.28: (XS_GenericDictionary1.prg) ** You will find the source code in the repository
The next example initializes the dictionary with key-value pairs with its instantiation. Again, a big compliment to the X# developers to make this possible.
**Example 9.29: (XS_GenericDictionary2.prg) **
// Example for using a Dictionary<T,S>
using System.Collections.Generic
Class CityInfo
Internal Property Name As String Auto
Internal Property Population As Int Auto
Public Method ToString() As String
Return i"City: {Self:Name} Population: {Self:Population}"
End Class
Function Start() As Void
Local cities := Dictionary<String, CityInfo>{} {;
{"ES", CityInfo{}{Name:="Esslingen", Population:=89000}},;
{"PL", CityInfo{}{Name:="Plochingen", Population:=34000}},;
{"GP", CityInfo{}{Name:="G�ppingen", Population:=45000}};
{"S", CityInfo{}{Name:="Stuttgart", Population:=45000}};
} As Dictionary<String, CityInfo>
ForEach k As String In cities:Keys
? cities[k]
Next
Getting keys and values
Another nice thing about dictionaries is that there is no need for extra functions, it's all built-in. Like querying for keys and values. And the null operator ? can be helpful too.
The ContainsKey() method checks for a key, the ContainsValue() method for a value. There is no equivalent for checking for both a key and a value but the TryGetValue() method comes close.
var d1 := Dictionary<String, Logic>{}
Local dValue As Boolean
if d1:TryGetValue("K1", out dValue)
? i"K1: {dValue}"
Else
? "No Value for K1"
EndIf
Accessing a variable that might be null would normally result in a NullReferenceException. This can be avoided with the ? operator:
d1?:Add("K1", True)
If d1 is null nothing will happen.
If a key does not exist, a KeyNotFoundException exception is the result. There is no way to avoid this exception but to use the ContainsKey() method.
Example 9.30: (XS_DictionaryKeysAndValues.prg) ** You will find the source code in the repository