Objective-C 编程语言官网文档(九)-静态行为

时间:2021-02-04 19:14:54

声明:本文档仅为个人学习过程中顺手翻译之作,方便开发的同胞借鉴参考。如有觉得译的不好不到位的地方,欢迎指正,将及时做出更正

尽量尊重原文档,因为首次Objective-C,有些地方可能直译了没有注意该语言的专有词,希望指正。如需转载,请注明出处


我的编程环境:

IDE:XCODE4.3.1

OS: MAC OS X 10.7.4

文章来译自:http://developer.apple.com/



静态行为

本节阐述了静态赋予类型是如何工作的,并讨论了Objective-C 其它的一些特性,包括如何临时地停止它固有动态特性。

默认的动态行为

按照设计,Objective-C 对象都是动态实体。关于它们的大多数决定从编译时推向运行时。

  • 对象的内存是在运行时,类或者类方法创建新的实例时动态分配的。

  • 对象是动态赋予类型的。在源码中 (编译时), 任何对象变量都可以是 id 类型的,无对象的类是什么。一个 id 变量(以及它的特定方法和数据结构)的实际所属类直到程序运行时才被确定。

  • 消息和方法是动态绑定的,就像在 “Dynamic Binding.” 中看到的。一个运行时进程会将消息中的方法选择器与“属于接收器”的方法实现进行匹配。

这些特性赋予了面向对象编程极大的可伸缩性以及能量,但也要付出一些代价。特别是编译器无法检查 id 变量的准确类型(类). 要获得更好的编译时类型检查,并让代码有更好的可读性,Objective-C 允许我们使用一个具体的类名来为变量静态赋予类型而不是泛泛的使用一个 id 类型。Objective-C 还允许我们关闭它的一些面向对象特性,以便将一些操作从运行时转到编译时来做。

注意: 消息的方式比方法调用在某种程度上慢一些,但是这些性能消耗同实际执行的工作比起来微不足道。通过取消objc的灵活性而带来的性能变化只有通过分析工具诸如Shark 或 Instruments进行分析时才能感觉的到。

静态赋予类型

如果在一个对象声明的 id 处使用了一个指向一个类名的指针,例如

Rectangle *thisObject;

编译器会将声明的变量的值限制为声明中指定的类的一个实例或者从该类继承的子类的一个实例。在上面的例子中, thisObject 只能是一个 Rectangle 类或其子类的对象。

静态赋予类型的对象跟声明为 id类型的对象拥有相同的内部数据结构。类型并不会影响对象;它只会影响提供给编译器的有关该对象的信息量以及源码中有关对象的可读信息量。

静态赋予类型也不会影响对象在运行时的处理。为静态赋予类型的对象分配的类跟 id 类型创建实例的类是一样的。如果 Square 是 Rectangle的子类,那么下面的代码也会创建一个拥有 Square 对象所有实例变量的对象,而不仅仅是 Rectangle 对象的实例变量:

Rectangle *thisObject = [[Square alloc] init];

发送到静态类型对象的消息也是动态绑定的,同发送给 id 类型对象是相同的。静态指定类型的接受者在运行时的消息处理中任然会进行精确类型的检测。 向 thisObject 对象发送 display 消息:

[thisObject display];

如上执行的是 Square 类中的display方法,而不是他的父类 Rectangle 中的方法。

作为提供给编译器额外的对象信息,静态类型比id类型提供了更多的可能:

  • 在某些情况下,它可以进行编译时的类型校验。

  • 它可以使对象摆脱相同名称的方法必须有相同参数类型和返回值的限制。
  • 它允许你使用结构指针运算符(.运算符)直接访问一个对象的实例变量。

前两条会在下面章节讨论,第三条参见 “Defining a Class.”

类型检查

当使用静态类型时编译器可以在以下两种情况下提供更好的类型校验:

  • 当向一个静态类型的接收者发送一个消息时,编译器可以确定接收者是否可以响应。如果接收者没有与消息中方法名相对应的方法编译器会发出一个警告。

  • 当一个静态类型的对象被分配到一个静态类型的变量,编译器会判断他们的类型是否匹配。如果不匹配会发出一个警告。

若要赋值操作时避免警告,需要对象的类型同变量的类型相同或者为变量类型的子类。就像下面的例子所展示的:

Shape     *aShape;
Rectangle *aRect;
 
aRect = [[Rectangle alloc] init];
aShape = aRect;

这里 aRect 可以被分配给 aShape ,因为矩形是图形的一种—— Rectangle 类继承自 Shape。但是,如果两者的角色调换一下也就是说将 aShape 分配给 aRect ,编译器会生成一个警告,因为不是所有的图形都是矩形。 (可以参见 1-2, 该处展示了类的继承关系,其中包括 Shape 和 Rectangle.)

