This chapter is about making and using a list of items. The chapter on using indexes with strings was a warm-up for this. Looking through a list is about the same as looking through the letters in a string.
The new thing is that each box in a list counts as a real variable. We can assign to them as well as read from them. A list is often a fast way to create a pile of similar variables all at once, giving us the ability to use them in a loop.
Every list holds one type, which goes inside a set of angle-braces. This declares a list of integers:
It’s our first 2-part type. You can’t use just List by itself, you always need the
second part, with a type in those brackets. The boxes in each list can be any type,
but they all have to be the same.
Let’s pretend N is [20 60 90 1]. This loop prints every box:
It’s the same as a loop to print every letter in a string, except lists use Count for the length (instead of Length, the way strings do. Why change the name? Beats me).
We can assign to boxes, which is new. In a list, N[0] is a working int the same way as d.age. Boxes in lists are L-Value’s. These are legal:
This loop changes everything in a list to 6:
We can’t add to a List as easily as to a string. We can add one thing at a time, only to the back end, using the Add command:
The same as for strings, going off the edge of a list is a run-time null reference exception error. For examples N[-3] or N[999]. If N has length 8 then N[8] is one past the end, and also an error.
Now for the details we skipped and explanations:
As mentioned, List’s need to have a type. List A; is nothing, and an error. List<string> is a list of strings. This should make complete sense. When we look at N[i] we need to know exactly what type it will be. We can’t have a list where any box could be any type.
More fun list types we can declare:
< and > as parenthesis are called angle brackets. We borrowed them from HTML
(and XML and JSON). The computer uses them for types.
GetComponent<Renderer>() uses them that way – Renderer is the type of thing
we’re looking for.
List’s are a reference type (oh, no!) But it’s OK. That only means that we’re required to new them. There won’t be any other monkey business. New lists start with nothing in them. Ex’s:
Notice how the same type goes in both places - the declare and the new. That was
always the rule (like Dog d = new Dog();), but we didn’t notice as much before
since the types didn’t have those extra <>’s.
We often like how lists start empty. We add what we want, stopping when we have enough. The final list is as big as it is. Here we want a list of random numbers that add to 20 or more:
The final N might be [3,8,1,7,4]. The 4 put the total over 20, quitting the loop. We
didn’t know the final list would have 5 slots.
Other times we want the list to start at an exact size and stay there. For example, we have 6 seats and are arranging people. The list should start out with 6 ""’s. We can make that with a quick loop:
You may have noticed the funny way Add looks – it has a dot like a field. It’s a member function, which we’ll see later. For now, it’s enough to know N.Add(3) grows N, while Q.Add(3) grows Q.
It also takes a different type depending on the list. It’s sort of like an overloaded function – int-list Adds takes ints, and so on. It doesn’t say it adds to the back, since that’s the obvious best place. The front would cause us to change the index of everyone else’s box.
Add also grows the list by a box and puts the new value there, which is like 2
things at once. Usually we like that – Flowers.Add("Petunia");. If all we want to
do is add a box, we can use a dummy value like N.Add(0).
An altogether list example:
As usual, we sometimes need variables and math for the indexes. Assume W has several boxes:
Setting values of int-lists can look odd, with numbers in and out of the []’s. For examples:
It can be equally funny-looking assigning int’s using some math. Real programs can have lines like these:
We can use the regular shorcuts. N[0]+=4; adds 4 to box 0, and N[0]++; adds 1.
For string lists, W[i]+="y"; adds a y to the end of that box.
A new thing we can do is a “slide”. N[k]=N[k+1]; feels like k is pulling the next box into it. N[k-1]=N[k]; is like k pushing itself into the next lower box. Both are slide-lefts.
N[k+1]=N[k]; feels like k is pushing itself to the right. N[k]=N[k-1]; is like k
pulling from the next lower box. They each slide right. These can all be used in loops
to slide the entire list.
Swapping two boxes is fun. Recall a normal swap is int tmp=a; a=b; b=tmp;. An array swap is the same:
If we want a pre-made list with certain values, there are a few tricks.
This is a repeat of when we want 10 slots, and will fill them later. We’ll Add ten empty-strings:
With int lists, it’s semi-common to start each box with its index. To do that, use i inside the Add:
If we wanted it to be 1,2,3 instead, we’d use NN.Add(i+1);. For 10, 20, 30 it
would be NN.Add((i+1)*10);
If our list starts with a more interesting sequence, we can have a loop hit the exact numbers, Add’ing. This makes a list with 20, 18, 16 …2. It will be as long as it needs to be for them to fit:
Sometimes it’s easier to create the list first, then use loops to set the values. Here evens are “goat” and odds are “cow”. One loop handles each:
Sometimes, for testing, it’s nice to fill a list with random numbers. This loop puts 1 to 100 in each slot:
Character lists aren’t very common. Strings are better, usually. But char lists allow us to easily change each character. You might use one for a Hangman game starting with all underscores:
Keeping to the hangman theme, we could use a list of 26 bools to remember which letters were guessed. There’s nothing special – each slot is true or false, but it looks odd:
Often we simply hand-Add each item:
In Unity we can cheat. public List<string> ColorsWords; appears in the
Inspector. We can pop it open, type a size (it starts at 0), and hand-enter the values.
When our program starts, the entire list is magically created.
An older use that you don’t see as much is a reading loop. Pretend readLine() reads one line from a file, with "" for when it’s past the end. We can write a “go until done” while loop with an Add as the body:
Most systems don’t read this anymore. But it’s a fun loop, regardless. readLine is driving it, and you can check it works for a 0-line file, and Adds the last line, but not the "" past-the-end line.
Like structs, there’s no built-in command to print an entire list. You usually use an index loop to look at each part. This turns an int list into a string with commas and parens. To get the commas correct, I’m going to do the first one by hand, then have the loop start at 1:
A bool list is fun to print, since we can shorten true and false to T and F. I’m going to leave out the commas:
It’s semi-common to dress up floats a little bit when printing a list. A list full of 8.333333333’s is going to overflow the screen. Unity sometimes rounds them to tenths, for printing. We can do that (I’m using a different trick for the commas):
This would show numbers like 0.003 as just 0. That’s usually fine. If your list is full of very small numbers, you wouldn’t print it this way.
A basic list search is the same as a string search, like counting how many ’e’s. But, with numbers, we can search for different sorts of things.
This warm-up loop counts how many 7’s there are in an int list. Nothing special about it:
We can count how many positive numbers there are. This is still pretty much the same, but it feels like it should have been harder to do:
We can do all the math we need inside the loop. This one counts how many are 11, 22, 33 …99. If you hate the math, the important part is N[i] is the current int, and we can compute lots of stuff for ints:
This counts how many strings in a list are 6 letters or longer:
W[i].Length is 2 parts. W[i] is a string. Maybe it’s narwhale". So W[i].Length
is the number of letters in narwhale. To compare, W[i].Count would be an error.
Strings don’t have a Count.
Finding the largest number is a clever rewrite of the Price-is-Right trick. Without a list, I did something like biggest=a; if(b>biggest) biggest=b; if(c>biggest) biggest=c; and so on. One if for each number.
The beauty of a list is we can do the same thing, but using a loop to run every if:
In my mind, the loop says “now give every box past 0 a chance to win.” If
we “unroll” the loop and write all the lines, it’s just if(N[1]>biggest)
biggest=N[1];, then again for every number after that. Lists are pretty
cool.
Here’s the same idea, but finding the shortest string in a list. It compares the lengths of the strings. I added test prints for fun:
If we ran this on ["aardvark", "cow", "horsey", "ox", "bear"] we’d see it
print aardvark, then cow, then ox, and the answer is ox.
My random town example used a big if-else to pick town names. A list can make picking a random word easier. Put all the words in a list, then pick a random index and use the word there.
This picks one word at random from any list:
With four words, the indexes are 0 to 3, which is exactly what Random.Range(0,
Ani.Count) can roll. This is the real reason random int works that way – back in the
day everyone knew “random 4” meant 0,1,2, or 3.
With randomness it’s hard to notice code that’s off-by-one. For example,
Random.Range(0, Ani.Count+1) could roll past the end and crash. But you might
test it 400 times, never have that happen at random, and think it works. It’s also
hard to notice if there’s 1 animal you can never get.
A hacky trick to pick one word more often is to repeat it in the list. If we add an extra Ani.Add("toad"); then we have 2 of them. We’ll roll a toad twice as often.
In the random word example, the list is just a bunch of stuff, and the order doesn’t matter. We also like to use lists to make a sequence: use item 0, then a little later use item 1, and so on.
For example, suppose I want the space bar to print words one at a time, from a
list.
The trick is to think of it like a slow loop. We’ll have an index starting at 0, add 1 each time, and when it hits Count we’ve gone off the edge. Here’s how it looks:
Here’s a completely different-seeming program using the same idea. I want a Cube to pop to preset spots, once a second. The preset locations will be stored in a slow-walked list.
For examples, [-7, 7, -5, 5, -3, 3] would make it dance around the center, closing in. But using [0, 2, 1, 3, 2, 4, 3, 5, 4, 6] makes it stagger-walk to the right. The code:
This is just the movement code and the slow-walk-list code crammed
together.
The trick can work for anything. Here’s a version using a list to control the delay. To keep it simple, this Cube pops left-to-right by 0.5 each tick. The code:
It’s kind of fun to try different numbers. Changing to [10, 10, 10, 100] would have it scramble 3 quick steps, then a pause. [80, 60, 40, 20, 10, 5] would have it appear to gather momentum.
This is another one of those programs that took a lot more work that it looks. It took me a few tries to make it start waiting using the first delay, and more work to have it not double-use the first delay.
Sometimes our program needs a pile of nearly identical ints. Using one list to
make them all is easier than declaring them individually, and can make the
program much simpler through the magic of indexing (seriously, this is a great
trick).
In this example, I want to roll a 6-sided die 50 times and count how many of each number. We need six int’s to hold the results, which we can get with a size-6 list. In our minds. Count[0] is how many 1’s we rolled, up to Count[5] is how many 6’s. The code:
The magic part is Count[roll-1]++;. In our minds, Count holds six regular variables. Count[roll-1] is using roll to look up which one. If roll is 1, that gives us Count[0], which holds how many 1’s we’ve gotten. If we roll a 6, the whole line says Count[5]++;, which is adding one to the 6’s box.
It’s a really clever trick to make a “variable variable.” Without a list, if we had 6
individual variables, we’d need a 6-part if: if(roll==1) count1++; else
if(roll==2) count2++; and so on.
Here’s the same trick for saving high scores. Pretend we have 10 levels in a game, with the high scores stored in a list. HighScore[0] is the high score for the first level, and so on.
The player can jump around between levels somehow. Code like this would look up the high score for the level they’re leaving, and update it if needed:
It’s another case where HighScores[levelNow] saves us a ten-part if.
There are some general tricks involving moving around the boxes in a list. These pop
up in a few places, and are good index practice.
A useful one is a shuffle. The final list will have the same things in it, but mixed up. The simplest way to do this is using one loop. Go through each item, and switch it with some random position:
One funny thing – we might switch with ourself. newPos could be equal to i. That
won’t break anything, and is actually good. A real shuffle has a small chance of
keeping each thing in the same place, and so does ours.
One use of a shuffle is randomly rolling 1-6 with no repeats. First create a list of 1 to 6, shuffle it, then slow-walk as you need the numbers:
Partial code:
It’s reverse thinking. It feels like we should do the random part as we request each
new number. But we can pre-load the randomness. In other words, it’s like
shuffling a deck of cards, then getting random cards by dealing from the
top.
Some fun exercises, which you don’t use much for real, are rotating a list – moving every variable one space left or right, and wrapping around. Here’s a picture of both types of shifts:
The main problems are the wrap-around and not erasing anything. To rotate left we save N[0], then shift everything from N[1] onward to the next lower box, then copy the saved N[0] to the right side:
As a check, with i starting at 1, the first slide is N[0]=N[1];, which is
correct.
For practice, we could also write that using a pull-left: N[i]=N[i+1];. We’d start at 0 and go to 1 less than the end:
Same check: since i now starts at 0, the first is N[0]=N[0+1]; – still
correct.
Rotating a list to the right is also good for index practice. We save the last item, then go right to left, sliding items forward one space (for fun, try going left to right instead, it just copies N[0] into every box):
Another quick check: the last slide is at i=1, so is N[1]=N[1-1], which is correct.
There are some fun things we can do using two lists.
With lists, copying List<int> B=A; doesn’t work at all. It just makes B point to A, with both sharing the same real list. Instead you need to copy the list, a box at a time:
Combining two lists end to end uses the same idea. Start with a fresh one, copying A then B into it:
The Add in the second loop always fools me. It’s running 0,1,2 …through B. But those numbers are only for B. The Add puts then on the end of C, at whatever box that happens to be.
Usually a size-0 list is temporary until we add our items. But sometimes the final list is size 0. That’s fine. If D is a list of the dogs you hate, and you like all dogs, it’s size-0. A loop over a size-0 list won’t cause an error – it quits right away.
In a size 0 list, D[0] is an error. You can’t look up any boxes, because there
aren’t any. That’s usually not a problem since a loop won’t try to check
it.
Having a final list be size 1 also seems like a waste. But it’s often what we want. You only hate 1 dog, but you could have hated a lot more.
Even though D[0] is the only box, you still have to write it out. D="Spike"; is an
error. The computer won’t figure out that D[0] is the only place it could
go.
List inputs and outputs to functions don’t have any new rules, but they’re nice to see.
A list can be an input to a function. You just put the type, like List<int> or List<string>.
This checks whether a string list contains a certain word:
You pass a list the usual way, with just the name:
Double equals, ==, won’t properly compare lists, since they’re pointers. As usual, if(A==B) checks whether both point to the same list, which is not useful. To check whether two different lists are the same, we need to hand-walk through both, comparing pairs at each index:
Same as before, you run it with just the names: if(equals(N1,N2)).
A fun one is checking whether a list has all numbers in increasing order, like [3, 8, 25, 26, 30]. It works like the double-letters example for strings. Every number compares to the one before it, and needs to be larger:
The early return true/false logic can be tricky. If just one pair isn’t going up, the
overall answer is false. We may as well quit and say that. But we can’t say the entire
list is increasing until we’re checked them all.
Since lists are pointers, changing a list in a function is changing the real list. In other words, you can write functions whose purpose is to change a list. This changes negative numbers in a list to zero:
We’d call it like fixNegatives(N1);.
We can return lists from functions. Usually the function creates the list and returns a pointer to it.
This function makes a list full of 0’s of whatever size you want. Notice how the return type is List<int>:
List inputs and outputs together are common. Here’s a basic list clone. It’s merely the list-copy code from above, in a function:
List<string> W2 = getListCopy(W1); runs it.
We can put the combine end-to-end code in a function. I think it’s clear enough this takes two lists and returns another:
Another exercise which is fun, but not all that common, is pair-wise adding two lists (pair-wise means using matching positions, like A[0]+B[0]). It looks like this:
If they aren’t the same length the options are to stop when the shorter runs out, or to pretend the rest are 0’s. I did it the first way, since it’s simpler. Here’s a function returning a pair-wise list add:
Returning a list computed form the input list is always fun. This returns only the even numbers. It’s pretty much the same as removing all z’s from a string:
Notice this could return a length-0 list, if everything in the input is odd, and that’s fine.
Lists give you the usual pointer errors. Using one that hasn’t been new’d will give run-time error null reference exception:
We’ll get all the usual list-index-out-of-bounds errors if we go past the end. And the last index is still N.Count-1.
A common off-end is assuming two lists are the same length, forgetting to compare sizes. This throws a null-ref error if B is shorter than A:
Another semi-common one is not checking for size 0 (or sometimes even 1.) This function returns the smallest item in the list. It crashes if the input is size 0:
The first line should be: if(N.Count==0) return -1;.
Another reminder: since these are pointers we get the same pointer non-error
errors as with classes: A=B; and if(A==b) work in the funny pointer way.
List lives in the namespace System.Collections.Generic. Pre-made Unity files have that line on the top. If you write your own from scratch, List by itself will give “not found” errors.
You can write out System.Collections.Generic.List<float> F;. Or, easier, copy that using line to the top of your file.
For testing, it’s a pain to make sample lists with lots of Add’s. There’s a way to make them all at once, which is very ugly, but still shorter:
It makes a normal list, so we could still use Animals.Add("worm"); later, if we
somehow needed to.
If you were wondering, there are some languages where you can just write N=(3,5,8); and have a list. But those languages have their own places you have to work extra hard to do something that seems like it should be easy.
We’re generally happy pretending that lists are normal variables. We might take advantage of their pointerness to change the contents inside of functions, but that’s about it. Even so, we can do real pointer things with lists.
Here L1 and L2 count are considered regular lists, while Lp acts as a pointer:
We can use this trick to choose between 2 lists in the slow-word display:
The rest of the code can use CurWords like it was a normal list, even though it’s
always W1 or W2.
We can write a function returning a pointer to whichever list has more 7’s:
The return value isn’t a new list – it’s a pointer to one of the lists we gave it: