Effective Objective-C 2.0: Item 6: Understand Properties

前面的runtime没看懂- -; 后面的atomicity 和 thread-safe 说明, 很好很强大 ^ ^


Item 6: Understand Properties

Properties are an Objective-C feature providing encapsulation of the data an object contains. Objects in Objective-C will usually contain a set of instance variables to store the data they need to work. Instance variables are usually accessed through accessor methods. A getter is used to read the variable, and a setter is used to write the variable. This concept was standardized and became part of the Objective-C 2.0 release through a feature called properties, which allow the developer to tell the compiler to write accessor methods automatically. This feature introduced a new “dot syntax” to make accessing the data stored by classes less verbose. You have probably used properties already, but you may not know about all the options. Also, you may not be aware of some of the intricacies surrounding properties. Item 6 illustrates the background surrounding the problem solved by properties and points out their key features.

A class to describe a person might store the person’s name, date of birth, address, and so on. You can declare instance variables in the public interface for a class as follows:

@interface EOCPerson : NSObject {
@public
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_someInternalData;
}
@end

This will be familiar if you are coming from the worlds of Java or C++, where you can define the scope of instance variables. However, this technique is rarely used in modern Objective-C. The problem with the approach is that the layout of an object is defined at compile time. Whenever the _firstName variable is accessed, the compiler hardcodes the offset into the memory region where the object is stored. This works fine until you add another instance variable. For example, suppose that another instance variable were added above _firstName:

@interface EOCPerson : NSObject {
@public
    NSDate *_dateOfBirth;
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_someInternalData;
}
@end

The offset that was once pointing to _firstName is now pointing to _dateOfBirth. Any code that had the offset hardcoded would end up reading the wrong value. To illustrate that point, Figure 2.1 shows the memory layout of the class, assuming 4-byte pointers, before and after adding the _dateOfBirth instance variable.

Image

Figure 2.1 Class data layout before and after adding another instance variable

Code that makes use of calculating the offset at compile time will break unless recompiled when the class definition changes. For example, code may exist in a library that uses an old class definition. If linked with code using the new class definition, there will be an incompatibility at runtime. To overcome this problem, languages have invented a variety of techniques. The approach Objective-C has taken is to make instance variables special variables held by class objects (see Item 14 for more on class objects) storing the offset. Then at runtime, the offset is looked up so that if the class definition changes, the offset stored is updated; whenever an access to the instance variable is made, the correct offset is used. You can even add instance variables to classes at runtime. This is known as the nonfragile Application Binary Interface (ABI). An ABI defines, among other things, the conventions for how code should be generated. The nonfragile ABI also means that instance variables can be defined in a class-continuation category (see Item 27) or in the implementation. So you don’t have to have all your instance variables declared in the interface anymore, and you therefore don’t leak internal information about your implementation in the public interface.

Encouraging the use of accessor methods rather than accessing instance variables directly is another factor overcoming this problem. Properties are backed by instance variables, but they provide a neat abstraction. You could write accessors yourself, but in true Objective-C style, accessors follow strict naming patterns. Because of this strict naming, it was possible to introduce a language construct to provide a means of automatically creating accessor methods. This is where the @property syntax comes in.

You use properties in the definition of an object interface to provide a standard means of accessing the data encapsulated by an object. As such, properties can also be thought of as shorthand for saying that there are going to be accessors to a variable of a given type and a given name. For example, consider the following class:

@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

To a consumer of the class, this is equivalent to writing the class like this:

@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end

To use a property, you use the dot syntax, similar to how you would access a member of a stack-allocated struct in plain C. The compiler turns the dot syntax into calls to the accessors, just as if you had invoked them directly. Thus, there is absolutely no difference between using the dot syntax and calling the accessors directly. The following code sample shows the equivalence:

EOCPerson *aPerson = [Person new];

aPerson.firstName = @"Bob"// Same as:
[aPerson setFirstName:@"Bob"];

NSString *lastName = aPerson.lastName// Same as:
NSString *lastName = [aPerson lastName];

But there’s more to properties than that. If you let it, the compiler will automatically write the code for you for these methods through a process called autosynthesis. It is important to note that it’s the compiler doing this, at compile time, so you won’t see source code in your editor for the synthesized methods. Along with generating the code, the compiler also automatically adds an instance variable to the class, of the required type with the name of the property prefixed with an underscore. In the preceding example, there would be two instance variables: _firstName and_lastName. It is possible to control the name of this instance variable by using the@synthesize syntax within the class’s implementation, like so:

@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

Using the preceding syntax would yield instance variables called _myFirstName and_myLastName instead of the defaults. It is unusual to want to change the name of the instance variable from the default; however, if you are not a fan of using the underscore approach to naming instance variables, you can use this to name them however you want. But I encourage you to use the default naming scheme, as it makes code readable by everyone if everyone sticks to the same conventions.

If you don’t want the compiler to synthesize the accessor methods for you, you can implement the methods yourself. However, if you implement only one of the accessors, the compiler will still synthesize the other for you. Another way to stop it from synthesizing is to use the @dynamic keywordwhich tells the compiler to not automatically create an instance variable to back the property and to not create the accessors for you. Also, when compiling code that accesses the property, the compiler will ignore the fact that the accessors have not been defined and trust you that they will be available at runtime. For example, this is used when subclassing CoreData’s NSManagedObject, where the accessors are dynamically created at runtime. NSManagedObject opts for this approach because the properties are not instance variables. The data comes from whatever database back end is being used. For example:

@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation EOCPerson
@dynamic firstName, lastName;
@end

In this class, no accessors or instance variables would be synthesized. Nor would the compiler warn if you attempted to access either property.

Property Attributes

Another aspect of properties that you should be aware of is all the attributes that can be used to control the accessors generated by the compiler. An example using three attributes looks like this:

