A First Example of Class Instances: Contact

Making a Datatype

C# comes with lots of built-in datatypes, but not everything we might want to use. We start with a very simple example of building your own new type of object: Contact information for a person involves several pieces of data, and they are all unified by the fact that they are for one person, so we would like to store them together as a unit. For simplicity, let us just consider the contact information to be name, phone number, and email address.

You could always keep three independent string variables, but conceptually the main idea is the contact. It just happens to have parts.

In order to treat a contact as one entity, we create a class, Contact. This way we can have a single variable refer to a Contact object. Such an object is an instance of the class.

It is important to distinguish between a class and an instance of a class: A class provides a template or instructions to make new instance objects. A common comparison is that a class is like a cookie cutter while an instance of the class is like a cookie. You might consider constructor parameters as being for different decorations on different cookies, so not all cookies must end up completely the same.

Later we will see an example for rational numbers, The Rational Class, where the parts of the class are more tightly integrated, but that is more complicated, so we defer it.

We have already considered built-in types with internal state, like a List: Each List can contain different data, and the internal data can be changed.

The idea of creating a new type of object opens new ground for managing data. Thus far we have stored data as local variables, and we have called functions, with two ways to get information in and out of functions:

  1. In through parameters and out through returned data.

  2. Directly via the user: in through the keyboard and out to the screen.

We have stored and passed around built-in types of object using this model.

We have alternatives for storing and accessing data in the methods within a new class that we write. Now we have the idea of an object that has internal state (like a contact with a name, phone, and email). We shall see that this state is not stored in local variables and does not need to be passed through parameters for methods within the class. Pay careful attention as we introduce this new location for data and the new ways of interacting with it.

This is quite a shift. Do not take it lightly.

We can create a new object with the new syntax. We can give parameters defining the initial state of the new object. In our example the obvious thing to do is supply parameters giving values for the three parts of the object state, so we can plan that

Contact c = new Contact("Marie Ortiz", "773-508-7890", "mortiz2@luc.edu");

would create a new Contact storing the data. A Contact object, created with new is an instance of the class Contact.

Like with built-in types, we can have the natural operations on the type as methods. For instance we can

  • look up individual pieces of the contact data with methods GetName, GetPhone and GetEmail

  • print it all out together, labeled, with method Print.

Thinking ahead to what we would like for our Contact objects, here is the testing code of contact1/test_contact1.cs:

using System;

namespace IntroCS
{
   public class TestContact
   {
      public static void Main()
      {
         Contact c1 = new Contact("Marie Ortiz", "773-508-7890",
                                  "mortiz2@luc.edu");
         Contact c2 = new Contact("Otto Heinz", "773-508-9999",
                                  "oheinz@luc.edu");
         Console.WriteLine("Marie's full name: " + c1.GetName());
         Console.WriteLine("Her phone number: " + c1.GetPhone());
         Console.WriteLine("Her email: " + c1.GetEmail());
         Console.WriteLine("\nFull contact info for Otto:");
         c2.Print();
      }
   }
}

When running this testing code, we would like the results:

Marie's full name: Marie Ortiz
Her phone number: 773-508-7890
Her email: mortiz2@luc.edu

Full contact info for Otto:
Name:  Otto Heinz
Phone: 773-508-9999
Email: oheinz@luc.edu

We are using the same object oriented notation that we have for many other classes: Calls to instance methods are always attached to a specific object. That has always been the part through the . of

object.method()

So far we have been thinking and illustrating how we would like objects in this Contact class to look like and behave from the outside. We could be describing another library class. Now, for the first time, we start to delve inside, to the code and concepts needed to make this happen. We start with the most basic parts. First we need a class:

Class Syntax

Our code is nested inside

public class Contact
{

   // ... fields, constructor, code for Contact omitted

}

This is the same sort of wrapper we have used for our Main programs! Before, everything inside was labeled static. Now we see what happens with the static keyword omitted….

Instance Variables

A Contact has a name, phone number and email address. We must remember that data. Each individual Contact that we use will have its own name, phone number and email address.

We have used some static variables before in classes, with the keyword static, where there is just one copy for the whole class. If we omit the static we get an instance variable, that is the particular data for an individual Contact, for an individual instance of the class. This is our new place to store data:

We declare these in the class and outside any method declaration. (This is in the same place as we would store Static Variables).

