By taking advantage of information hiding to"wrap"a standard collection type inside of one that we've invented, we've allowed for the flexibility to change the internal details of this implementation without disrupting the client code that takes advantage of this collection type. Down the road, we may wish to switch from using an ArrayList to a different predefined collection type, and because we've declared our students collection as a private attribute, we're free to do so, as long as we don't change the headers of our existing public methods.Now, how do we use this collection class that we've invented? Let's show it in action in the Course class. public class Course { // We declare an attribute to be a collection of type // EnrollmentCollection, and will use it to manage // all of the students who register for this course. private EnrollmentCollection enrolledStudents; // Other simple attributes. string courseName; int credits; // etc. // Parameterless constructor. public Course() { enrolledStudents = new EnrollmentCollection(); // Other details omitted. } // Other constructors' details omitted. public bool Enroll(Student s) { // All we have to do is to pass the Student reference // in to the collection's enroll method; the collection // does all of the hard work! This is another example of // delegation. enrolledStudents.Enroll(s); } public bool Drop(Student s) { // Ditto! enrolledStudents.Drop(s); } // etc. } Collections provide a way to overcome the limitation that we noted in Chapter 4 about methods only being able to return a single result. If we define a method as having a return type that is a collection type, we can hand back an arbitrary sized collection of object references to the client code that invokes the method.In the code snippet shown next for the Course class, we provide a GetRegisteredStudents method to enable client code to request a"handle"on the entire collection of Student objects that are registered for a particular course: public class Course { private EnrollmentCollection enrolledStudents; // Other details omitted ... // The following method returns a reference to an entire collection // containing however many students are registered for the course in question. EnrollmentCollection GetRegisteredStudents() { return enrolledStudents; } An example of how client code would then use such a method is as follows: // Instantiate a course and several students. Course c = new Course(); Student s1 = new Student(); Student s2 = new Student(); Student s3 = new Student(); // Enroll the students in the course. c.Enroll(s1); c.Enroll(s2); c.Enroll(s3); // Now, ask the course to give us a handle on the collection of // all of its registered students ... EnrollmentCollection ec = c.GetRegisteredStudents(); // ... and iterate through the collection, printing out a grade report for // each Student (pseudocode). for (each Student in EnrollmentCollection) { s.PrintGradeReport(); } Of course, if we return a direct handle on a collection such as enrolledStudents to client code, we are giving client code the ability to modify that collection (e.g., removing a Student reference). Design considerations may warrant that we create a copy of the collection before returning it, so that the original collection is not modified: public class Course { private EnrollmentCollection enrolledStudents; // Other details omitted ... // The following method returns a COPY of the Student's enrolledStudent // collection, so that client code can't modify the OFFICIAL version. EnrollmentCollection GetRegisteredStudents() { EnrollmentCollection temp = new EnrollmentCollection(); // Pseudocode. copy contents of enrolledStudents to temp return temp; } } Another way to avoid the problem of allowing client code to modify a collection in the previous example would be to have the GetRegisteredStudents method return an enumeration of the elements contained in the collection.We'll discuss how to use enumerators in Chapter 13. Collections of Super types. We said earlier that arrays, as simple collections, contain items that are all of the same type: all int (egers), for example, or all (references to) Student objects. As it turns out, this is true of collections in general: we'll typically want to constrain them to contain similarly typed objects. However, the power of inheritance steps in to make collections quite versatile.It turns out that if we declare an array to hold objects of a given type-e.g., Person-then we're free to insert objects explicitly declared to be of type Person or of any types derived from Person-for example, UndergraduateStudent,GraduateStudent, and Professor.This is due to the"is a"nature of inheritance: UndergraduateStudent, GraduateStudent, and Professor objects, as subclasses of Person, are simply special cases of Person objects. The C# compiler would therefore be perfectly happy to see code as follows: Person[] people = new Person[100]; // of Person object references Professor p = new Professor(); UndergraduateStudent s1 = new UndergraduateStudent(); GraduateStudent s2 = new GraduateStudent(); // Add a mixture of professors and students in random order to the array; // as long as Professor, UndergraduateStudent, and GraduateStudent are // all derived from Person, the compiler will be happy! people[0] = s1; people[1] = p; people[2] = s2; // etc. As we'll see when we discuss C# collection types in more detail in Chapter 13, C# collections other than Arrays actually don't allow us to specify what type of object they will hold when we declare a collection, as illustrated here: ArrayList list = new ArrayList(); // No type designated! // ArrayLists hold generic Objects. versus: Student[] s = new Student[100]; // With Arrays, we DO specify a // type (Student, in this case) Most C# collections are automatically designed to hold objects of type Object, which as we learned in an earlier chapter is the superclass of all other classes in the C# language, user defined or otherwise. So, from the compiler's perspective we can put whatever types of object we wish into a collection in any combination. But, it's still important that you, as the programmer, know what the intended base type for a collection is going to be from an application design standpoint, so that you discipline yourself to only insert objects of the proper type (including derived types of that type) into the collection. This will be important when we subsequently attempt to iterate through and process all of the objects in the collection: we'll need to know what general class of object they are, so that we'll know what methods they can be called upon to perform.We'll talk about this in more detail when we discuss polymorphism in Chapter 7. Composite Classes, Revisited You may recall that when we talked about the attributes of the Student class back in Chapter 3, we held off on assigning types to a few of the attributes, as shown in Table 6-1. Table 6-1. Proposed Data Structure for the Student Class Armed with what we now know about collections, we can go back and assign types to attributes course Load and transcript.