- What Is an Object, Anyway?
- Instances and Methods
- An Objective-C Class for Working with Fractions
- The @interface Section
- The @implementation Section
- The program Section
- Accessing Instance Variables and Data Encapsulation
- Summary
- Exercises
Accessing Instance Variables and Data Encapsulation
You've seen how the methods that deal with fractions can access the two instance variables numerator and denominator directly by name. In fact, an instance method can always directly access its instance variables. A class method can't, however, because it's dealing only with the class itself, not with any instances of the class (think about that for a second). But what if you wanted to access your instance variables from someplace else—for example, from inside your main routine? You can't do that directly because they are hidden. The fact that they are hidden from you is a key concept called data encapsulation. It enables someone writing class definitions to extend and modify the class definitions, without worrying about whether programmers (that is, users of the class) are tinkering with the internal details of the class. Data encapsulation provides a nice layer of insulation between the programmer and the class developer.
You can access your instance variables in a clean way by writing special methods to set and retrieve their values. We wrote setNumerator: and setDenominator: methods to set the values of the two instance variables in our Fraction class. To retrieve the values of those instance variables, you'll need to write two new methods. For example, you'll create two new methods called, appropriately enough, numerator and denominator to access the corresponding instance variables of the Fraction that is the receiver of the message. The result is the corresponding integer value, which you return. Here are the declarations for your two new methods:
–(int) numerator; –(int) denominator;
And here are the definitions:
–(int) numerator { return numerator; } –(int) denominator { return denominator; }
Note that the names of the methods and the instance variables they access are the same. There's no problem doing this (although it might seem a little odd at first); in fact, it is common practice. Program 3.4 tests your two new methods.
Program 3.4.
// Program to access instance variables – cont'd #import <Foundation/Foundation.h> //---- @interface section ---- @interface Fraction: NSObject { int numerator; int denominator; } -(void) print; -(void) setNumerator: (int) n; -(void) setDenominator: (int) d; -(int) numerator; -(int) denominator; @end //---- @implementation section ---- @implementation Fraction -(void) print { NSLog (@"%i/%i", numerator, denominator); } -(void) setNumerator: (int) n { numerator = n; } -(void) setDenominator: (int) d { denominator = d; } -(int) numerator { return numerator; } -(int) denominator { return denominator; } @end //---- program section ---- int main (int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Fraction *myFraction = [[Fraction alloc] init]; // Set fraction to 1/3 [myFraction setNumerator: 1]; [myFraction setDenominator: 3]; // Display the fraction using our two new methods NSLog (@"The value of myFraction is: %i/%i", [myFraction numerator], [myFraction denominator]); [myFraction release]; [pool drain]; return 0; }
Program 3.4. Output
The value of myFraction is 1/3
This NSLog statement displays the results of sending two messages to myFraction: the first to retrieve the value of its numerator, and the second the value of its denominator:
NSLog (@"The value of myFraction is: %i/%i", [myFraction numerator], [myFraction denominator]);
So, in the first message call, the numerator message will be sent to the Fraction object myFraction. In that method, the code will return the value of the numerator instance variable for that fraction. Remember, the context of a method while it is executing is the object that is the receiver of the message. So when the numerator method accesses and returns the value of the numerator instance variable, it's myFraction's numerator that will be accessed and returned. That returned integer value is then passed along to NSLog to be displayed.
For the second message call, the denominator method will be called to access and return the value of myFraction's denominator, which is then passed to NSLog to be displayed.
Incidentally, methods that set the values of instance variables are often collectively referred to as setters, and methods used to retrieve the values of instance variables are called getters. For the Fraction class, setNumerator: and setDenominator: are the setters, and numerator and denominator are the getters. Collectively, setters and getters are also referred to as accessor methods.
Make sure you understand the difference between setters and the getters. The setters don't return a value because their purpose is to take an argument and to set the corresponding instance variable to the value of that argument. No value needs to be returned in that case. That's the purpose of a setter: to set the value of an instance variable, so setters typically do not return values. On the other hand, the purpose of the getter is to "get" the value of an instance variable stored in an object and to send it back to the program. In order to do that, the getter must return the value of the instance variable using the return statement.
Again, the idea that you can't directly set or get the value of an instance variable from outside of the methods written for the class, but instead have to write setter and getter methods to do so is the principle of data encapsulation. So you have to use methods to access this data that is normally hidden to the "outside world." This provides a centralized path to the instance variables and prevents some other code from indirectly changing these values, which would make your programs harder to follow, debug, and modify.
We should also point out that there's also a method called new that combines the actions of an alloc and init. So this line could be used to allocate and initialize a new Fraction:
Fraction *myFraction = [Fraction new];
It's generally better to use the two-step allocation and initialization approach so you conceptually understand that two distinct events are occurring: You're first creating a new object and then you're initializing it.