当表达式中等号两边的任何一个对象的类型为 id 时都不会进行类型校验。静态类型的对象可以任意的分配给 id 类型的对象,或者 id 类型的对象也可以分配给任何一个静态类型对象。由于像 alloc 和 init 这样的方法会返回 id 类型的对象,所以编译器并不能确保为静态类型变量返回一个类型合适的对象。像下面这样为变量赋值虽然很容易出错误,但是仍然是被允许的:

Rectangle *aRect;
aRect = [[Shape alloc] init];

返回类型和参数类型

通常情况下,不同类中的同名方法其参数和返回值类型也必须相同。这一限制由编译器强制执行以实现动态绑定。因为在运行时编译器无法知道一个消息接收者的类型(实现被调用方法的类),编译器必须对相同名字的方法相似对待。当编译器为运行时系统准备方法的信息时,它仅创建一个方法描述来应对每一个方法选择器。

但是,当向静态类型对象发送一个消息时,编译器知道接收者的类型。编译器可以取得关于这个特定类中方法的定义信息,因此消息的返回值和参数类型就不再受上面的约束。

为一个继承类赋予静态类型

一个类的实例可以被静态的制定为它本身类的类型也可以被指定为它所继承的类的类型。例如,所有的实例都可以被静态的指定为 NSObject 类型。

但是,编译器只是通过类型定义中的类名来确定类的静态类型并进行类型校验。因此为一个实例指定一个它所继承类的类型会使编译器所预期的处理同运行时实际的处理出现偏差。

例如,如果你把一个 Rectangle 实例指定为 Shape 类型:

Shape *myRectangle = [[Rectangle alloc] init];

编译器会认为它就是一个 Shape 实例。如果你向这个对象发送一个消息让其运行一个 Rectangle 中的方法:

BOOL solid = [myRectangle isFilled];

编译器会表示很愤怒Objective-C 编程语言官网文档(九)-静态行为。因为 isFilled 方法是在 Rectangle 类中定义的而不是在 Shape 中。

但是,如果发送一个消息,来运行一个 Shape 中的方法:

[myRectangle display];

编译器表示很淡定Objective-C 编程语言官网文档(九)-静态行为,即使 Rectangle 已经覆写了这个方法。但是在运行时,实际运行的方法是 Rectangle 版本中定义的方法。

类似,假如有一个 Upper 类声明了一个 worry 方法,它有一个 double 类型返回值:

- (double)worry;

同时Upper 的子类 Middle 复写了这个方法并返回了一个新类型的返回值:

- (int)worry;

如果一个实例被静态的指定为 Upper 类型,编译器会认为他的 worry 方法返回一个 double 类型返回值,同时如果一个实例被指定为 Middle 类型,编译器会认为 worry 应该返回一个int类型。如果一个Middle实例被指定为 Upper 类型编译器会通知运行时系统对象的 worry 方法返回一个 double 类型,但是在运行时实际的返回值为 int 类型同时会生成一个错误。

静态赋予类型可以使同名方法无须受到必须具有相同参数类型及返回值类型的约束,但是只有应用在不同继承关系中定义的方法,这样做才可靠。


英文原文:点击打开链接


Enabling Static Behavior

This chapter explains how static typing works and discusses some other features of Objective-C, including ways to temporarily overcome its inherent dynamism.

Default Dynamic Behavior

By design, Objective-C objects are dynamic entities. As many decisions about them as possible are pushed from compile time to runtime:

  • The memory for objects is dynamically allocated at runtime by class methods that create new instances.

  • Objects are dynamically typed. In source code (at compile time), any object variable can be of type id no matter what the object’s class is. The exact class of an id variable (and therefore its particular methods and data structure) isn’t determined until the program runs.

  • Messages and methods are dynamically bound, as described in “Dynamic Binding.” A runtime procedure matches the method selector in the message to a method implementation that “belongs to” the receiver.

These features give object-oriented programs a great deal of flexibility and power, but there’s a price to pay. In particular, the compiler can’t check the exact types (classes) of id variables. To permit better compile-time type checking, and to make code more self-documenting, Objective-C allows objects to be statically typed with a class name rather than generically typed as id. Objective-C also lets you turn off some of its object-oriented features in order to shift operations from runtime back to compile time.

Note: Messages are somewhat slower than function calls, typically incurring an insignificant amount of overhead compared to actual work performed. The exceptionally rare case where bypassing the dynamism of Objective-C might be warranted can be proven by use of analysis tools like Shark or Instruments.

Static Typing

If a pointer to a class name is used in place of id in an object declaration such as

Rectangle *thisObject;

the compiler restricts the value of the declared variable to be either an instance of the class named in the declaration or an instance of a class that inherits from the named class. In the example above, thisObject can be only a Rectangle object of some kind.