@property (nonatomic, readwrite, copy) NSString *firstName;

Four categories of attributes can be applied.

Atomicity

By default, synthesized accessors include locking to make them atomic. If you supply the attribute nonatomic, no locking is used. Note that although there is no atomic attribute (atomic is assumed by the lack of the nonatomic attribute), it can still be applied with no compiler errors in case you want to be explicit. If you define the accessors yourself, you should provide the specified atomicity.

Read/Write

Image readwrite Both a getter and a setter are available. If the property is synthesized, the compiler will generate both methods.

Image readonly Only a getter is available, and the compiler will generate it only if the property is synthesized. You may want to use this if you expose a property only for read externally but redeclare it as read/write internally in the class-continuation category. See Item 27 for more information.

Memory-Management Semantics

Properties encapsulate data, and that data needs to have concrete ownership semantics. This affects only the setter. For example, should the setter retain the new value or simply assign it to the underlying instance variable? When the compiler synthesizes the accessors, it uses these attributes to determine what code to write for you. If you create your own accessors, you must stick to what you specify for this attribute.

Image assign The setter is a simple assign operation used for scalar types, such asCGFloat or NSInteger.

Image strong This designates that the property defines an owning relationship. When a new value is set, it is first retained, the old value is released, and then the value is set.

Image weak This designates that the property defines a nonowning relationship. When a new value is set, it is not retained; nor is the old value released. This is similar to what assign does, but the value is also nilled out when the object pointed to by the property at any time is destroyed.

Image unsafe_unretained This has the same semantics as assign but is used where the type is an object type to indicate a nonowning relationship (unretained) that is not nilled out (unsafe) when the target is destroyed, unlike weak.

Image copy This designates an owning relationship similar to strong; however, instead of retaining the value, it is copied. This is often used when the type is NSString*to preserve encapsulation, since the value passed into the setter might be an instance of the subclass NSMutableString. If it’s this mutable variant, the value could be mutated after the property is set, without the object’s knowing. So an immutable copy is taken to ensure that the string cannot change from underneath the object. Any object that may be mutable should take a copy.

Method Names

The names of the accessor methods can be controlled by using the following attributes:

Image getter=<name> Specifies the name of the getter. This method is usually used for Boolean properties where you want the getter to be prefixed with is. For example, on the UISwitch class, the property for whether the switch is on or off is defined like so:

@property (nonatomic, getter=isOn) BOOL on;

Image setter=<name> Specifies the name of the setter. This method is not commonly used.

You can use these attributes to get fine-grained control over the synthesized accessors. However, you should note that if you implement your own accessors, you should make them adhere to the specified attributes yourself. For example, a property declared as copy should ensure that a copy of the object is taken in the setter. Otherwise, users of the property would be under false impressions, and bugs could be introduced from the contract’s not being upheld.

Ensuring that you adhere to the semantics laid out in the property definition is important even in other methods that may set the property. For example, consider an extension of the EOCPerson class. It declares the properties’ memory-management semantics as copy because the value might be mutable. It also adds an initializer that sets up the initial values of the first and last names:

@interface EOCPerson : NSManagedObject

@property (copy) NSString *firstName;
@property (copy) NSString *lastName;

- (id)initWithFirstName:(NSString*)firstName
               lastName:(NSString*)lastName;

@end

In the implementation of the custom initializer, it is important to adhere to the copy semantics laid out in the property definitions. The reason is that the property definitions document the contract that the class has with the values that are set. So an implementation of the initializer would look like this:

- (id)initWithFirstName:(NSString*)firstName
               lastName:(NSString*)lastName
{
    if ((self = [super init])) {
        _firstName = [firstName copy];
        _lastName = [lastName copy];
    }
    return self;
}

You may wonder why you can’t simply use the properties’ setter methods instead, which will always ensure that the correct semantics are used. You should never use your own accessors in an init (or dealloc) method, as explained further in Item 7.

If you’ve read Item 18, you’ll know that, if possible, it’s best to make an object immutable. Applied to EOCPerson, you’d make both properties read-only. The initializer will set the values, after which they cannot be changed. In this scenario, it is important to still declare what memory-management semantics you use for the values. So the property definitions would end up looking like this:

@property (copy, readonly) NSString *firstName;
@property (copy, readonly) NSString *lastName;

Even though no setters are created, because the properties are read-only, it is still important to document what the semantics are when the initializer is run to set the values. Without this documented, a consumer of the class could not assume that it takes a copy and therefore might make an extra copy before calling the initializer. Doing so would be both redundant and less efficient.

You may wonder how atomic and nonatomic differ. As described earlier, atomicaccessors include locks to ensure atomicity. This means that if two threads are reading and writing the same property, the value of the property at any given point in time is valid. Without the locks, or nonatomic, the property value may be read on one thread while another thread is midway through writing to it. If this happens, the value that’s read could be invalid.

If you’ve been developing for iOS at all, you’ll notice that all properties are declarednonatomic. The reason is that, historically, the locking introduces such an overhead on iOS that it becomes a performance problem. Usually, atomicity is not required anyway, since it does not ensure thread safety, which usually requires a deeper level of locking. For example, even with atomicity, a single thread might read a property multiple times immediately after one another and obtain different values if another thread is writing to it at the same time. Therefore, you will usually want to usenonatomic properties on iOS. But on Mac OS X, you don’t usually find that atomicproperty access is a performance bottleneck.

Things to Remember

Image The @property syntax provides a way of defining what data an object encapsulates.

Image Use attributes to provide the right semantics for the data being stored.

Image Ensure that anywhere a property’s backing instance variable is set, the declared semantics are adhered to.

Image Use nonatomic on iOS, since performance is severely impacted if atomic is used.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值