|
|
|
If you've made it this far, give yourself a pat on the back. If you understand everything so far, slap yourself for lying. Hearing someone explain OOP is one thing, but it almost always takes awhile before you start thinking about programs in terms of objects. In the meanwhile, it's time to cover one of the finer points of objects. Remember the "ref" keyword? When it's applied to a parameter in a method, any changes the method makes to that parameter will happen to the original variable, as well. Without it, a method is only operating on a copy of the original variable's data. Do objects work the same way? Actually, with objects, the "ref" keyword is totally redundant, because objects are always passed by reference. If you don't believe me, try this for yourself: class Class1 { static readonly string ln = Environment.NewLine; [STAThread] static void Main(string[] args) { Student stu1 = new Student(1, "Joe"); Console.WriteLine("Student1's name before method call: " + stu1.Name); UseStudent(stu1); Console.WriteLine("Student1's name after method call: " + stu1.Name); } static void UseStudent(Student stu) { stu.Name = "Oops; someone changed me!"; } } class Student { private int _id; private string _name; public int Id { get{ return _id; } } public string Name { get{ return _name; } set{ _name = value; } } public Student(int id, string name) { _id = id; _name = name; } } After calling UseStudent(), you will find that Main()'s student object has had its name changed from inside the UseStudent(). The method operated on our original object, without using the "ref" keyword at all! C++ programmers tend to be irritated by this behavior. You see, in C++, variables and objects work exactly the same way. In C++, if you pass an object without using the C++ equivalent of "ref", the method will only be operating on a copy of the original object, just like it would with a simple datatype. My best guess as to why C# automatically passes objects by reference is that 90% of the time, you want to pass an object by reference, anyway. Remember that an object isn't some stupid integer or string, which could be changed without restraint by anyone with access to it. Classes can be programmed to have restrictions on how their data is accessed and used, so it's a lot safer to share them between methods. In addition, objects take up a lot more space in your computer's memory than a simple variable does, so you really don't want any more copies of your object floating around than necessary. That being said, what do we do for that 10% of the time when we don't want the method to mess around with the original object? Well, the most common way of doing this is to manually create an identical (but separate) copy of the object, and then pass that in instead of the original. This isn't as painful as it sounds; you simply add one extra method to your Student class and you're all set: class Student { // Add this method public Student Clone() { // Create new student, and copy this student's data over to him Student stu = new Student(); stu._name = this._name; stu._id = this._id; return stu; } } That's right; we've created a method in the Student class with a return type of Student! Hey, we can do that. As you can see, all the Clone() method does is create a new student, and then copy its information over to the new student (I've included the "this" keyword here to clarify which member is which). It then returns the new student, which is an exact but separate replica of its parent. But, wait a second! _name and _id are private members! How are we accessing the new student's private members from outside of the object? Well, remember what the private keyword actually does? It guarantees that only code within the actual class can access the member. And the Clone() method is in the Student class, so we can access the new instance's private members. This might seem a bit strange, but when you really think about it, there's no point in hiding the object's private data from the class which defined it in the first place! The laws of encapsulation are not broken by this trick, because the Student class already knows about its own private members, anyway. Using the Clone() method in Main() couldn't be simpler. We just assign the value of Clone() to a new Student object, like so: Student stu2 = new Student(2, "Fred"); Console.WriteLine(ln + "Student2's name before method call: " + stu2.Name); Student stuCopy = stu2.Clone(); // create a temporary copy UseStudent(stuCopy); // pass the copy into "UseStudent" Console.WriteLine("Student2's name before method call: " + stu2.Name); If we don't need the clone for later use, we can also just drop it directly into the method call, without storing it anywhere at all: UseStudent(stu2.Clone()); Having done all that, you might be wondering why we didn't create a new copy of stu2 simply by doing this: Student stu2 = new Student(2, "Fred"); Student stuCopy = stu2; // NOT an independant copy The reason is that, once again, objects work differently than variables. Objects not only pass by reference, they're even stored by reference! In the above code, you now have two separate Student objects that actually reference the same thing! If you don't believe me, try doing this: Student stu1 = new Student(1, "Joe"); Student stu2 = stu1; Console.WriteLine("Student1's name: " + stu1.Name); Console.WriteLine("Student2's name: " + stu2.Name); Console.WriteLine("Changing Student1's name to \"Charlie\"."); stu1.Name = "Charlie"; Console.WriteLine("Student1's name: " + stu1.Name); Console.WriteLine("Student2's name: " + stu2.Name); You will get the following output: Student1's name: Joe Student2's name: Joe Changing Student1's name to "Charlie". Student1's name: Charlie Student2's name: Charlie So as you can see, once you've assigned one object to another, they truly are just different names for the same thing. What happens to one will happen to the other. Cloning objects is the only way to escape this behavior. Incidentally, normal variables can never behave in the way described above. If you assign an int to another int, you will always be storing a copy of the original value, and not the original variable itself. This holds true, even when using the "ref" keyword. Speaking of memory, we should at least briefly mention memory leaks. In C++, if you forgot to properly delete an object when you were done with it, that memory would be permanently lost to the OS when the object went out of scope. C# and Java are much more robust at "cleaning up" after themselves (at the cost of some performance), but they can still develop memory "leaks" if you're not careful. C# can't "clean up" an object until there are no more references (variables) in your program that point to it. Setting the original object to "null" won't accomplish anything, if the object is still referenced in three other variables in your program. You must remove all of the references to the object before it can be cleaned up. Now, the fact that objects can be "shared" in multiple places is not a bad thing. It can actually be quite useful. For example, this ability makes an excellent substitute for static global variables. By storing the same object in multiple places in your program (even in different classes!), you can make changes to just one object that will be seen in multiple places across your program. This might sound a lot like global variables all over again, but with two important differences: First, it's now a class that you're dealing with, and not an unthinking variable that conforms to whatever value it's given. You can control and limit the way in which this "common" class is used. Secondly, only the places where you choose store a reference to this object will have access to it. With a public static variable, anyone can see and use it, whether you want them to or not. The code sample below summarizes this lesson, and also illustrates how a common object can be shared between two separate classes. By the way, for the general interest of the C++ crowd, C# does actually support the * syntax in front of datatypes. However, I would caution against using these; the very fact that you need special compilation options and keywords with any methods that use them should illustrate that they're not for ordinary use. It's best if you write your C# application to behave like an actual C# application, and not like an application trying to mimic C++. It's worth noting that some programmers, instead of defining a method called "Clone()", will instead define a constructor that accepts an instance of the class. This is called a "copy constructor", and its "guts" work basically the same way as a Clone() method: you're still copying the members from the old instance into the new instance. The only difference is that, instead of calling the Clone() method on your old instance, you're passing your old instance into the constructor of your new instance. using System; namespace D_Ref { class Class1 { static readonly string ln = Environment.NewLine; [STAThread] static void Main(string[] args) { Student stu1 = new Student(1, "Joe"); // Normal behavior: original object is changed Console.WriteLine("Student1's name before method call: " + stu1.Name); UseStudent(stu1); Console.WriteLine("Student1's name after method call: " + stu1.Name); // A way to pass in the original object's data without letting // the method make changes to it Student stu2 = new Student(2, "Fred"); Console.WriteLine(ln + "Student2's name before method call: " + stu2.Name); Student stuCopy = stu2.Clone(); // create a temporary copy UseStudent(stuCopy); // pass the copy into "UseStudent" Console.WriteLine("Student2's name before method call: " + stu2.Name); // Create a course and an id Course course = new Course("Math"); int id = 5; // Send them both to stu1, which stores them stu1.SetCourseAndId(course, ref id); // Print the current values of stu1 Console.WriteLine(ln + "Student1's data before changing in Main(): Id = {0}, CourseName = {1}", stu1.Id, stu1.CourseName); // Now, change the original values of the object and variable in Main() course.Name = "Science"; id = 8; // Re-print the current values of stu1 // The id is different, but the course has changed - the "Course" object // in Main() and the "Course" object in stu1 are the same object! Console.WriteLine("Student1's data after changing in Main(): Id = {0}, CourseName = {1}", stu1.Id, stu1.CourseName); PromptForExit(); } static void UseStudent(Student stu) { stu.Name = "Oops; someone changed me!"; } static void PromptForExit() { Console.Write(ln + "Program complete! Hit enter to exit..."); Console.ReadLine(); } } class Student { private int _id; private string _name; private Course _course; public int Id { get{ return _id; } } public string Name { get{ return _name; } set{ _name = value; } } public string CourseName { get{ return _course.Name; } } public Student() { _id = -1; _name = ""; _course = new Course("No Course"); } public Student(int id, string name) { _id = id; _name = name; _course = new Course("No Course"); } public void SetCourseAndId(Course course, ref int id) { _course = course; // this stores a reference to the actual object! _id = id; // this just stores the value of the variable } // Method that provides an exact (but separate) copy of this student public Student Clone() { // Create new student, and copy this student's data over to him Student stu = new Student(); stu._name = this._name; // We can access the private members of Student because we're still // writing this code in the Student class... crafty, eh? stu._id = this._id; return stu; } } class Course { private string _name; public string Name { get{ return _name; } set{ _name = value; } } public Course(string name) { _name = name; } } } |
|