Statically typed objects have the same internal data structures as objects declared to be of type id. The type doesn’t affect the object; it affects only the amount of information given to the compiler about the object and the amount of information available to those reading the source code.

Static typing also doesn’t affect how the object is treated at runtime. Statically typed objects are dynamically allocated by the same class methods that create instances of type id. If Square is a subclass of Rectangle, the following code would still produce an object with all the instance variables of a Square object, not just those of aRectangle object:

Rectangle *thisObject = [[Square alloc] init];

Messages sent to statically typed objects are dynamically bound, just as messages to objects typed id are. The exact type of a statically typed receiver is still determined at runtime as part of the messaging process. A displaymessage sent to the thisObject object:

[thisObject display];

performs the version of the method defined in the Square class, not the one in its Rectangle superclass.

By giving the compiler more information about an object, static typing opens up possibilities that are absent for objects typed id:

  • In certain situations, it allows for compile-time type checking.

  • It can free objects from the restriction that identically named methods must have identical return and parameter types.

  • It permits you to use the structure pointer operator to directly access an object’s instance variables.

The first two possibilities are discussed in the sections that follow. The third is covered in “Defining a Class.”

Type Checking

With the additional information provided by static typing, the compiler can deliver better type-checking services in two situations:

  • When a message is sent to a statically typed receiver, the compiler can make sure the receiver can respond. A warning is issued if the receiver doesn’t have access to the method named in the message.

  • When a statically typed object is assigned to a statically typed variable, the compiler makes sure the types are compatible. A warning is issued if they’re not.

An assignment can be made without warning, provided the class of the object being assigned is identical to, or inherits from, the class of the variable receiving the assignment. The following example illustrates this:

Shape     *aShape;
Rectangle *aRect;
 
aRect = [[Rectangle alloc] init];
aShape = aRect;

Here aRect can be assigned to aShape because a rectangle is a kind of shape—the Rectangle class inherits fromShape. However, if the roles of the two variables are reversed and aShape is assigned to aRect, the compiler generates a warning; not every shape is a rectangle. (For reference, see Figure 1-2, which shows the class hierarchy including Shape and Rectangle.)

There’s no check when the expression on either side of the assignment operator is of type id. A statically typed object can be freely assigned to an id object, or an id object to a statically typed object. Because methods likealloc and init return objects of type id, the compiler doesn’t ensure that a compatible object is returned to a statically typed variable. The following code is error-prone, but is allowed nonetheless:

Rectangle *aRect;
aRect = [[Shape alloc] init];

Return and Parameter Types

In general, methods in different classes that have the same selector (the same name) must also share the same return and parameter types. This constraint is imposed by the compiler to allow dynamic binding. Because the class of a message receiver (and therefore class-specific details about the method it’s asked to perform), can’t be known at compile time, the compiler must treat all methods with the same name alike. When it prepares information on method return and parameter types for the runtime system, it creates just one method description for each method selector.

However, when a message is sent to a statically typed object, the class of the receiver is known by the compiler. The compiler has access to class-specific information about the methods. Therefore, the message is freed from the restrictions on its return and parameter types.

Static Typing to an Inherited Class

An instance can be statically typed to its own class or to any class that it inherits from. All instances, for example, can be statically typed as NSObject.

However, the compiler understands the class of a statically typed object only from the class name in the type designation, and it does its type checking accordingly. Typing an instance to an inherited class can therefore result in discrepancies between what the compiler thinks would happen at runtime and what actually happens.

For example, if you statically type a Rectangle instance as Shape as shown here:

Shape *myRectangle = [[Rectangle alloc] init];

the compiler treats it as a Shape instance. If you send the object a message to perform a Rectangle method,

BOOL solid = [myRectangle isFilled];

the compiler complains. The isFilled method is defined in the Rectangle class, not in Shape.

However, if you send it a message to perform a method that the Shape class knows about such as

[myRectangle display];

the compiler doesn’t complain, even though Rectangle overrides the method. At runtime, the Rectangle version of the method is performed.

Similarly, suppose that the Upper class declares a worry method that returns a double as shown here:

- (double)worry;

and the Middle subclass of Upper overrides the method and declares a new return type:

- (int)worry;

If an instance is statically typed to the Upper class, the compiler thinks that its worry method returns a double, and if an instance is typed to the Middle class, the compiler thinks that worry returns an int. Errors result if aMiddle instance is typed to the Upper class: The compiler informs the runtime system that a worry message sent to the object returns a double, but at runtime it actually returns an int and generates an error.

Static typing can free identically named methods from the restriction that they must have identical return and parameter types, but it can do so reliably only if the methods are declared in different branches of the class hierarchy.