10.2. Class Instance Examples
10.2.1. More Getters and Setters
As an exercise you could write a simple class Example, with
int
instance variablen
anddouble
instance variabled
a simple constructor with parameters to set instance variables
n
andd
getters and setters for both instance variables (4 methods in all)
a ToString that prints a line with both instance variables labeled.
Compare yours to example_class/example_class.cs.
We include a testing class at the end of this file.
class ExampleTest
{
public static void Main()
{
Example e = new Example(1, 2.5); // use constructor
// this creates the first Example object with reference e
Console.WriteLine("e.n = {0}", e.GetN()); // prints 1
Console.WriteLine("e.d = {0}", e.GetD()); // prints 2.5
Console.WriteLine(e); // prints Example: n = 1, d = 2.5
e.SetN(25);
e.SetD(3.14159);
Console.WriteLine("e.n = {0}", e.GetN()); // prints 25
Console.WriteLine("e.d = {0}", e.GetD()); // prints 3.14159
Console.WriteLine(e); // prints Example: n = 25, d = 3.14159
Example e2 = new Example(3, 10.5);
// this creates the second Example object with reference e2
Console.WriteLine(e2); // prints Example: n = 3, d = 10.5
e = e2; // now both e and e2 reference the second object
// the first Example object is now no longer referenced
// and its memory can be reclaimed at runtime if necessary
Console.WriteLine(e); // prints Example: n = 3, d = 10.5
e2.SetN(77); // symbolism uses e2, not e
Console.WriteLine(e2); // prints Example: n = 77, d = 10.5
Console.WriteLine(e); // prints Example: n = 77, d = 10.5
// but e is the same object - so its fields are changed
}
} // end of class ExampleTest
Besides the obvious tests, we also use
the fact that an Example object is mutable to play with References and Aliases:
In the
last few lines of Main
, after e
becomes an alias for e2
,
we change
an object under one name, and it affect the alias the same way.
Check this by running the program!
Make sure you can follow the code and the output from running.
Beyond Getters and Setters
Thus far we have had very simple classes with instance variables and
getter and setter methods, and maybe a ToString method.
These classes were designed to introduce the basic syntax for
classes with instances. The classes have just been containers for data
that we can read back, and change if there are setter methods - pretty
boring and limited. Many objects have more extensive behaviors, so we will
take a small step and imagine a somewhat more complicated Averager
class:
A new
Averager
starts with no data acknowledged.Be able to enter data values one at a time (method
AddDatum
). We will usedouble
values.At any point be able to return the average of the numbers entered so far (method
GetAverage
). The average is the sum of all the values divided by number of values. Withdouble
values we assume adouble
average. This does not make sense if there are no values so far, but with double type we can use the valueNaN
(Not a Number) in this case.Be able to return the number of data elements entered so far (method
GetDataCount
)Be able to clear the
Averager
, going back to no data elements considered yet, like in a newAverager
(methodClear
)It is good to have a
ToString
method. We choose to have it label the number of data items and the average of the items.
The object starts from a fixed state (no data) so we do not need any constructor parameters.
We can imagine a demonstration class AveragerDemo
with a Main
method
containing
Averager a = new Averager();
Console.WriteLine("New Averager: " + a);
foreach (double x in new[] {5.1, -7.3, 11.0, 3.7}) {
Console.WriteLine ("Next datum " + x);
a.AddDatum (x);
Console.WriteLine("average {0} with {1} data values",
a.GetAverage(), a.GetDataCount());
}
a.Clear();
Console.WriteLine("After clearing:");
Console.WriteLine("average {0} with {1} data values",
a.GetAverage(), a.GetDataCount());
It should print
New Averager: items: 0; average: NaN
Next datum 5.1
average 5.1 with 1 data values
Next datum -7.3
average -1.1 with 2 data values
Next datum 11
average 2.93333333333333 with 3 data values
Next datum 3.7
average 3.125 with 4 data values
After clearing:
average NaN with 0 data values
Now that we have a clear idea of the proposed outward behavior, we
can consider how to implement the Averager
class.
We could store a list of all the data values entered in any instance, requiring a large amount of memory for a long list. However, this functionality was built into early calculators, with very limited memory. How can we do it without remembering the whole list? Consider a concrete example:
If I have entered numbers 2.1, 4.5 and 5.4, and want the average, it is
\((2.1+4.5+5.4)/3=12.0/3=4.0\)
If I want to include a further number 5.0, the average becomes
\((2.1+4.5+5.4+5.0)/4=17.0/4=4.25\)
Note the relationship to the previous average expression:
\(=((2.1+4.5+5.4)+5.0)/4=(12.0+5.0)/(3+1)\)
In the numerator we have added the latest value to the previous sum,
and in the denominator the count of data items is increased by one.
All we need to remember to
go on to include the next item is the sum of values so far and the
number of values so far. We can just have instance variables
sum
and dataCount
.
You might think how to create this class….
The full Averager
code follows:
using System;
namespace IntroCS
{
/// a class that is more than a container
class Averager
{
private int dataCount;
private double sum;
/// new Averager with no data
public Averager()
{
Clear();
}
public void AddDatum(double value)
{
sum += value;
dataCount++;
}
public int GetDataCount()
{
return dataCount;
}
/// Gets the average of the data
/// or NaN if no data.
public double GetAverage()
{
return sum/dataCount; // is NaN if dataCount is 0
}
public void Clear()
{
sum = 0.0;
dataCount = 0;
}
public override string ToString ()
{
return string.Format("items: {0}; average: {1}",
GetDataCount(), GetAverage());
}
}
}
Several things to note:
We show that a constructor, like an instance method, can include a call to a further instance method. Though we illustrate this idea, the constructor code is actually unneeded. See the Unneeded Constructor Code Exercise below.
We have methods that are not ToString or mere getters or setters of instance variables. The logic of the class requires more.
The
GetAverage
method does return data obtained by reading instance variable, but it does a calculation using the instance variables first. See Alternate Internal State Exercise.
The code for both classes is in project averager.
10.2.2. Statistics Exercise
Modify the project averager so the Averager
class is
converted to Statistics
. Besides having methods to calculate the count
of data items and average, also calculate the standard deviation with
a method StandardDeviation
.
It turns out that the only other instance variable needed is
the sum of the squares of the data items, call it sumOfSqr
.
Before calculating the standard deviation, suppose we
assign the current average to a local variable avg
.
Then the handiest form of expression for the standard deviation is
Math.Sqrt((sumOfSqr - avg*avg)/dataCount)
Modify the demonstration program to show the standard deviation, too.
10.2.3. Unneeded Constructor Code Exercise
Recall that objects are always initialized. Each instance variable
has a default value assigned before a constructor is even run.
The default value for numeric instance variables is 0, so the
call to Clear
in the constructor could be left out, leaving the
body of the constructor empty! Try commenting that line out
and test that the behavior of demo program is the same.
Emphasizing the fact that you are thinking about the
initial values of instance variables is not a bad idea. Hence
a common practice is to
explicitly assign even the default values in the constructor, as
we did initially with the call to Clear
inside the constructor.
If no constructor definition is explicitly provided at all,
the compiler implicitly creates one with no parameters and an empty body.
This means that the entire constructor in Averager
could be omitted.
Comment the whole constructor out and check.
There is a defensive programming reason to provide even the do-nothing constructor explicitly: If you use the implicit constructor and then decide to add a constructor with parameters, the implicit constructor is no longer defined by the compiler, so any remaining call to it in your code becomes an error.
10.2.4. Alternate Internal State Exercise
The way we represent the internal state for an Averager
is the
best probably, but if it turns out that the GetAverage
method
is called a lot more often than a method that changes the state,
we could avoid repeated division by saving the average as an
instance variable. We could keep that instead of sum
(and still keep dataCount
). We can manage to
keep the same public interface to the methods. With these
alternate instance variables how would you change
the implementation code and not change the method headings or meanings?
If we keep the assumption that the average of 0
items is
double.Nan
, you will need to treat adding the first datum as
a special case. The code is simpler if we change the outward assumptions
enough to consider the average of 0 items
to be 0. Try it either way.
In the version with NaN you can avoid testing for NaN,
but if you choose to test for
NaN, you need the boolean Double.IsNaN
function, because the expression
double.NaN == double.NaN
is false!