多维数组
多维数组将其它数组作为其元素。例如,考虑一个任务列表,它存储为字符串索引数组:
var tasks:Array = ["wash dishes", "take out trash"];
如果要将一周中每天的任务存储为一个单独的列表,可以创建一个多维数组,一周中的每天使用一个元素。每个元素包含一个与 tasks
数组类似的索引数组,而该索引数组存储任务列表。在多维数组中,可以使用任意组合的索引数组和关联数组。以下部分中的示例使用了两个索引数组或由索引数组组成的关联数组。练习的时候,您也许需要采用其它组合。
子主题
两个索引数组
使用两个索引数组时,可以将结果呈现为表或电子表格。第一个数组的元素表示表的行,第二个数组的元素表示表的列。
例如,以下多维数组使用两个索引数组跟踪一周中每一天的任务列表。第一个数组 masterTaskList
是使用 Array 类构造函数创建的。此数组中的各个元素分别表示一周中的各天,其中索引 0 表示星期一,索引 6 表示星期日。可将这些元素当成是表的行。可通过为 masterTaskList
数组中创建的七个元素中的每个元素分配数组文本来创建每一天的任务列表。这些数组文本表示表的列。
var masterTaskList:Array = new Array(); masterTaskList[0] = ["wash dishes", "take out trash"]; masterTaskList[1] = ["wash dishes", "pay bills"]; masterTaskList[2] = ["wash dishes", "dentist", "wash dog"]; masterTaskList[3] = ["wash dishes"]; masterTaskList[4] = ["wash dishes", "clean house"]; masterTaskList[5] = ["wash dishes", "wash car", "pay rent"]; masterTaskList[6] = ["mow lawn", "fix chair"];
可以使用括号记号访问任意任务列表中的单个项。第一组括号表示一周的某一天,第二组括号表示这一天的任务列表。例如,要检索星期三的列表中的第二项任务,请首先使用表示星期三的索引 2,然后使用表示列表中的第二项任务的索引 1。
trace(masterTaskList[2][1]); // 输出:dentist
要检索星期日的任务列表中的第一项,请使用表示星期日的索引 6 和表示列表中的第一项任务的索引 0。
trace(masterTaskList[6][0]); // 输出:mow lawn
具有索引数组的关联数组
要使单个数组的访问更加方便,可以使用关联数组表示一周的各天并使用索引数组表示任务列表。通过使用关联数组可以在引用一周中特定的一天时使用点语 法,但要访问关联数组的每个元素还需额外进行运行时处理。以下示例使用关联数组作为任务列表的基础,并使用键和值对来表示一周中的每一天:
var masterTaskList:Object = new Object(); masterTaskList["Monday"] = ["wash dishes", "take out trash"]; masterTaskList["Tuesday"] = ["wash dishes", "pay bills"]; masterTaskList["Wednesday"] = ["wash dishes", "dentist", "wash dog"]; masterTaskList["Thursday"] = ["wash dishes"]; masterTaskList["Friday"] = ["wash dishes", "clean house"]; masterTaskList["Saturday"] = ["wash dishes", "wash car", "pay rent"]; masterTaskList["Sunday"] = ["mow lawn", "fix chair"];
点语法通过避免使用多组括号改善了代码的可读性。
trace(masterTaskList.Wednesday[1]); // 输出:dentist trace(masterTaskList.Sunday[0]); // 输出:mow lawn
可以使用 for..in
循环来循环访问任务列表,但必须使用括号记号来访问与每个键关联的值,而不是使用点语法。由于 masterTaskList
为关联数组,因而不一定会按照您所期望的顺序检索元素,如以下示例所示:
for (var day:String in masterTaskList) { trace(day + ": " + masterTaskList[day]) } /* output: Sunday: mow lawn,fix chair Wednesday: wash dishes,dentist,wash dog Friday: wash dishes,clean house Thursday: wash dishes Monday: wash dishes,take out trash Saturday: wash dishes,wash car,pay rent Tuesday: wash dishes,pay bills */
克隆数组
Array 类不具有复制数组的内置方法。可以通过调用不带参数的 concat()
或 slice()
方法来创建数组的“浅副本”。
在浅副本中,如果原始数组具有对象元素,则仅复制指向对象的引用而非对象本身。与原始数组一样,副本也指向
相同的对象。对对象所做的任何更改都会在两个数组中反映出来。
在“深副本”中,将复制原始数组中的所有对象,从而使新数组和原始数组指向不同的对象。深度复制需要多行代码,
通常需要创建函数。可以将此类函数作为全局实用程序函数或 Array 子类的方法来进行创建。
以下示例定义一个名为 clone()
的函数以执行深度复制。其算法采用了一般的 Java 编程技巧。此函数创建深副本的
方法是:将数组序列化为 ByteArray 类的实例,然后将此数组读回到新数组中。此函数接受对象,因此既可以将此
函数用于索引数组,又可以将其用于关联数组,如以下代码所示:
import flash.utils.ByteArray; function clone(source:Object):* { var myBA:ByteArray = new ByteArray(); myBA.writeObject(source); myBA.position = 0; return(myBA.readObject()); }
高级主题
扩展 Array 类
Array 类是少数不是最终类的核心类之一,也就是说您可以创建自己的 Array 子类。本部分提供了创建 Array
子类的示例,并讨论了在创建子类过程中会出现的一些问题。
如前所述,ActionScript 中的数组是不能指定数据类型的,但您可以创建只接受具有指定数据类型的元素的
Array 子类。以下部分中的示例定义名为 TypedArray 的 Array 子类,该子类中的元素限定为具有第一个参
数中指定的数据类型的值。这里的 TypedArray 类仅用于说明如何扩展 Array 类,并不一定适用于生产,这
有以下若干原因。第一,类型检查是在运行时而非编译时进行的。第二,当 TypedArray 方法遇到不匹配时,
将忽略不匹配并且不会引发异常;当然,修改此方法使其引发异常也是很简单的。第三,此类无法防止使用数组
访问运算符将任一类型的值插入到数组中。第四,其编码风格倾向于简洁而非性能优化。
声明子类
可以使用 extends
关键字来指示类为 Array 的子类。与 Array 类一样,Array 的子类应使用 dynamic
属性。
否则,子类将无法正常发挥作用。
以下代码显示 TypedArray 类的定义,该类包含一个保存数据类型的常量、一个构造函数方法和四个能够将元
素添加到数组的方法。此示例省略了各方法的代码,但在以后的部分中将列出这些代码并详加说明:
public dynamic class TypedArray extends Array { private const dataType:Class; public function TypedArray(...args) {} AS3 override function concat(...args):Array {} AS3 override function push(...args):uint {} AS3 override function splice(...args) {} AS3 override function unshift (...args):uint {} }
由于本示例中假定将编译器选项 -as3
设置为 true
,而将编译器选项 -es
设置为 false
,因而这四个被覆盖的方
法均使用 AS3 命名空间而非 public
属性。这些是 Adobe Flex Builder 2 和
Adobe Flash CS3 Professional 的默认设置。有关详细信息,请参阅AS3 命名空间 。
提示 |
| 如果您是倾向于使用原型继承的高级开发人员,您可能会在以下两个方面对 TypedArray 类进行较小的改动,以使其在编译器选项 |
TypedArray 构造函数
由于此子类构造函数必须接受一列任意长度的参数,从而造成了一种有趣的挑战。该挑战就是如何将参数传递给
超类构造函数以创建数组。如果将一列参数作为数组进行传递,则超类构造函数会将其视为 Array 类型的一个参
数,并且生成的数组长度始终只有 1 个元素。传递参数列表的传统处理方式是使用 Function.apply()
方法,此
方法将参数数组作为第二个参数,但在执行函数时将其转换为一列参数。遗憾的是,Function.apply()
方法不能
和构造函数一起使用。
剩下的唯一方法是在 TypedArray 构造函数中重新创建 Array 构造函数的逻辑。以下代码说明在 Array 类构
造函数中使用的算法,您可以在 Array 子类构造函数中重复使用此算法:
public dynamic class Array { public function Array(...args) { var n:uint = args.length if (n == 1 && (args[0] is Number)) { var dlen:Number = args[0]; var ulen:uint = dlen; if (ulen != dlen) { throw new RangeError("Array index is not a 32-bit unsigned integer ("+dlen+")"); } length = ulen; } else { length = n; for (var i:int=0; i < n; i++) { this[i] = args[i] } } } }
TypedArray 构造函数与 Array 构造函数中的大部分代码都相同,只在四个地方对代码进行了改动。其一,
参数列表中新增了一个必需的 Class 类型参数,使用此参数可以指定数组的数据类型。其二,将传递给构造函
数的数据类型分配给 dataType
变量。其三,在 else
语句中,在 for
循环之后为 length
属性赋值,以使
length
只包括相应类型的参数。其四,for
循环的主体使用 push()
方法的被覆盖版本,以便仅将正确数据类型
的参数添加到数组中。以下示例显示 TypedArray 构造函数:
public dynamic class TypedArray extends Array { private var dataType:Class; public function TypedArray(typeParam:Class, ...args) { dataType = typeParam; var n:uint = args.length if (n == 1 && (args[0] is Number)) { var dlen:Number = args[0]; var ulen:uint = dlen if (ulen != dlen) { throw new RangeError("Array index is not a 32-bit unsigned integer ("+dlen+")") } length = ulen; } else { for (var i:int=0; i < n; i++) { // 在 push() 中完成类型检查 this.push(args[i]) } length = this.length; } } }
TypedArray 覆盖的方法
TypedArray 类覆盖前述四种能够将元素添加到数组的方法。在每种情况下,被覆盖方法均添加类型检查,
这种检查可以防止添加不正确数据类型的元素。然后,每种方法均调用其自身的超类版本。
push()
方法使用 for..in
循环来循环访问参数列表,并对每个参数执行类型检查。可以使用 splice()
方法,
从 args
数组中删除所有不是正确类型的参数。在 for..in
循环结束之后,args
数组中将只包含类型为 dataType
的值。然后对更新后的 args
数组调用 push()
的超类版本,如以下代码所示:
AS3 override function push(...args):uint { for (var i:* in args) { if (!(args[i] is dataType)) { args.splice(i,1); } } return (super.push.apply(this, args)); }
concat()
方法创建一个名为 passArgs
的临时 TypedArray 来存储通过类型检查的参数。这样,便可以重复使用
push()
方法中的类型检查代码。for..in
循环用于循环访问 args
数组,并为每个参数调用 push()
。由于将
passArgs
指定为类型 TypedArray,因而将执行 push()
的 TypedArray 版本。然后,concat()
方法将调用
其自身的超类版本,如以下代码所示:
AS3 override function concat(...args):Array { var passArgs:TypedArray = new TypedArray(dataType); for (var i:* in args) { // 在 push() 中完成类型检查 passArgs.push(args[i]); } return (super.concat.apply(this, passArgs)); }
splice()
方法使用任意一列参数,但前两个参数始终指的是索引号和要删除的元素个数。这就是为什么被覆盖的
splice()
方法仅对索引位置 2 或其以后的 args
数组元素执行类型检查。该代码中一个有趣的地方是,for
循环中的 splice()
调用看上去似乎是递归调用,但实际上并不是递归调用,这是由于 args
的类型是 Array
而非 TypedArray,也就是说,args.splice()
调用是对此方法超类版本的调用。在 for..in
循环结束后,
args
数组中将只包含索引位置 2 或其以后位置具有正确类型的值,并且 splice()
会调用其自身的超类版本,
如以下代码所示:
AS3 override function splice(...args):* { if (args.length > 2) { for (var i:int=2; i< args.length; i++) { if (!(args[i] is dataType)) { args.splice(i,1); } } } return (super.splice.apply(this, args)); }
用于将元素添加到数组开头的 unshift ()
方法也可以接受任意一列参数。被覆盖的 unshift ()
方法使用的算法
与 push()
方法使用的算法类似,如以下示例代码所示:
AS3 override function unshift (...args):uint { for (var i:* in args) { if (!(args[i] is dataType)) { args.splice(i,1); } } return (super.unshift .apply(this, args)); } }
示例:PlayList
此 PlayList 示例在管理歌曲列表的音乐播放列表应用程序环境中展示了使用数组的多种技巧。这些方法包括:
- 创建索引数组
- 向索引数组中添加项
- 使用不同的排序选项按照不同的属性对对象数组排序
- 将数组转换为以字符分隔的字符串
要获取该范例的应用程序文件,
请访问 www.adobe.com/go/learn_programmingAS3 samples_flash_cn 。
可以在 Samples/PlayList 文件夹中找到 PlayList 应用程序文件。该应用程序包含以下文件:
文件 | 说明 |
---|---|
PlayList.mxml 或 PlayList.fla | Flash 或 Flex 中的主应用程序文件(分别为 FLA 和 MXML)。 |
com/example/programmingas3 /playlist/Song.as | 表示一首歌曲信息的值对象。由 PlayList 类管理的项为 Song 实例。 |
com/example/programmingas3 /playlist/SortProperty.as | 伪枚举,其可用值代表 Song 类的属性,可以根据这些属性对 Song 对象列表排序。 |
子主题
PlayList 类概述 向列表中添加歌曲 对歌曲列表排序 将数组元素组合为以字符分隔的字符串PlayList 类概述
PlayList 类管理一组 Song 对象。它具有一些公共方法,可将歌曲添加到播放列表(addSong()
方法)以
及对列表中的歌曲排序(sortList()
方法)。此外,它还包括一个只读存取器属性 songList
,可提供对播
放列表中实际歌曲集的访问。在内部,PlayList 类使用私有 Array 变量跟踪其歌曲:
public class PlayList { private var _songs:Array; private var _currentSort:SortProperty = null; private var _needToSort:Boolean = false; ... }
除了 PlayList 类用来跟踪其歌曲列表的 _songs
Array 变量以外,还有另外两个私有变量,分别用来跟踪是
否需要对列表排序 (_needToSort
) 以及在给定时间对列表进行排序所依据的属性 (_currentSort
)。
跟所有对象一样,声明 Array 实例只做了创建 Array 工作的一半。在访问 Array 实例的属性或方法之前,
必须在 PlayList 类的构造函数中完成对它的实例化。
public function PlayList() { this._songs = new Array(); // 设置初始排序。 this.sortList(SortProperty.TITLE); }
构造函数的第一行用于实例化 _songs
变量,以使其处于待用状态。此外,还会调用 sortList()
方法来设置
初始排序所依据的属性。
向列表中添加歌曲
用户在应用程序中输入新歌曲后,数据条目表单上的代码将调用 PlayList 类的 addSong()
方法。
/** * 向播放列表中添加歌曲。 */ public function addSong(song:Song):void { this._songs.push(song); this._needToSort = true; }
在 addSong()
内,将调用 _songs
数组的 push()
方法,以便将传递给 addSong()
的 Song 对象添加为该数
组中的新元素。不管之前应用的是哪种排序方法,使用 push()
方法时,都会将新元素添加到数组末尾。
也就是说,调用 push()
方法后,歌曲列表的排序很可能不正确,因此将 _needToSort
变量设置为 true
。
理论上说,可以立即调用 sortList()
方法,而无需跟踪是否要在给定时间对列表进行排序。实际上,
不需要立即对歌曲列表排序,除非要对其进行检索。通过延迟排序操作,应用程序不会执行不必要的排序,
例如,在将几首歌曲添加到列表中且不需要对其进行检索时。
对歌曲列表排序
由于由 PlayList 管理的 Song 实例为复杂对象,因而此应用程序的用户可能要根据不同的属性(例如,
歌曲名称或发行年份)来对播放列表排序。在 PlayList 应用程序中,对歌曲列表排序的任务由以下三部分
组成:确定列表排序所依据的属性,指出按照该属性排序时应使用的排序操作,以及执行实际排序操作。
排序属性
Song 对象跟踪若干属性,包括歌曲名称、歌手、发行年份、文件名和用户选择的歌曲所属流派。
在这些属性当中,只有前三个可用于排序。为了方便开发人员,此示例中包括 SortProperty 类,
此类与枚举作用相同,其值表示可用于排序的属性。(http://www.my400800.cn )
public static const TITLE:SortProperty = new SortProperty("title"); public static const ARTIST:SortProperty = new SortProperty("artist"); public static const YEAR:SortProperty = new SortProperty("year");
SortProperty 类包含 TITLE
、ARTIST
和 YEAR
三个常量,其中的每个常量都存储一个包含相关 Song
类属性(可用于排序)的实际名称的字符串。在代码的其余部分,只要指出排序属性,都是使用枚举成员
完成的。例如,在 PlayList 构造函数中,最初通过调用 sortList()
方法对列表排序,如下所示:
// 设置初始排序。 this.sortList(SortProperty.TITLE);
由于将排序属性指定为 SortProperty.TITLE
,因而将根据歌曲名称对歌曲排序。
依据属性排序和指定排序选项
对歌曲列表排序的实际工作是由 PlayList 类在 sortList()
方法中进行的,如下所示:
/** * 依据指定属性对歌曲列表排序。 */ public function sortList(sortProperty:SortProperty):void { ... var sortOptions:uint; switch (sortProperty) { case SortProperty.TITLE: sortOptions = Array.CASEINSENSITIVE; break; case SortProperty.ARTIST: sortOptions = Array.CASEINSENSITIVE; break; case SortProperty.YEAR: sortOptions = Array.NUMERIC; break; } // 对数据执行实际的排序操作。 this._songs.sortOn(sortProperty.propertyName, sortOptions); // 保存当前排序属性。 this._currentSort = sortProperty; // 记录列表已排序。 this._needToSort = false; }
如果根据歌名或歌手排序,则应按照字母顺序排序,要根据年份排序,则按照数字顺序排序最为合理。
switch
语句用于根据 sortProperty
参数中指定的值定义合适的排序选项,此选项将存储在 sortOptions
变量中。这里,再次使用命名的枚举成员而非硬编码值来区分不同属性。
确定排序属性和排序选项之后,将通过调用 sortOn()
方法并将上述两个值作为参数传递来完成对 _songs
数组的排序。对歌曲列表进行排序之后,记录当前排序属性。
将数组元素组合为以字符分隔的字符串
在本示例中,数组除了用于在 PlayList 类中维护歌曲列表以外,还用于在 Song 类中帮助管理指定歌曲
所属的流派的列表。请考虑 Song 类定义中的以下代码片断:
private var _genres:String; public function Song(title:String, artist:String, year:uint, filename:String, genres:Array) { ... // 流派作为数组传递进来 // 但存储为以分号分隔的字符串。 this._genres = genres.join(";"); }
创建新 Song 实例时,将把用于指定歌曲所属的一个或多个流派的 genres
参数定义为 Array 实例。这有
助于将多个流派组合为一个可以传递给构造函数的变量。但在内部,Song 类在 _genres
私有变量中以分
号分隔的 String 实例形式来保存流派。通过调用 join()
方法(使用文本字符串值 ";"
作为指定分隔符),
Array 参数将转换为以分号分隔的字符串。
通过使用相同的标记,genres
存取器可将流派作为 Array 进行设置或检索:
public function get genres():Array { // 流派存储为以分号分隔的字符串, // 因此需要将它们转换为 Array 以将它们传递出去。 return this._genres.split(";"); } public function set genres(value:Array):void { // 流派作为数组传递进来, // 但存储为以分号分隔的字符串。 this._genres = value.join(";"); }
genres
set
存取器的行为与构造函数完全相同;它接受 Array 并调用 join()
方法以将其转换为以分号分隔
的 String。get
存取器执行相反的操作:调用 _genres
变量的 split()
方法,使用指定分隔符(采用与前面
相同的文本字符串值 ";"
)将字符串拆分为由值组成的数组。