Simple Classes
Up to now, we've dealt with programs consisting of nothing more than simple straightforward functions. Now it's time to get in touch with more complex stuff, so we'll learn to deal with functions and methods. Based on methods, we'll start to implement classes and you'll see how classes can be used to efficiently achieve very specific goals.
Objects and Instances
Some people reading this book might be familiar with object-oriented software development in general and some will already have practical experience. Because we want all readers to benefit from this book, we've dedicated the next section to object-oriented programming and its fundamentals.
Objects are nothing more than data structures, data, and methods for processing the data at once. Objects are templates for a certain piece of data. Every object accepts a set of parameters that define it. The methods of an object can be used only in combination with an object. This leads to an encapsulation of data that is visible only inside the object.
In this section, we take our first look at classes. To get a first impression of objects, it's time to consider an example:
using System; class Demo { public static void Main() { Human hugo = new Human(); } } // The class itself public class Human { public Human () { Console.WriteLine("Born to be alive ..."); } }
In this example, we use two classes. The class called Demo contains the main function that every C# program starts with. In addition to that, we implement a class named Human, which also consists of just one function. This function is known as a constructor. The only target of a constructor is to create new objects. In our case, we call the constructor to generate a variable called hugo. The data type of hugo is Human. To generate additional humans, we could call the constructor more than once.
A constructor must always have the same name as the class it represents. This is an important point because functions that have the wrong name will never be treated as constructors by the compiler. The constructor of the Human object does nothing else than displaying text on the screen. At the end of the program, all variables are deleted automatically.
When working with objects, we must distinguish between objects and instances. Common linguistic usage does not make a big difference between instances and objects, but it's important to understand how things work: Objects are templates that tell us how objects are built and what they look like. A variable generated by a constructor is called an instance. To make the difference clearer, we'll explain the main ideas with the help of an example. The class called Human is the construction plan for all humans. Let's see what happens when we start the constructor:
[hs@duron mono]$ mono human.exe Born to be alive ...
As you can see, a string is displayed.
Functions and Methods
To access an object, a programmer needs methods. If you want hugo to learn to walk, we have to teach him how this works by implementing a method. In the next example, we teach hugo some nice things:
using System; class Demo { public static void Main() { Human hugo = new Human(); hugo.GrowUp(); hugo.DrinkBeer(); } } public class Human { public Human () { Console.WriteLine("Hello World ..."); } public void GrowUp () { Console.WriteLine("Now I am old enough :) "); } public void DrinkBeer () { Console.WriteLine("I am drinking beer"); } }
This version of our program supports more than just one constructor. We've implemented two methods that will help us to treat hugo in the way we want him to be treated. In our example, hugo grows up and drinks his first glass of beer. It's important to see how we've implemented these methods. All methods are public, which means that they can be called by a method that isn't in our classin this case, we're talking about the Main function. If we had declared these methods as private, there would be no way for the main program to work with hugo because private methods are not visible inside the main program.
The return value of a method is important as well. In the code we've seen before, all methods return void (undefined). This makes sense because the code does nothing other than displaying text. If we want a method to perform complex operations, we have to choose a data type that can be used to store the desired result.
The output of our program is in no way surprising:
[hs@duron mono]$ mono ex.exe Hello World ... Now I am old enough :) I am drinking beer
Three lines are displayed.
Variables and Scope
After you've learned how to implement basic classes, we'll take a closer look at variables and scope in general. In addition, we'll deal with constant values.
Constants
If variables must not be changed at runtime, they can be marked as constants. To mark a variable as a constant, the keyword const has to be used.
using System; class MyConst { public static void Main() { const double pi = 3.14; Console.WriteLine("Pi: " + pi ); } }
In this case, we cannot change the value of pi; otherwise, the compiler will report an error. This makes sense because many predefined values rely on this feature.
[hs@duron mono]$ mono ex.exe Pi: 3.14
ScopeThe Basics
Variables are an essential component of every program. However, a variable cannot be seen everywhere inside the program. This is important because otherwise you'd have to invent a new name for every variable in the system. In huge packages, this is nearly impossible. Therefore, every variable has a certain scope, which means that it's available in a clearly defined part of the software. Let's get to some practical stuff and see how the things that we just discussed can be put to work:
using System; class Demo { public static void Main() { string name = "Hugo"; Human hugo = new Human(name); } } class Human { public Human(string objname) { Console.WriteLine("I am " + objname); Console.WriteLine("I am " + name); } }
In the main function, we define a variable and an instance of Human. The constructor of this object tries to access these variables. Because we haven't declared name inside the class, it cannot be seen, so the compiler reports an error:
[hs@duron mono]$ mcs method.cs ex.cs(17) error CS0103: The name 'name' could not be found in 'Human' Compilation failed: 1 error(s), 0 warnings
As you can see, the desired value is not visible. This is a fundamental concept because every variable and method has a clearly defined scope.
Block and Scope
In some cases, variables cannot be seen in an entire function. It can be useful to restrict the scope even more. This makes sense, particularly in the case of loops or conditions. Even for that purpose, C# provides all you need:
using System; class Demo { public static void Main() { int i = 3; if (i == 3) { int j = 25; } Console.WriteLine("j: " + j ); } }
In this example, we create two variables. The first one can be seen in the entire function. However, j can be seen only inside the If block. If we try to print the content of j outside the If block, the compiler will report an error because it cannot find the correct value.
[hs@duron mono]$ mcs ex.cs ex.cs(13) error CS0103: The name 'j' could not be found in 'Demo' Compilation failed: 1 error(s), 0 warnings
The behavior you just saw is important because it helps the programmer to split the code into logical blocks.
Variables
Until now, we've dealt with objects that consist of nothing more than a constructor and a set of methods. However, in some cases, it's useful to store data directly inside an instance. Let's get back to the class called Human we've seen before. A human being has many attributes, such as name, weight, gender, and so forth. Those values can be assigned to an instance. Let's see how this can be done with the help of a simple C# application:
using System; class Demo { public static void Main() { string name = "Hugo"; double weight = 89.2; Human hugo = new Human(name, weight); Console.WriteLine("Name: " + hugo.name); } } class Human { public string name; double weight; public Human(string objname, double objweight) { this.name = objname; this.weight = objweight; } }
We define two variables in the definition of the class. One of these values is public, but we will deal with this subject later in this chapter. Inside the constructor, we assign the desired values to the class's internal values.
Our instance contains the desired information and it can be easily accessed by using a dot and the name of the class's internal value:
[hs@duron mono]$ mono human.exe Name: Hugo
The name is displayed on screen.
In the next example, we try to display the second variable as well. Let's look at the following piece of code:
using System; class Demo { public static void Main() { string name = "Hugo"; double weight = 89.2; Human hugo = new Human(name, weight); Console.WriteLine("Name: " + hugo.name); Console.WriteLine("Name: " + hugo.weight); } } class Human { public string name; double weight; public Human(string objname, double objweight) { this.name = objname; this.weight = objweight; } }
The compiler displays a fatal error:
[hs@duron mono]$ mcs human.cs human.cs(12) error CS0122: 'Human.weight' is inaccessible due to its protection level Compilation failed: 1 error(s), 0 warnings
The reason for the error is simple: Not all variables can be seen from outside. name has been declared public, so everybody can access it from outside. weight can be seen only inside the class itself; therefore, nobody can see it from outside. Protecting variables has many advantages. Especially when your class contains temporary variables that are part of a complex operation, hiding data makes sense.
C# provides an advanced system for defining the scope of variables. The following list is a brief overview of all keywords provided by C#:
private: A variable can be seen only inside the class.
protected: A variable can be seen only inside the class or a derived class.
internal: The variable can be seen inside the same assembly.
protected internal: The variable is visible inside an assembly or a derived class.
public: The variable can be seen outside the class.
The keywords we just discussed enable the programmer to define precisely what can be seen where. It's essential to make use of these keywords to protect your application from disaster.
Functions and Scope
Not only the scope of variables can be limited. Almost everything we've learned about variables can be used for methods as well. With the help of limitations, it's possible to hide functions from the user. Hiding functions can be useful because many classes contain internal functions that must not be exposed to the user for security reasons.
Let's get started with a look at an example:
using System; class Demo { public static void Main() { Human hugo = new Human("Hugo"); hugo.GrowUp(); } } public class Human { public Human(string name) { Console.WriteLine(name + " is born ..."); } public void GrowUp() { this.LearnToRead(); this.LearnToWrite(); } private void LearnToRead() { Console.WriteLine("Reading is simple ..."); } private void LearnToWrite() { Console.WriteLine("Writing is fun ..."); } }
At the beginning of the program, we create an instance of Human. This time we implement various methods. The method called GrowUp is a public methodall other methods are private and cannot be called from the Main function.
The example will lead to a simple result set:
[hs@duron mono]$ mono ex.exe Hugo is born ... Reading is simple ... Writing is fun ...
The GrowUp method calls both other methods, which need not be seen by the Main function because they're called from inside the object.
Methods can be marked with the help of keywords. Let's look at the keywords provided by C#:
abstract: Prototype without implementation.
extern: An external method can be called from modules that are outside the program.
intern: The method can be called only inside the class.
New: Hides a method that has the same name.
Override: Redefines a method that has been marked as virtual in the base class.
Private: The method can be called only inside the class.
Protected: The method can be seen only inside the class or inside a derived class.
Static: Method isn't part of an instance of a certain class.
Virtual: Makes sure that a derived class can be marked as override.
As you've already seen, when talking about methods, it's useful to expose to the user only those functions that are absolutely necessary.