They are fields of the class. As we will discuss more in terms of safety and security, we add the word “private” at the beginning:

public class Contact
{
   private string name;
   private string phone;
   private string email;

   // ... constructor, code for Contact omitted

}

You also see that we are lazy in this example, and abbreviate the longer descriptions fullName, phoneNumber and emailAddress.

It is important to distinguish instance variables of a class and local variables. A local variable is only accessible inside the block (surrounded by braces) where it was declared, and is destroyed at the end of the execution of that block. However the class fields name, phone and email are remembered by C# as long as the Contact object is in use.

The lifetime of a variable is from when it is first created until it is no longer accessible by the program. We repeat:

Note

Instance variable have a completely different lifetime and scope from local variables. An object and its instance variables, persist from the time a new object is created with new for as long as the object remains referenced in the program.

We need to get values into our field variables. They describe the state of our Contact.

We have used constructors for built-in types. Now for the first time we create one.

Constructors

The constructor is a slight variation on a regular method: Its name is the same as the kind of object constructed, so here it is the class name, Contact. It has no return type (and no static). Implicitly you are creating the kind of object named, a Contact in this case. The constructor can have parameters like a regular method. We will certainly want to give a state to our new object. That means giving values to its fields. Recall we are want to store this state in instance variables name, phone and email:

      public Contact(string fullName, string phoneNumber, string emailAddress)
      {
         name = fullName;
         phone = phoneNumber;
         email = emailAddress;
      }

While the local variables in the formal parameters disappear after the constructor terminates, we want the data to live on as the state of the object. In order to remember state after the constructor terminates, we must make sure the information gets into the instance variables for the object. This is the basic operation of most constructors: Copy desired formal parameters in to initialize the state in the fields. That is all our simple code above does.

Note that name, phone and email are not declared as local variables. They refer to the instance variables, but we are not using full object notation: an object reference and a dot, followed by the field.

So far always we have always been referring to a built-in type of object defined in a different class, like arrayObject.Length. The constructor is creating an object, and the use of the bare instance variable names is understood to be giving values to the instance variables in this Contact object that is being constructed. Inside a constructor and also inside an instance method (discussed below) C# allows this shorthand notation without someObject..

Instance Methods

The instance variable names and method names used without an object reference and dot refer to the current instance. Whenever a constructor or non-static method in the class is called, there is always a current object:

  1. In a constructor, referring to the object being created. In execution, a static method like Main must create the first object.

  2. When some instance method methodName is called with explicit dot notation, someObject.methodName(), then it is acting on the current object someObject. In any program’s execution the first call to an instance method must either be in this explicit form or from within a constructor for a new object.

  3. If that constructor or instance method calls a further instance method inside the same class, without using dot notation, then the further method has the same current object…. We will see examples of this as we go along.

Again, this means that in execution, whenever an instance method is called, there is a current specific object. This is the object associated with any instance variable or method referred to in that method, if there is not an explicit prefix in the someObject. form. This will take practice to get used to.

Getters

In instance methods you have an extra way of getting data in and out of the method: Reading or setting instance variables. (As we have just pointed out, in execution there will always be a current object with its specific state.) The simplest methods do nothing but reading or setting instance variables. We start with those:

The private in front of the field declarations was important to keep code outside the class from messing with the values. On the other hand we do want others to be able to inspect the name, phone and email, so how do we do that? Use public methods.

Since the fields are accessible anywhere inside the class’s instance methods, and public methods can be used from outside the class, we can simply code

      public string GetName()
      {
         return name;
      }

      public string GetPhone()
      {
         return phone;
      }

      public string GetEmail()
      {
         return email;
      }

These methods allow one-way communication of the name, phone and email values out from the object. These are examples of a simple category of methods: A getter simply returns the value of a part of the object’s state, without changing the object at all.

Note again that there is no static in the method heading. The field value for the current Contact is returned.

A standard convention that we are following: Have getter methods names start with “Get”, followed by the name of the data to be returned.

