包和命名空间

包和命名空间是两个相关的概念。使用包,可以通过有利于共享代码并尽可能减少命名冲突的方式将多个类定义捆绑在一起。使用命名空间,可以控制标识符(如属性名和方法名)的可见性。无论命名空间位于包的内部还是外部,都可以应用于代码。包可用于组织类文件,命名空间可用于管理各个属性和方法的可见性。

在 ActionScript 3.0 中,包是用命名空间实现的,但包和命名空间并不同义。在声明包时,可以隐式创建一个特殊类型的命名空间并保证它在编译时是已知的。显式创建的命名空间在编译时不必是已知的。

下面的示例使用 package 指令来创建一个包含一个类的简单包:

package samples 
{ 
    public class SampleCode 
    { 
        public var sampleGreeting:String; 
        public function sampleFunction() 
        { 
            trace(sampleGreeting + " from sampleFunction()"); 
        } 
    } 
}

在本例中,该类的名称是 SampleCode。由于该类位于 samples 包中,因此编译器在编译时会自动将其类名称限定为完全限定名称:samples.SampleCode。编译器还限定任何属性或方法的名称,因此 sampleGreeting sampleFunction() 分别变成 samples.SampleCode.sampleGreeting samples.SampleCode.sampleFunction()

许多开发人员(尤其是那些具有 Java 编程背景的人)可能会选择只将类放在包的顶级。但是,ActionScript 3.0 不但支持将类放在包的顶级,而且还支持将变量、函数甚至语句放在包的顶级。此功能的一个高级用法是,在包的顶级定义一个命名空间,使之对于该包中的所有类均可用。但是,请注意,在包的顶级只允许使用两个访问说明符: public internal 。Java 允许将嵌套类声明为私有,而 ActionScript 3.0 则不同,它既不支持嵌套类也不支持私有类。

但是,在其他许多方面,ActionScript 3.0 中的包与 Java 编程语言中的包非常相似。从上一个示例可看出,完全限定的包引用使用点运算符 ( . ) 来表示,这与 Java 相同。可以用包将代码组织成直观的分层结构,以供其他程序员使用。这样,您就可以将自己所创建的包与他人共享,还可以在自己的代码中使用他人创建的包,从而推动了代码共享。

使用包还有助于确保所使用的标识符名称是唯一的,而且不与其他标识符名称冲突。事实上,有些人认为这才是包的主要优点。例如,假设两个希望相互共享代码的编程人员各创建了一个名为 SampleCode 的类。如果没有包,这样就会造成名称冲突,唯一的解决方法就是重命名其中的一个类。但是,使用包,就可以将其中的一个(最好是两个)类放在具有唯一名称的包中,从而轻松地避免了名称冲突。

您还可以在包名称中嵌入点来创建嵌套包,这样就可以创建包的分层结构。ActionScript 3.0 提供的 flash.display 包就是一个很好的示例。flash.display 包嵌套在 flash 包中。

大部分 ActionScript 3.0 都划分到 flash 包中。例如,flash.display 包中包含显示列表 API,flash.events 包中包含新的事件模型。

创建包

ActionScript 3.0 在包、类和源文件的组织方式上具有很大的灵活性。早期的 ActionScript 版本只允许每个源文件有一个类,而且要求源文件的名称与类名称匹配。ActionScript 3.0 允许在一个源文件中包括多个类,但是,每个文件中只有一个类可供该文件外部的代码使用。换言之,每个文件中只有一个类可以在包声明中进行声明。您必须在包定义的外部声明其他任何类,以使这些类对于该源文件外部的代码不可见。在包定义内部声明的类的名称必须与源文件的名称匹配。

ActionScript 3.0 在包的声明方式上也具有更大的灵活性。在早期的 ActionScript 版本中,包只是表示用于放置源文件的目录,您不必用 package 语句来声明包,而是在类声明中将包名称包括在完全限定的类名称中。在 ActionScript 3.0 中,尽管包仍表示目录,但是它现在不只包含类。在 ActionScript 3.0 中,使用 package 语句来声明包,这意味着您也可以在包的顶级声明变量、函数和命名空间,甚至还可以在包的顶级包括可执行语句。如果在包的顶级声明变量、函数或命名空间,则在顶级只能使用 public internal 属性,并且每个文件中只能有一个包级声明使用 public 属性(无论该声明是类声明、变量声明、函数声明还是命名空间声明)。

