When we make a class, we often write some helpful functions using it. For the Dog class we had setDog(d1,"Spike",2) and showDog(d1);. In our minds, those were Dog functions.
Member functions is a trick to make them feel more like Dog functions. We can have d1.set("Spike",2); and d1.show();, and it feels like d1 is setting itself, or running its personal show function.
This is really the other half of how to make a class or struct. After we add fields,
we also add functions. Both are used with a dot.
Member functions don’t actually do anything new. We’re merely moving around where we define some functions and changing the way we call them. This might feel like a lot of rules for not much gain. But it’s a nice way to organize things, all of the built-in classes and structs use them, and we’ll need them for some Object-Oriented tricks later on.
Here’s a rewrite of the old setupDog function as a member function:
The basic rule is that we can use name and age directly. We learned you can never do that – you have to say which Dog it is first. But member functions have that built into the call. pet2.setup("Spot",5) remembers pet2 called it. Clearly name is the name of pet2. If we run it again with d1.setup("",0), we’re now clearly using d1’s fields.
It’s a very clever trick. You have to call them using Dog-dot – that’s a rule. So a
member function is guaranteed to be running for one Dog. It gets to use that Dog’s
fields in a simple, fast way.
There’s nothing special showing how setup is a member function. Functions
written inside of a class are automatically member functions. As we can see, they use
parameters the normal way. They can return things. They have only that one extra
rule: always running “for” some Dog.
It helps to see why we’re doing all this work. Some of the advantages of having member functions:
These little things add up. Once you know how, instead of writing a helpful
function, you write a helpful member function.
Here are the member function rules listed out:
Here are some more Dog member functions, all with no inputs:
We know they really all have 1 input – the dog that calls them.
desc() is the same simple “help print me” function we’ve written for structs before. We’d call it like string w = dog2.desc();. As usual, it uses name and age and knows they mean dog2’s name and age. Notice how it returns a value the same way as a normal function.
reset() is showing the rule of how you’re also allowed to change the fields. pet1.reset(); would blank out the inside of pet1.
isAPuppy() only reads age, which is fine. We’ve seen the function-in-an-if trick
with regular functions. Now we can use if(d2.isAPuppy()).
Member functions with inputs use them in the normal way. This checks whether a dog is within an age range:
Now bool adult = d2.inAgeRange(4,9)) is legal.
You’re allowed to have a member function that doesn’t use any fields, but you’d never do it. It would be pointlessly confusing. This terrible function picks a random Dog age:
We be required to call it like int n=d1.randomDogAge();. But it doesn’t use d1
for anything, or change it. It runs the exact same no matter what Dog we give it.
Why did we require you to run it from a Dog – that’s just confusing. It would be
better as a normal function.
The most complex member functions take another Dog as input. The standard example is copying an input Dog into ourself. d1.copyFrom(d2) makes d1 a copy of d2:
Inside, dd.name uses the input Dog, the same as any other function, while name is
our name.
Here’s a simple class for a 2D point with 2 member functions:
p1.set(1, 7.2f); runs the first one. It’s allowed to change the inside of p1. The input xx’s and field x’s can be confusing – people try all sorts of naming rules to tell inputs from fields.
float dist=p1.fromCenter(); runs the second. Nothing special – it reads p1’s x and y, and returns the answer (that’s the actual formula for distance).
Way back in the struct chapter we made something to hold the four variables we need to slowly move and wrap-around. This was it:
That was a nice way to group them. But the code hand-moved each variable, like m1.val+=m1.spd;. That would be way better as a function. Now that we know member functions, it would be even better as one of those.
If you remember, val goes up or down by spd, wrapping around when it hits min or max:
doMove is nothing special. The math is the same as previously. But it’s written
next to the variables, inside the function, making it a little easier to find.
mover1.doMove(); runs it. Of course, its purpose is to change mover1.val.
We usually write some more helpful member functions. Setting min and max at the same time feels natural, so we should write that:
Here’s our final code to fade from red to black, 20 times in a row:
It’s supposed to feel as if we’re asking the redMove variable to run it’s own doMove function.
A few chapters ago we made a simple class to hold data for one square in a game board. That particular one allowed the user to toggle between two colors (I called that selecting a space) and it showed which space you were “on” by making it a little smaller.
We can make those member functions:
We can now change squares like this:
It looks somewhat nice – a space in the board is turning its highlight on or off.
The pop-up trick also works: Board[col][row]. (a dot) causes the member
functions, including highlight, to pop-up.
When someone presses space we’d use Board[x][y].toggleSelect();. That
handles the coloring.
In both of these examples, 95% of the work was “hey, that should be a function”. The last 5% was seeing that it may as well be a member function.
Member functions can call other member functions. They use the same shortcut rule as variables – no dot, simply write the name. Here Dog’s have a d1.superDesc() which uses their desc() function:
We call desc() and it means the desc() of the current Dog. In other words,
d1.superDesc() knows it’s also running desc() for d1.
Here’s a completely fake function that uses two Dogs – the calling Dog and an extra input Dog. It shows how the rules work even for funny stuff:
The first call to superDesc() knows to use ours, which knows to use our desc(). The second call, other.superDesc(), runs on the other dog, which runs desc() on the other dog. The computer remembers which dog you’re on, even when you flip back-and-forth.
Most of the pre-made structs and classes have member functions. Now that we know how they work, we can enter variable-dot and look for them.
Vector3 has a Set member function – we can write Vector3 pos; pos.Set(-7,2,0);. It’s merely a shortcut for hand-setting them. It looks like this:
There’s nothing special about new_x, that’s just the name they picked.
Oddly, Color doesn’t have a Set function or anything like it. They didn’t write
one.
I used transform.Translate(1,0,0) several chapters ago without explaining the
dot. Now we know it’s a member function. transform is a variable (you get it for free
in Unity if your script is on a gameObject). When you type transform-dot, you can
see field position and member function Translate.
If you have GameObject g;, typing g-dot shows a big list, including one named SetActive(bool);. That tells us we can use g.SetActive(false); to hide it (and again with true to un-hide).
Put another way, we knew there had to be a way to temporarily de-activate a Cube. There is, and it’s a member function.
I lied when I wrote that string was a basic variable type. You probably figured out that string is way more complicated than something like int, float or bool. string is really a built-in C# class with a bunch of special rules to make it act like a basic type.
But since it’s really a class, it has member functions. As usual, we can type w-dot
to find them. Lots of them are really fun:
A funny thing about this, and the ones before it, are that we know member functions can change themselves, but these don’t. We prefer returning the changed version and not changing the original.
Those are all simple loops, mostly things we’re written before. But it’s still nice to have them “in” the computer as easy-to-find member functions. Also, maybe, they used some tricks to run them a little faster than our versions.
Back in the List chapter, we used L.Add(6); to grow L by one box. That’s obviously a member function.
There are a few more:
It works for any index. L.RemoveAt(L.Count-1); gets rid of the last item, which doesn’t need a slide-loop at all.
There are more than that, but that should give the idea.
Inside of a member function, you can use this to mean “me”. For example you could write this.name instead of just name.
Times when you need it are rare and strange. Suppose there’s a real global function taking a dog as input, which our member function needs to run on itself:
There’s no good reason to have a global function like that – it would be a member
function. But on big projects funny stuff can happen. Sometimes you’re stuck using
not-quite-right things.
You’ll sometimes see a style where every member variable has this in front:
In the second function, using this let us re-use the names in the inputs. this.a=a; copies the input a into the member variable a.
We’ve already seen functions like makeDog("Rex",8) which create and return a Dog. But languages like to make those official. We’ve seen it: p1=new Vector3(1,0,9); or cc=new Color(0,0,1);. Those are officially called constructor’s.
An example of a Dog constructor. This allows us to write Dog d1 = new Dog("K-9",12);:
The body of the function is normal, even boring. But the part in front looks strange. Special rules for constructors:
Here are several for a Cow struct. The let us call new Cow() or new Cow("Lu-lu",7,1475) or new Cow("Alice") and do various odd things:
All three of them simply set the 3 Cow member variables. The first one replaces
the old new Cow(). Our basic cow is now Bessy, 2, and 1500. The second is the
standard one that copies the inputs straight into the fields. The third shows that
we can do anything a normal function would do, as long as we set all 3
fields.
Constructors are member functions in a strange way. The first thing they
do is create a Dog or Cow. Then they run as member functions on that.
When we use name=nm;, we’re assigning to the thing that we’re about to
return.
Here’s one more set of constructors for the old FullName class:
We can use f1=new FullName(); as usual, or f1=new FullName("Cher","");,
who gets (no last name) for a last name.
Constructors are allowed to call other functions. It’s common to have a shared setup function:
Every constructor figures out what it wants to values to be, and calls setup as a quick way to assign them.
C# has two very strange rules for constructors. One is about class/struct and we’ve seen it:
For a class, new Dog("Spot",4) allocates a real Dog, on the heap. Then it gets filled in by your code and a pointer is returned (automatically, without you writing anything). But for a struct, new Cow("Bessy",4,1500) creates a temporary Cow. It can be copied into a Cow variable.
It’s the same funny rule as before.
The other strange rule – when you write a class, you get a free constructor. When you write Cow, you automatically get new Cow() that makes empty strings and zero’s.
But if you write a single constructor of your own, the free one goes away. If you wanted it, you have to re-write it yourself. That’s why FullName had it. There’s no special reason for this rule – other languages do it differently.
This is probably obvious: the names of member functions are only for inside the class. It’s the same rule as for member variables. It’s fine to use the same member function names in different classes, or anywhere else outside the class.
This has three doStuff’s, which are fine since they’re in different scopes:
There’s never going to be any confusion between a1.doStuff(), b1.doStuff(); and just regular doStuff();.
Member functions are just regular functions that we thought looked nicer using the variable-dot notation. They look best when there’s one special item. d1.copyFrom(d2) is fine since d1 is changing itself, using d2.
But compares look nicer as normal functions, since neither is special. Below shows checking for 2 equal dogs written both ways:
The first one is if(d1.isEqual(d2)). The second is if(isEqual(dog1,
dog2)).
Vector3.Distance(v1, v2); is another one like this. It could have been v1.Distance(v2);. But since neither point is more special than the other, the first way seems nicer.
This is the last tricky rule. Consider a Cow defined inside one of our scripts:
There are 2 ways to look at Cow. The most obvious way is that every Cow must be created by a cowUser. A Cow would then automatically know about who made it and be able to use those variables.
That’s called an inner class. Some languages use it, like Java, but not
C#.
The other way to look at it is that Cow just happens to be written inside. But it’s really a completely independent struct. Any other script could declare one. A Cow can’t depend on belonging to a particular cowUser.
That’s the way C# does it.