This section is about a new rule where a function can change its inputs for real. We can partly do that now with pointers – fixName(dog1); can adjust dog1 – but this lets us do it with ints, strings, floats, and structs.
We try not to use this rule if there’s any better way. But it’s good for the times you need it, and it’s a basic computer science concept. The normal way, which copies inputs, is named call-by-value. This new way is call-by-reference.
This rounds a float to the nearest value, changing it in place:
Notice how all it does is change n into the correct value. We’ve seen functions like this before. It works because n is a solid link to the variable you gave it:
Here are the rules for call-by-reference (what you type, mixed in with how it works):
For examples, roundMe(ref 12.3f) is an error – not a box. roundMe(ref x+1.0f); is an error – also not a box. int n=4; roundMe(ref n) is an errors – not a float box. But roundMe(ref c1.wt); and roundMe(ref A[2]); are fine.
In float q=3.6f; roundMe(ref q); I like to think that we’re giving it q. Without the ref, we’re merely giving it 3.6. With ref we’re handing over the entire box.
Here’s another version of the old Clamp function which changes the input into the correct range, using call-by-ref:
Notice how all it does is change num. Running it looks like this:
Here’s moveTowards rewrite. It pushes the input float towards the target, but not past:
moveMeBy(ref n, 10, 0.1f); pushes n 0.1 closer to 10, stopping on
it.
Those three are not useful functions. At first, roundMe(ref x); seems better than the hacky x=round(x);. But a return value let’s us do more:
A swap is an actually useful call-by-ref function. We’ve seen the 3-step swap dance. Now we can put it into a function:
Notice how both have the ref in front, since both need to change. A sample use:
Another real one I sometimes use is for computing a min and max. If it needs to, it adjust one of them to contain the new value:
It doesn’t seem like much, but it can help find the min and max of a list:
If saves me from writing 2 if’s, has a descriptive name, and there’s no way to write it without using call-by-ref.
We can use the call-by-ref trick to have a function return 2 things. We’ll give the function 2 empty boxes, asking it to put the answers in them.
This function attempts to break a string into 2 words if it has a comma. We can’t return 2 things, but we can fake it with 2 “output” parameters:
In our minds, this returns w1 and w2. A sample run:
commaSplit feels as if it has 1 input, and 2 slots for the outputs.
Suppose we often want to convert floats like 5.32 into a 5 and a 0.32. We could write a “return-by-reference” function for that. In our minds it has 1 real input, and 2 outputs:
We call it by making 2 spots for the results and calling with the as the last 2 inputs:
As we’ve seen, there are 2 main ways to use call-by-reference. The first is when we give it a box to possibly adjust. The function will read from it, and might change it or might not. The second is giving blank boxes for the answers. The function has no reason to read from them, and will definitely fill them all with the answers.
C# decided to make an extra rule for that second way: out for “output parameter”. It’s the same as ref except with more error messages (you can’t read from them, and you have to assign to them).
Here’s commaSplit rewritten with out:
It’s the exact same, except for the word out.
It’s another fun example of language design. On the one hand, out is a tiny change from ref, and you don’t need it, and it’s another special word to remember. On the other hand, why not build the idea of output parameters into the language?
It turns out that C# is the only language to that chose to add it. In fact, Java decided the whole call-by-reference thing was so rare and hacky that Java doesn’t have even ref.
We often have functions that either give an answer, or they don’t work. Often they can return -1 or null, but not always. Sometimes we like to have them return a bool saying it they worked, and give back the real answer in a reference parm.
For example, int.TryParse is a C# built-in that attempts to convert w into an int, or else returns false:
For "37" it will return true, and num will be 37. For "3cat" it returns false and
num’s value won’t matter.
The more common way to use it looks very strange at first:
Normally, functions inside of an if shouldn’t do anything except return true or
false. But in this case, it seems fine to have it also fill n.
We can rewrite commaSplit to use this style. It returns false if there isn’t a comma:
The new way to use it:
We can already change classes from inside of a function, so it seems like we wouldn’t also need call-by-reference for them. But sometimes we do.
This function to swap two goats (assume Goat is a class) does nothing:
Here’s a picture after we call swap(g1, g2);:
If we change a.age, we’re changing the real age. But making a point to b is simply moving a local variable. We can’t change where g1 points, only the contents of what it points to.
Adding ref in front of a and b fixes the problem:
Now changing a is also changing g1.
This next one is simpler, but somewhat made-up. Suppose we want a function to blank out a goat, which works for null goats. This version doesn’t work:
It’s the same problem. We can’t make the original goat pointer aim at a different Goat. ref fixes it:
Raycast is probably the most complicated function in Unity – it uses an output parameter with a bool return, and has a bunch of overloads and default parameters. It makes a nice example of lots of things, including call-by-reference.
What it actually does is pretend to shine a laser pointer in the 3D world. It
tells us what it hit, if anything. We use it to shoot a game laser, or check
where a mouse is aimed, or check whether we have room to move in a certain
direction.
With all the combinations of overloads and default parameters, there are 16 versions! Four are simplifications of this monster:
The last 3 use default parameters. That means we can ignore them, leave them out, and the system fills them with the standard values. That leaves us with this:
The first two inputs are the imaginary ray we’re shooting: where it starts in 3D, and the arrow to follow coming out of the start.
The third is the answer. out means it’s an output parameter, which means we merely need to give it a blank RaycastHit struct. That’s a specially made struct, whose only purpose is to hold results of a raycast. As usual, hitInfo is an unimportant variable name.
So here’s a legal use of raycast. It shoots from the camera, straight forward:
But a raycast could also hit nothing. That’s why it returns a bool. False means it missed. We use it like this:
Let’s play around just a little more. The 3 parameters we skipped are distance, layerMask, and triggerInteraction. We have to fill them in from left-to-right, which means we can use distance.
This shoots a ray for 20 units (if something is 21 units away, it won’t reach and returns false):
Another version uses a Ray as the first input. Here’s the struct Ray. It’s incredibly simple, for a struct:
All it does is group together a starting position and a direction arrow. Here’s a raycast using a Ray:
Finally, here’s a semi-useful script to test raycasts. The player moves along the bottom of the screen, shooting a raycast straight up. Whatever it hits turns red, then white when we move away:
You may have noticed that we now have two different uses of the word reference: call-by-reference, and reference types. That’s an unfortunate accident, especially since they have similar meanings, but different.
Reference types are actual variables, with their own boxes. Which are pointers. Call-by-reference variables aren’t their own variables. They’re alternate names for the original box.
It’s common to say Dog d1; is a reference. Inside a function where w1 was passed-by-reference we’d also call w1 a reference. Yikes!
Then take this function:
d1 is a reference variable passed by value. d2 is a reference passed by reference. x
is a value-type passed by reference. Yikes, yikes!