包的作用是组织代码并防止名称冲突。您不应将包的概念与类继承这一不相关的概念混淆。位于同一个包中的两个类具有共同的命名空间,但在其他任何方面,它们不必相关。同样,在语义方面,嵌套包可以与其父包无关。

导入包

如果您希望使用位于某个包内部的特定类,则必须导入该包或该类。这与 ActionScript 2.0 不同,在 ActionScript 2.0 中,类的导入是可选的。

例如,想想以前列举过的 SampleCode 类示例。如果该类位于名为 samples 的包中,那么,在使用 SampleCode 类之前,您必须使用下列导入语句之一:

import samples.*;

import samples.SampleCode;

通常, import 语句越具体越好。如果您只打算使用 samples 包中的 SampleCode 类,则应只导入 SampleCode 类,而不应导入该类所属的整个包。导入整个包可能会导致意外的名称冲突。

还必须将定义包或类的源代码放在 类路径 内部。类路径是用户定义的本地目录路径列表,它决定编译器在何处搜索导入的包和类。类路径有时称为 生成路径 源路径

在正确地导入类或包之后,可以使用类的完全限定名称 (samples.SampleCode),也可以只使用类名称本身 (SampleCode)。

当同名的类、方法或属性会导致代码不明确时,完全限定的名称非常有用,但是,如果将它用于所有的标识符,则会使代码变得难以管理。例如,在实例化 SampleCode 类的实例时,使用完全限定的名称会导致代码冗长:

var mySample:samples.SampleCode = new samples.SampleCode();

包的嵌套级别越高,代码的可读性越差。如果您确信不明确的标识符不会导致问题,就可以通过使用简单的标识符来提高代码的可读性。例如,如果在实例化 SampleCode 类的新实例时仅使用类标识符,代码就会比较简洁:

var mySample:SampleCode = new SampleCode();

如果您尝试使用标识符名称,而不先导入相应的包或类,则编译器无法找到类定义。另一方面,即便您导入了包或类,只要尝试定义的名称与所导入的名称冲突,也会产生错误。

创建包时,该包的所有成员的默认访问说明符是 internal ,这意味着,默认情况下,包成员仅对其所在包的其他成员可见。如果您希望某个类对包外部的代码可用,则必须将该类声明为 public 。例如,下面的包包含 SampleCode 和 CodeFormatter 两个类:

// SampleCode.as file 
package samples 
{ 
    public class SampleCode {} 
} 
 
// CodeFormatter.as file 
package samples 
{ 
    class CodeFormatter {} 
}

SampleCode 类在包的外部可见,因为该类声明为 public 类。但是,CodeFormatter 类仅在 samples 包的内部可见。如果您尝试访问位于 samples 包外部的 CodeFormatter 类,将产生错误,如下面的示例所示:

import samples.SampleCode; 
import samples.CodeFormatter; 
var mySample:SampleCode = new SampleCode(); // okay, public class 
var myFormatter:CodeFormatter = new CodeFormatter(); // error

如果您希望这两个类在包外部都可用,就必须将它们都声明为 public 。不能将 public 属性应用于包声明。

完全限定的名称可用来解决在使用包时可能发生的名称冲突。如果您导入两个包,但它们用同一个标识符来定义类,就可能会发生名称冲突。例如,请考虑下面的包,该包也有一个名为 SampleCode 的类:

package langref.samples 
{ 
    public class SampleCode {} 
}

如果按如下方式导入两个类,则使用 SampleCode 类时会发生名称冲突:

import samples.SampleCode; 
import langref.samples.SampleCode; 
var mySample:SampleCode = new SampleCode(); // name conflict

编译器无法确定要使用哪个 SampleCode 类。要解决此冲突,必须使用每个类的完全限定名称,如下所示:

var sample1:samples.SampleCode = new samples.SampleCode(); 
var sample2:langref.samples.SampleCode = new langref.samples.SampleCode();
注: 具有 C++ 背景的程序员经常会将 import 语句与 #include 混淆。 #include 指令在 C++ 中是必需的,因为 C++ 编译器一次处理一个文件,而且除非显式包括了头文件,否则不会在其他文件中查找类定义。ActionScript 3.0 有一个 include 指令,但是它的作用不是为了导入类和包。要在 ActionScript 3.0 中导入类或包,必须使用 import 语句,并将包含该包的源文件放在类路径中。

命名空间