In this first simple version of Contact we add one further method, to print all the contact information with labels.

      public void Print()
      {
         Console.WriteLine (@"Name:  {0}
Phone: {1}
Email: {2}", name, phone, email);
      }

Again, we use the instance variable names, plugging them into a format string. Remember the @ syntax for multiline strings from String Special Cases.

You can see and see the entire Contact class in contact1/contact1.cs.

This is our first complete class defining a new type of object. Look carefully to get used to the features introduced, before we add more ideas:

This Object

We will be making an elaboration on the Contact class from here on. We introduce new parts individually, but the whole code is in contact2/contact2.cs.

The current object is implicit inside a constructor or instance method definition, but it can be referred to explicitly. It is called this. In a constructor or instance method, this is automatically a legal local variable to reference. You usually do not need to use it explicitly, but you could. For example the current Contact object’s name field could be referred to as either this.name or the shorter plain name. In our next version of the Contact class we will see several places where an explicit this is useful.

In the first version of the constructor, repeated here,

      public Contact(string fullName, string phoneNumber, string emailAddress)
      {
         name = fullName;
         phone = phoneNumber;
         email = emailAddress;
      }

we used different names for the instance variables and the formal parameter names that we used to initialize the instance variables. We chose reasonable names, but we are adding extra names that we are not going to use later, and it can be confusing. The most obvious names for the formal parameters that will initialize the instance variables are the same names.

If we are not careful, there is a problem with that. An instance variable, however named, and a local variable are not the same. This is nonsensical:

public Contact(string name, string phone, string email)
{
   name = name; // ????
   ...

Logically we want this pseudo-code in the constructor:

instance variable name = local variable name

We have to disambiguate the two uses. The compiler always looks for local variable identifiers first, so plain name will refer to the local variable name declared in the formal parameter list. This local variable identifier hides the matching instance variable identifier. We have to do something else to refer to the instance variable. The explicit this object comes to the rescue: this.name refers to a part of this object. It must refer to the instance variable, not the local variable. Our alternate constructor is:

      public Contact(string name, string phone, string email)
      {
         this.name = name;
         this.phone = phone;
         this.email = email;
      }

Setters

The original version of Contact makes a Contact object be immutable: Once it is created with the constructor, there is no way to change its internal state. The only assignments to the private instance variables are the ones in the constructor. In real life people can change their email address. We might like to allow that with our Contact objects. Users can read the data in a Contact with the getter methods. Now we need setter methods. The naming conventions are similar: start with “Set”. In this case we must supply the new data, so setter methods need a parameter:

      public void SetPhone(string newPhone)
      {
         phone = newPhone;
      }

In SetPhone, like in our original constructor, we illustrate using a new name for the parameter that sets the instance variable. For comparison we use the alternate identifier matching approach in the other setter:

      public void SetEmail(string email)
      {
         this.email = email;
      }

Now we can alter the contents of a Contact:

Contact c1 = new Contact("Marie Ortiz", "773-508-7890", "mortiz2@luc.edu");
c1.SetEmail ("maria.ortiz@gmail.com");
c1.SetPhone("555-555-5555");
c1.Print();

would print

Name:  Marie Ortiz
Phone: 555-555-5555
Email: maria.ortiz@gmail.com

ToString Override

We created the Print method for a Contact, and it is helpful to assemble all the data for display and print it. The issue there is that the method does two separate clear things: combining the string data and printing it. You might want the same string but put in a file or used some other way. Our Print method will not help.

A good design decision is to separate the different actions: the first is to generate the 3-line string showing the full state of the object. Once we have this string, we can easily print it or write it to a file or …. Hence we want a method to generate a descriptive string.

Think more generally about string representations: All the built-in types can be concatenated into strings with the ‘+’ operator, or displayed with Console.Write. We would like that behavior with our custom types, too. How can the compiler know how to handle types that were not invented when the compiler was written?

The answer is to have common features among all objects. Any object has a ToString method, and that method is used implicitly when an object is used with string concatenation, and also for Write. The default version supplied by the system is not very useful for an object that it knows nothing about! You need to define your own version, one that knows how you have defined your type, with its own specific instance variables. You need to have that version used in place of the default: You need to override the default. To emphasize the change in meaning, the word override must be in the heading:

public override string ToString()
{
   return string.Format (@"Name:  {0}
 {1}
 {2}", name, phone, email);
}

See what the method does: it uses the object state to create and return a single string representation of the object.

For any kind of new object that you create and want to be able to implicitly convert to a string, you need a ToString method with the exact same heading as the ToString for a Contact.

A more complete discussion of override would lead us into class hierarchies and inheritance, which we are not emphasizing in this book.

We still might like to have a convenience method Print. It now can be written much more easily, using our latest ToString.

We want one instance method, Print to call another instance method ToString for the same object. How does this work? It is like when instance method GetName refers to an instance variable name without using dot notation. Then name is assumed to refer to this object associated with the call to GetName. We can change our Print definition to:

public void Print()
{
   Console.WriteLine(ToString());
}

Here ToString() is a method called without dot notation explicitly attaching it to an object. As with instance variables, it is implicitly attached to this object, the one attached to the call to Print.

Again, the whole code for the elaborated Contact is in example contact2/contact2.cs.

New testing code is in contact2/test_contact2.cs. Run the project and check that it does what you would expect. There are several new features illustrated in the testing code:

public static void Main()
{
   Contact c1 = new Contact("Marie Ortiz", "773-508-7890",
                            "mortiz2@luc.edu");
   Contact c2 = new Contact("Otto Heinz", "773-508-9999",
                            "oheinz@luc.edu");
   Console.WriteLine("Marie's full name: " + c1.GetName());
   Console.WriteLine("Her phone number: " + c1.GetPhone());
   Console.WriteLine("Her email: " + c1.GetEmail());
   Console.WriteLine("All together:\n{0}", c1);
   Console.WriteLine("Full contact info for Otto:");
   c2.Print();
   c1.SetEmail("maria.ortiz@gmail.com");
   c2.SetPhone("123-456-7890");
   Contact c3 = new Contact("Amy Li", "847-111-2222",
                            "amy.li22@yahoo.com");
   Console.WriteLine("With changes and added contact:");
   var allc = new List<Contact>(new Contact[] {c1, c2, c3});
   foreach(Contact c in allc) {
      Console.WriteLine("\n"+c);
   }
}

Contact is now a type we can use with other types. Main ends creating a List<Contact> and an array of Contacts, and processes Contacts in the List with a foreach loop.

We mentioned that this particular signature in the ToString heading means that the system recognizes it in string concatenation and in substitutions into a Write or WriteLine format string. Find both in Main.

The ToString override also means that the body of our Print definition in the Contact class could have been even shorter, using the object this:

public void Print()
{
   Console.WriteLine(this);
}

When we use Console.WriteLine on this current object, which is not already a string, there is an automatic call to ToString.

Local Variables Hiding Instance Variables

A common error is for students to try to declare the instance variables twice, once in the regular instance variable declarations, outside of any constructor or method and then again inside a constructor, like:

public Contact(string fullName, string phoneNumber, string emailAddress)
{
   string name = fullName;      // LOGICAL ERROR!
   string phone = phoneNumber;  // LOGICAL ERROR!
   string email = emailAddress; // LOGICAL ERROR
}

This is deadly. It is worse than redeclaring a local variable, which at least will trigger a compiler error.

Warning

Instance variable only get declared outside of all functions and constructors. Same-name local variable declarations hide the instance variables, but compile just fine. The local variables disappear after the constructor ends, leaving the instance variables without your desired initialization. Instead the hidden instance variables just get the default initialization, null for an object or 0 for a number.

There is a related strange compiler error. This is not likely to happen frequently, but thinking through its logic (or illogic) could be helpful in understanding local and instance variables: Generally when you get a compiler error, the error is at or before the location the error is referenced, but with local variables covering instance variables, the real cause can come later in the text of the method. Below, when you first refer to r in Badnames, it appears to be correctly referring to the instance variable r:

class ForwardError
{
    private int r = 3;

    // ...

    void BadNames(int a, int b)
    {
        int n = a*r + b; // legal in text *just* to here; instance field r
        //...
        int r = a % b; // r declaration makes *earlier* line wrong
        //...
    }

The compiler scans through all of BadNames, and sees the r declared locally in its scope. The error may be marked on the earlier line, where the compiler then assumes r is the later declared local int variable, not the instance variable. The error it sees is a local variable used before declaration.

This is based on a real student example. This example points to a second issue: using variable names that that are too short and not descriptive of the variable meaning, and so may easily be the same name as something unrelated.

Lifetime and Scope Exercise/Example

Be careful to distinguish lifetime and scope. Either a local variable or an instance variable can be temporarily out of scope, but still be alive. Can you construct an example to illustrate that? One of ours is lifetime_scope/lifetime_scope.cs.