通过命名空间可以控制所创建的属性和方法的可见性。请将 public private protected internal 访问控制说明符视为内置的命名空间。如果这些预定义的访问控制说明符无法满足您的要求,您可以创建自己的命名空间。

如果您熟悉 XML 命名空间,那么,您对这里讨论的大部分内容不会感到陌生,但是 ActionScript 实现的语法和细节与 XML 的稍有不同。即使您以前从未使用过命名空间,也没有关系,因为命名空间概念本身很简单,但是其实现涉及一些您需要了解的特定术语。

要了解命名空间的工作方式,有必要先了解属性或方法的名称总是包含两部分:标识符和命名空间。标识符通常被视为名称。例如,下面的类定义中的标识符是 sampleGreeting sampleFunction()

class SampleCode 
{ 
    var sampleGreeting:String; 
    function sampleFunction () { 
        trace(sampleGreeting + " from sampleFunction()"); 
    } 
}

只要定义不以命名空间属性开头,其名称就会用默认 internal 命名空间限定,这意味着,这些定义仅对同一个包中的调用方可见。如果编译器设置为严格模式,则编译器会发出一个警告,指明 internal 命名空间将应用于没有命名空间属性的任何标识符。为了确保标识符可在任何位置使用,您必须在标识符名称的前面明确加上 public 属性。在上面的示例代码中, sampleGreeting sampleFunction() 都有一个命名空间值 internal

使用命名空间时,应遵循以下三个基本步骤。第一步,必须使用 namespace 关键字来定义命名空间。例如,下面的代码定义 version1 命名空间:

namespace version1;

第二步,在属性或方法声明中,使用命名空间(而非访问控制说明符)来应用命名空间。下面的示例将一个名为 myFunction() 的函数放在 version1 命名空间中:

version1 function myFunction() {}

第三步,在应用了该命名空间后,可以使用 use 指令进行引用,也可以使用命名空间来限定标识符的名称。下面的示例通过 use 指令来引用 myFunction() 函数:

use namespace version1; 
myFunction();

还可以使用限定名称引用 myFunction() 函数,如下面的示例所示:

version1::myFunction();

定义命名空间

命名空间中包含一个名为统一资源标识符 (URI) 的值,该值有时称为 命名空间名称 。使用 URI 可确保命名空间定义的唯一性。

可通过使用以下两种方法之一来声明命名空间定义,以创建命名空间:像定义 XML 命名空间那样使用显式 URI 定义命名空间;省略 URI。下面的示例说明如何使用 URI 来定义命名空间:

namespace flash_proxy = "http://www.adobe.com/flash/proxy";

URI 用作该命名空间的唯一标识字符串。如果您省略 URI(如下面的示例所示),编译器将创建一个唯一的内部标识字符串来代替 URI。您对于这个内部标识字符串不具有访问权限。

namespace flash_proxy;

在定义了命名空间(具有 URI 或没有 URI)后,就不能在同一个作用域内重新定义该命名空间。如果尝试定义的命名空间以前在同一个作用域内定义过,则将生成编译器错误。

如果在某个包或类中定义了一个命名空间,则该命名空间可能对于此包或类外部的代码不可见,除非使用了相应的访问控制说明符。例如,下面的代码显示了在 flash.utils 包中定义的 flash_proxy 命名空间。下面的示例中没有使用访问控制说明符,这意味着 flash_proxy 命名空间仅对于 flash.utils 包内部的代码可见,而对于该包外部的任何代码都不可见:

package flash.utils 
{ 
    namespace flash_proxy; 
}

下面的代码使用 public 属性使 flash_proxy 命名空间对该包外部的代码可见:

package flash.utils 
{ 
    public namespace flash_proxy; 
}

应用命名空间

应用命名空间意味着在命名空间中放置定义。可以放在命名空间中的定义包括函数、变量和常量(不能将类放在自定义命名空间中)。

例如,请考虑一个使用 public 访问控制命名空间声明的函数。在函数的定义中使用 public 属性会将该函数放在 public 命名空间中,从而使该函数对于所有的代码都可用。在定义命名空间之后,可以按照与使用 public 属性相同的方式来使用所定义的命名空间,该定义对于可以引用您的自定义命名空间的代码可用。例如,如果您定义一个名为 example1 的命名空间,则可以添加一个名为 myFunction() 的方法并将 example1 用作属性,如下面的示例所示:

namespace example1; 
class someClass 
{ 
    example1 myFunction() {} 
}

如果在声明 myFunction() 方法时将 example1 命名空间用作属性,则意味着该方法属于 example1 命名空间。

在应用命名空间时,应切记以下几点:

  • 对于每个声明只能应用一个命名空间。

  • 不能一次将同一个命名空间属性应用于多个定义。换言之,如果您希望将自己的命名空间应用于 10 个不同的函数,则必须将该命名空间作为属性分别添加到这 10 个函数的定义中。

  • 如果您应用了命名空间,则不能同时指定访问控制说明符,因为命名空间和访问控制说明符是互斥的。换言之,如果应用了命名空间,就不能将函数或属性声明为 public private protected internal

引用命名空间

在使用借助于任何访问控制命名空间(如 public private protected internal )声明的方法或属性时,无需显式引用命名空间。这是因为对于这些特殊命名空间的访问由上下文控制。例如,放在 private 命名空间中的定义会自动对于同一个类中的代码可用。但是,对于您所定义的命名空间,并不存在这样的上下文相关性。要使用已经添加到某个自定义命名空间中的方法或属性,必须引用该命名空间。

可以用 use namespace 指令来引用命名空间,也可以通过名称限定符 ( :: ) 来使用命名空间限定名称。用 use namespace 指令引用命名空间会“打开”该命名空间,以便将该命名空间应用于任何未限定的标识符。例如,如果您已经定义了 example1 命名空间,则可以通过使用 use namespace example1 来访问该命名空间中的名称:

use namespace example1; 
myFunction();

一次可以打开多个命名空间。在使用 use namespace 打开了某个命名空间之后,它会在打开它的整个代码块中保持打开状态。不能显式关闭命名空间。

但是,如果同时打开多个命名空间则会增加发生名称冲突的可能性。如果您不愿意打开命名空间,则可以用命名空间和名称限定符来限定方法或属性名,从而避免使用 use namespace 指令。例如,下面的代码说明如何用 example1 命名空间来限定 myFunction() 名称:

example1::myFunction();

使用命名空间

在包含在 ActionScript 3.0 中的 flash.utils.Proxy 类中,可以找到用来防止名称冲突的命名空间的实际示例。Proxy 类取代了 ActionScript 2.0 中的 Object.__resolve 属性,可用来截获对未定义的属性或方法的引用,以免发生错误。为避免名称冲突,Proxy 类的所有方法都位于 flash_proxy 命名空间中。

为了更好地了解 flash_proxy 命名空间的使用方法,您需要了解如何使用 Proxy 类。Proxy 类的功能仅对于继承它的类可用。换言之,如果您要对某个对象使用 Proxy 类的方法,则该对象的类定义必须扩展 Proxy 类。例如,如果您希望截获对未定义的方法的调用,则应扩展 Proxy 类,然后覆盖 Proxy 类的 callProperty() 方法。

前面已讲到,实现命名空间的过程通常分为三步,即定义、应用然后引用命名空间。但是,由于您从不显式调用 Proxy 类的任何方法,因此 flash_proxy 命名空间只会被定义和应用,而不会被引用。ActionScript 3.0 定义 flash_proxy 命名空间并在 Proxy 类中应用它。在您的代码中,只需要将 flash_proxy 命名空间应用于扩展 Proxy 类的类。

flash_proxy 命名空间按照与下面类似的方法在 flash.utils 包中定义:

package flash.utils 
{ 
    public namespace flash_proxy; 
}

该命名空间将应用于 Proxy 类的方法,如下面摘自 Proxy 类的代码所示:

public class Proxy 
{ 
    flash_proxy function callProperty(name:*, ... rest):* 
    flash_proxy function deleteProperty(name:*):Boolean 
    ... 
}

如下面的代码所示,您必须先导入 Proxy 类和 flash_proxy 命名空间。随后必须声明自己的类,以便它对 Proxy 类进行扩展(如果是在严格模式下进行编译,则还必须添加 dynamic 属性)。在覆盖 callProperty() 方法时,必须使用 flash_proxy 命名空间。

package 
{ 
    import flash.utils.Proxy; 
    import flash.utils.flash_proxy; 
 
    dynamic class MyProxy extends Proxy 
    { 
        flash_proxy override function callProperty(name:*, ...rest):* 
        { 
            trace("method call intercepted: " + name); 
        } 
    } 
}

如果您创建 MyProxy 类的一个实例,并调用一个未定义的方法(如在下面的示例中调用的 testing() 方法),则 Proxy 对象会截获对该方法的调用,并执行覆盖后的 callProperty() 方法内部的语句(在本例中为一个简单的 trace() 语句)。

var mySample:MyProxy = new MyProxy(); 
mySample.testing(); // method call intercepted: testing

将 Proxy 类的方法放在 flash_proxy 命名空间内部有两个好处。第一个好处是,在扩展 Proxy 类的任何类的公共接口中,拥有单独的命名空间可提高代码的可读性。(在 Proxy 类中大约有 12 个可以覆盖的方法,所有这些方法都不能直接调用。将所有这些方法都放在公共命名空间中可能会引起混淆。)第二个好处是,当 Proxy 子类中包含名称与 Proxy 类方法的名称匹配的实例方法时,使用 flash_proxy 命名空间可避免名称冲突。例如,您可能希望将自己的某个方法命名为 callProperty() 。下面的代码是可接受的,因为您所用的 callProperty() 方法位于另一个命名空间中:

dynamic class MyProxy extends Proxy 
{ 
    public function callProperty() {} 
    flash_proxy override function callProperty(name:*, ...rest):* 
    { 
        trace("method call intercepted: " + name); 
    } 
}

当您希望以一种无法由四个访问控制说明符( public private internal protected )实现的方式提供对方法或属性的访问时,命名空间也可能会非常有用。例如,您可能有几个分散在多个包中的实用程序方法。您希望这些方法对于您的所有包均可用,但是不希望这些方法成为公共方法。为此,您可以创建一个命名空间,并将它用作您自己的特殊访问控制说明符。

下面的示例使用用户定义的命名空间将两个位于不同包中的函数组合在一起。通过将这两个函数组合到同一个命名空间中,可以通过一条 use namespace 语句使这两个函数对于某个类或某个包可见。

本示例使用四个文件来说明此方法。所有的文件都必须位于您的类路径中。第一个文件 (myInternal.as) 用来定义 myInternal 命名空间。由于该文件位于名为 example 的包中,因此您必须将该文件放在名为 example 的文件夹中。该命名空间标记为 public ,因此可以导入到其他包中。

// myInternal.as in folder example 
package example 
{ 
    public namespace myInternal = "http://www.adobe.com/2006/actionscript/examples"; 
}

第二个文件 (Utility.as) 和第三个文件 (Helper.as) 定义的类中包含应可供其他包使用的方法。Utility 类位于 example.alpha 包中,这意味着该文件应放在 example 文件夹下的 alpha 子文件夹中。Helper 类位于 example.beta 包中,这意味着该文件应放在 example 文件夹下的 beta 子文件夹中。这两个包(example.alpha 和 example.beta)在使用命名空间之前必须先导入它。

// Utility.as in the example/alpha folder 
package example.alpha 
{ 
    import example.myInternal; 
     
    public class Utility 
    { 
        private static var _taskCounter:int = 0; 
     
        public static function someTask() 
        { 
            _taskCounter++; 
        } 
         
        myInternal static function get taskCounter():int 
        { 
            return _taskCounter; 
        } 
    } 
} 
 
// Helper.as in the example/beta folder 
package example.beta 
{ 
    import example.myInternal; 
     
    public class Helper 
    { 
        private static var _timeStamp:Date; 
         
        public static function someTask() 
        { 
            _timeStamp = new Date(); 
        } 
         
        myInternal static function get lastCalled():Date 
        { 
            return _timeStamp; 
        } 
    } 
}

第四个文件 (NamespaceUseCase.as) 是主应用程序类,应是 example 文件夹的同级。在 Flash Professional 中,此类用作 FLA 的文档类。NamespaceUseCase 类还导入 myInternal 命名空间,并使用该命名空间调用两个位于其他包中的静态方法。在本示例中,使用静态方法的目的仅在于简化代码。在 myInternal 命名空间中既可以放置静态方法也可以放置实例方法。

// NamespaceUseCase.as 
package 
{ 
    import flash.display.MovieClip; 
    import example.myInternal; // import namespace 
    import example.alpha.Utility;// import Utility class 
    import example.beta.Helper;// import Helper class 
     
    public class NamespaceUseCase extends MovieClip 
    { 
        public function NamespaceUseCase() 
        { 
            use namespace myInternal; 
             
            Utility.someTask(); 
            Utility.someTask(); 
            trace(Utility.taskCounter); // 2 
             
            Helper.someTask(); 
            trace(Helper.lastCalled); // [time someTask() was last called] 
        } 
    } 
}