本章提供有关动态实体如何工作的基本信息。讨论了以下主题:
- 使用JSON简单构造函数
- 使用动态表达式和点语法
- 使用
%Set()
、%Get()
和%Remove()
- 方法链
- 错误处理
- 将动态实体和JSON互转
- 将大型动态实体序列化为流
2.1 使用JSON简单构造函数
动态实体是 %DynamicObject
或 %DynamicArray
的实例,旨在将JSON数据操作无缝集成到ObjectScript应用程序中。尽管您可以使用标准的 %New()
方法创建这些类的实例,但动态实体支持一组更灵活和直观的构造函数。JSON简单构造函数允许您通过将JSON字符串直接分配给变量来创建动态实体。例如,以下代码创建 %DynamicObject
, %DynamicArray
的空实例:
set dynamicObject = {}
set dynamicArray = []
write dynamicObject,!,dynamicArray
3@%Library.DynamicObject
1@%Library.DynamicArray
与 %New()
构造函数不同,简单构造函数 {}
和 []
可以接受JSON格式的字符串作为参数。例如,以下代码使用名为prop1的属性创建动态对象:
set dynamicObject = {"prop1":"a string value"}
write dynamicObject.prop1
a string value
事实上,JSON简单构造函数 {}
和 []
可以用于指定任何有效的JSON数组或对象结构。简单地说,任何有效的JSON文本字符串也是有效的ObjectScript表达式,其计算结果为动态实体。
注:
必须始终引用JSON属性名称
JSON语言规范(请参见https://json.org/)是Javascript对象表示法的一个子集,并在某些领域实施更严格的规则。一个重要的区别是,JSON规范要求所有属性名称都用双引号括起来。另一方面,Javascript语法在许多情况下允许使用未加引号的名称。
动态实体存储JSON字符串中每个对象属性或数组元素的精确表示。任何动态实体都可以使用 %ToJSON()
方法将存储的数据作为JSON字符串返回。在转换为文字字符串或从文字字符串转换时,数据不会丢失或损坏。下面的示例创建一个动态数组,然后调用 %ToJSON()
来构造并返回表示存储数据的新JSON字符串:
set dynamicArray = [[1,2,3],{"A":33,"a":"lower case"},1.23456789012345678901234,true,false,null,0,1,""]
write dynamicArray.%ToJSON()
[[1,2,3],{"A":33,"a":"lower case"},1.23456789012345678901234,true,false,null,0,1,""]
此动态数组存储并返回了几个有效值:
- 前两个元素是嵌套数组和嵌套对象。在JSON语法中,数组和对象结构可以嵌套到任何深度。
- 属性名称区分大小写。嵌套对象有两个不同的属性,分别名为“A”和“a”。
- 第三个值是非常高精度的十进制数。如果将该值存储为标准浮点数,则该值将向下舍入,但动态数组保留了原始值的精确表示。
- 最后六个元素包含JSON数据类型值
true
、false
和null
,以及相应的ObjectScript值0
、1
和""
。同样,动态实体保留每个值的精确表示。
2.2 使用动态表达式和点语法
在JSON中存储值的方式与在ObjectScript中表达值的方式之间存在显著差异。如果每次需要使用ObjectScript值时都必须将其转换为JSON语法或将其转换成JSON语法,则JSON数据存储将非常有用,因此动态实体的设计目的是使转换过程透明。您可以始终存储和检索ObjectScript值,而不必担心它在JSON语法中的表示。
文本JSON构造函数也不例外。到目前为止,我们的所有示例都完全采用JSON语法,但文字构造函数也可以接受动态表达式中定义的值,这些表达式只是用括号括起来的ObjectScript表达式。
例如,以下动态数组构造函数存储两个Unicode字符。在运行时,文本构造函数计算每个元素并存储计算值。第一个元素是用JSON语法定义的,第二个元素是ObjectScript函数调用,但生成的存储值是相同的:
write ["\u00E9",($CHAR(233))].%ToJSON()
["é","é"]
您可以将ObjectScript表达式视为set语句右侧的代码。任何求值为值而不是对象引用的ObjectScript表达式都可以序列化为JSON文本字符串。以下示例在对象属性obj.LIST
中存储一个$LIST
值(它是一个分隔字符串,而不是一个对象)。然后创建数组并将obj.LIST
的每个列表项提取到一个单独的元素中:
set obj = {"list":($LISTFROMSTRING("Deborah Noah Martha Bowie"," "))}
set array = [($LIST(obj.list,1)),($LIST(obj.list,2)),($LIST(obj.list,3)),($LIST(obj.list,4))]
write obj.%ToJSON(),!,array.%ToJSON()
{"list":"\t\u0001Deborah\u0006\u0001Noah\b\u0001Martha\u0007\u0001Bowie"}
["Deborah","Noah","Martha","Bowie"]
您不能使用动态表达式定义属性名(尽管有一些方法可以通过编程方式定义属性名。有关详细信息,请参阅“使用%Set()
、%Get()
和%Remove()
”)。
当然,文字构造函数并不是操作对象属性和数组元素的唯一方法。例如,以下代码创建了一个空的动态对象,并使用标准的对象点语法来定义内容:
set dynArray = []
set dynArray."0" = "02"_"33"
set dynArray."1" = {}
set dynArray."1".foo = $CHAR(dynArray."0")
write dynArray.%ToJSON()
[233,{"foo":"é"}]
在本例中,文字构造函数仅用于创建空的动态实体。赋值语句遵循几个简单的规则:
- 指定的值是标准的ObjectScript表达式。(在本例中,“02”_“33”是一个ObjectScript字符串表达式,计算结果为整数233)。
- 数组元素由数组索引编号寻址,索引编号必须是用双引号括起来的数字文本。动态数组是从零开始的。
- 对象属性由属性名称寻址。尽管属性名是字符串,但如果属性名是有效的类成员名,则双引号是可选的。
- 如果指定的实体成员尚不存在,则将在为其分配值时创建它。
如前所述,值始终以ObjectScript格式存储和检索,而不管它们如何以JSON语法表示。下面的示例演示了使用点语法时应注意的其他一些事实。
(1)使用点语法创建动态对象属性:
本例使用文本构造函数和点语法创建动态对象 dynObj
,其中包含名为A、a和C引号的属性。在文本字符串中,必须引用所有属性名称。在set语句和write语句中,属性名A或a不需要引号,但必须用于C引号:
set dynObj = {"a":"stuff"}
set dynObj."C quote" = " ""C quote"" contains a space "
set dynObj.a = " lower case ""a"" "
set dynObj.A = " upper case ""A"" "
write !,dynObj.%ToJSON()
{"a":" lower case \"a\" ","C quote":" \"C quote\" contains a space ","A":" upper case \"A\" "}
动态对象是无序列表,因此值不一定按创建顺序存储。请参阅“使用%GetNext()在动态实体上迭代”,以了解演示这一点的示例。
(2)使用点语法创建动态数组元素:
动态数组是从零开始的。此示例在定义元素2之前为数组元素3赋值。元素不必按顺序定义,元素2可能未定义。有关详细信息,请参阅“了解稀疏阵列和未分配值”。
set dynArray = [true,false]
set dynArray."3" = "three"
set dynArray."2" = 0
write dynArray.%ToJSON()
[true,false,0,"three"]
尽管前两个元素被定义并存储为JSON布尔值true
和false
,但它们被返回为整数1
和0
,这是等效的ObjectScript布尔值:
write "0=/"_dynArray."0"_"/, 1=/"_dynArray."1"_"/, 2=/"_dynArray."2"_"/, 3=/"_dynArray."3"_"/"
0=/1/, 1=/0/, 2=/0/, 3=/three/
由于存储的值始终以ObjectScript格式返回,JSON true
、false
和null
将作为ObjectScript 0
、1和""
(空字符串)返回。但是,原始JSON值保留在动态实体中,如果需要可以恢复。有关标识存储值的原始数据类型的信息,请参阅“使用数据类型”。
注:点语法不应与非常长的属性名称一起使用。
尽管动态对象属性可以具有任意长度的名称,但ObjectScript不能使用超过180个字符的属性名称。如果动态对象属性名称超过此限制,则尝试在点语法中使用该名称将导致误导性的错误,即使该属性存在且名称有效。可以使用%Set()和%Ge()方法来避免此错误,它们接受任意长度的属性名。
2.3 使用%Set()
、%Get()
和%Remove()
虽然文字构造函数和点语法可以用于创建动态实体成员和操作值,但它们并不适合所有用途。动态实体提供 %Set()
、 %Get()
和 %Remove()
方法,以实现对创建、读取、更新和删除操作的完全编程控制。
这些方法最重要的优点之一是成员标识符(属性名和数组索引号)不必是文本。可以使用ObjectScript变量和表达式来指定值和标识符。
(1)使用 %Set()
、 %Get()
和 %Remove()
以编程方式指定值和标识符:
下面的示例使用文本构造函数 {}
创建一个对象,并调用新对象的 %Set()
方法来添加一系列名为propn的值为100+n的属性。名称和值都由ObjectScript表达式定义:
set dynObj = {}
for i=1:1:5 { do dynObj.%Set("prop"_i,100+i) }
write dynObj.%ToJSON()
{"prop1":101,"prop2":102,"prop3":103,"prop4":104,"prop5":105}
相同的变量可以与 %Get()
一起使用以检索属性值:
for i=1:1:5 { write dynObj.%Get("prop"_i)_" " }
101 102 103 104 105
%Remove()
方法从动态实体中删除指定的成员并返回值。此示例删除五个属性中的三个,并将返回值连接到字符串removedValues。write语句显示已删除值的字符串和dynObj的当前内容:
set removedValues = ""
for i=2:1:4 { set removedValues = removedValues_dynObj.%Remove("prop"_i)_" " }
write "Removed values: "_removedValues,!,"Remaining properties: "_dynObj.%ToJSON()
Removed values: 102 103 104
Remaining properties: {"prop1":101,"prop5":105}
注:
尽管在这些简单的示例中使用了for循环,但正常的迭代方法应该是 %GetNext()
(稍后在“使用%GetNext()在动态实体上迭代”中描述)。
%Get()
和 %Remove()
都返回指定成员的ObjectScript值,但返回嵌入动态实体的方式有一个重要的区别:
%Get()
通过引用返回值。返回值是对属性或元素的OREF(对象引用),它又包含对嵌入实体的引用。%Remove()
销毁指定的属性或元素(使成员OREF无效),但返回一个直接指向先前嵌入实体的有效OREF。
(2)使用%Get()
和%Remove()
检索嵌套的动态实体:
在以下示例中,属性 dynObj.address
的值是一个动态对象。%Get()
语句在变量addrPointer中存储对属性(而不是属性值)的引用。此时,addrPointer可用于访问嵌入实体地址的道路属性:
set dynObj = {"name":"greg", "address":{"road":"Old Road"}}
set addrPointer = dynObj.%Get("address")
set dynObj.address.road = "New Road"
write "Value of "_addrPointer_" is "_addrPointer.road
Value of 2@%Library.DynamicObject is New Road
%Remove()
语句破坏该属性并向属性值返回新的OREF。
set addrRemoved = dynObj.%Remove("address")
write "OREF of removed property: "_addrPointer,!,"OREF returned by %Remove(): "_addrRemoved
OREF of removed property: 2@%Library.DynamicObject
OREF returned by %Remove(): 3@%Library.DynamicObject
在调用 %Remove()
之后,addrRemoved
包含对以前嵌入的动态对象的有效OREF。
write addrRemoved.%ToJSON()
{"road":"New Road"}
您可以使用 %Remove()
方法以任何顺序删除成员。这对对象和数组有不同的含义,如下例所示。
(3)删除对象属性:
对象属性没有固定的顺序。这意味着可以按任何顺序销毁属性,但删除属性并添加另一个属性也可能会更改序列化和返回属性的顺序。下面的示例创建了一个动态对象,并通过对 %Set()
的三次连续调用定义了三个属性:
set dynObject={}.%Set("propA","abc").%Set("PropB","byebye").%Set("propC",999)
write dynObject.%ToJSON()
{"propA":"abc","PropB":"byebye","propC":999}
现在调用 %Remove()
来销毁属性PropB
,然后添加新属性PropD
。生成的动态对象不会按创建顺序序列化其属性:
do dynObject.%Remove("PropB")
set dynObject.propD = "added last"
write dynObject.%ToJSON()
{"propA":"abc","propD":"added last","propC":999}
这也会影响迭代器方法 %GetNext()
返回属性的顺序。有关使用 %GetNext()
的类似示例,请参阅使用 %GetNext()
对动态实体进行迭代。
(4)删除数组元素:
数组是从零开始的有序列表。当您对一个元素调用 %Remove()
时,该元素之后的所有元素的数组索引编号都将减1。以下示例连续三次调用 %Remove(1)
,每次都删除一个不同的元素:
set dynArray = ["a","b","c","d","e"]
set removedValues = ""
for i=1:1:3 { set removedValues = removedValues_dynArray.%Remove(1)_" " }
write "Removed values: "_removedValues,!,"Array size="_dynArray.%Size()_": "_dynArray.%ToJSON()
Removed values: b c d
Array size=2: ["a","e"]
堆栈操作通常使用 %Push()
和 %Pop()
而不是 %Set()
和 %Remove()
来实现,但您可以通过将 %Pop()
替换为%Remove(0)
来实现队列(请参阅“将%Push
和%Pop
用于动态数组”)。
%Remove()
对所有数组的工作方式相同,包括包含未定义值的元素的数组。有关演示 %Remove()
如何处理稀疏数组的示例,请参阅“了解稀疏数组和未分配值”。
2.3.1 将动态实体指定为特性值
可以使用 %Set()
或 %Push()
将动态实体嵌套在另一个动态实体中。例如,可以将动态对象指定为属性值或数组元素。本章前面的示例演示了如何检索嵌套对象(请参阅“使用%Get()
和%Remove()
检索嵌套动态实体”)。下面的示例演示了创建嵌套对象的一种方法。
将动态实体指定为属性值
此示例创建了一个名为myData的属性的动态对象,该属性的值为另一个动态对象:
{"myData":{"myChild":"Value of myChild"}}
以下代码创建此对象。不需要将 %Set()
参数指定为变量,但这样做将允许您在运行时分配任何有效的名称或值:
set mainObj = {}
set mainPropName="myData"
set nestedObj = {}
set nestedPropName="myChild"
set nestedPropValue="Value of myChild"
do nestedObj.%Set(nestedPropName, nestedPropValue)
do mainObj.%Set(mainPropName,nestedObj)
write mainObj.%ToJSON()
此代码生成以下输出:
USER>write mainObj.%ToJSON()
{"myData":{"myChild":"Value of myChild"}}
注:
不要将类型参数与对象值一起使用
%Set()
方法有一个可选的类型参数,允许您在某些有限的情况下指定值参数的数据类型(请参阅“用%Set()
或%Push()
重写默认数据类型”)。当值参数是动态实体时,不能使用类型参数。如果尝试这样做,将引发错误。
2.4 方法链
%Set()
和 %Push()
方法返回对它们修改的实体的引用。返回的引用可以立即用于在同一表达式内调用同一实体上的另一个方法。
开始链的动态实体可以是构造函数( {}
或 []
)或现有实体。方法 %Set()
和 %Push()
返回可链接的引用,可以从链中的任何位置调用。链中的最后一项可以是实体可用的任何方法。
在以下示例中,单个write语句使用对 %FromJSON()
、 %Set()
、 %Push()
和 %ToJSON()
的链接调用来创建、修改和显示动态数组:
set jstring = "[123]"
write [].%FromJSON(jstring).%Set(1,"one").%Push("two").%Push("three").%Set(1,"final value").%ToJSON()
[123,"final value","two","three"]
%FromJSON()
仅作为链中的第一个方法调用有用,因为它不会返回调用实体的修改版本。相反,它只是忽略调用实体并返回一个从JSON字符串反序列化的全新实例。有关更多信息,请参阅“将动态实体和JSON互转”。
您还可以通过使用 %Get()
、 %Pop()
、 %CetNext()
或 %Remove()
检索嵌套实体来启动链。
2.5 错误处理
动态实体在发生错误时抛出异常,而不是返回%Status
。在以下示例中,抛出的异常包含足够的信息,可以断定方法参数中的第二个字符无效:
set invalidObject = {}.%FromJSON("{:}")
<THROW>%FromJSON+37^%Library.DynamicAbstractObject.1 *%Exception.General Parsing error 3 Line 1 Offset 2
在处理动态数据时,最好假设某些数据不符合您的期望。任何使用动态对象的代码都应该在某个级别上用 TRY-CATCH
块包围(请参阅使用ObjectScript中的“TRY-CATCH
机制”)。例如:
TRY {
set invalidObject = {}.%FromJSON("{:}")
}
CATCH errobj {
write errobj.Name_", "_errobj.Location_", error code "_errobj.Code,!
RETURN
}
Parsing error, Line 1 Offset 2, error code 3
2.6 将动态实体和JSON互转
可以使用 %ToJSON()
方法序列化动态实体(将其转换为JSON字符串),使用 %FromJSON()
和 %FromJSONFile()
方法反序列化(将JSON转换为动态实体)。
(1)将动态实体序列化为JSON:
以下示例创建并修改动态对象,然后使用 %ToJSON()
对其进行序列化并显示结果字符串:
set dynObject={"prop1":true}.%Set("prop2",123).%Set("prop3","foo")
set objString = dynObject.%ToJSON()
write objString
{"prop1":true,"prop2":123,"prop3":"foo"}
动态数组的序列化方式相同:
set dynArray=[].%Push("1st value").%Push("2nd value").%Push("3rd value")
set arrayString = dynArray.%ToJSON()
write arrayString
["1st value","2nd value","3rd value"]
这两个示例都使用方法链接(请参阅本章前面的“方法链接”)。
(2)将JSON反序列化为动态对象:
%FromJSON()
方法将JSON字符串转换为动态实体。下面的示例构造一个动态数组并将其序列化为字符串jstring。对 %FromJSON()
的调用将jstring反序列化为名为newArray的新动态实体,然后对其进行修改并显示:
set jstring=["1st value","2nd value","3rd value"].%ToJSON()
set newArray={}.%FromJSON(jstring)
do newArray.%Push("new value")
write "New entity:"_newArray.%ToJSON()
New entity:["1st value","2nd value","3rd value","new value"]
请注意,此示例从动态对象构造函数( {}
)调用 %FromJSON()
,即使返回的值是动态数组。 %FromJSON()
是 %DynamicAbstractObject
的类方法,因此可以从任何动态实体或构造函数调用。
如果JSON数据存储在.JSON文件中,则可以使用 %FromJSONFile()
方法而不是 %FromJSON()
来反序列化数据。
(3)使用 %ToJSON()
和 %FromJSON()
:
由于每次调用 %FromJSON()
都会创建一个新的动态实体,因此它可以用于复制现有实体或初始化一组相同的实体。
在以下示例中,属性 dynObj.address
的值是一个动态对象。该属性由变量 addrPointer
引用,通过调用 %FromJSON()
创建新的动态对象 addrClone
来克隆属性值:
set addrPointer.road = "Wright Ave."
set addrClone.road = "Sinister Ave."
write !,"Property = "_dynObj.address.%ToJSON(),!,"Clone = "_addrClone.%ToJSON()
Property = {"road":"Wright Ave."}
Clone = {"road":"Sinister Ave."}
如果JSON数据存储在.JSON文件中,则可以使用 %FromJSONFile()
方法而不是 %FromJSON()
方法克隆数据。
2.6.1 将大型动态实体序列化为流
如果动态实体足够大,%ToJSON()
的输出可能会超过字符串的最大长度(请参阅”服务器端编程方向指南“中的“字符串长度限制”)。本节中的示例使用名为longStr
的最大长度字符串。以下代码片段演示如何生成longStr:
set longStr=""
for i=1:1:$SYSTEM.SYS.MaxLocalLength() { set longStr = longStr_"x" }
write "Maximum string length = "_$LENGTH(longStr)
Maximum string length = 3641144
当表达式使用 %ToJSON()
的返回值时,字符串将构建在程序堆栈上,该程序堆栈受字符串长度限制。例如,读/写语句,如 write dyn.%ToJSON()
或赋值语句,如 set x=dyn.%ToJSON()
将尝试将字符串放在堆栈上。以下示例将longStr的两个副本添加到动态数组,并尝试将序列化字符串分配给变量,导致ObjectScript返回错误:
set longArray = [(longStr),(longStr)]
set tooBig = longArray.%ToJSON()
SET tooBig = longArray.%ToJSON()
^
<MAXSTRING>
这个问题的一般解决方案是在DO命令中通过引用传递 %ToJSON()
输出,而不实际检查返回值。输出直接写入当前设备,输出长度没有限制。在以下示例中,设备是一个流。
(1)写入文件流:
此示例将动态对象longObject
写入文件,然后检索它。变量longStr是本节开头定义的值:
set longObject = {"a":(longStr),"b":(longStr)}
set file=##class(%File).%New("c:\temp\longObjectFile.txt")
do file.Open("WSN")
do longObject.%ToJSON(file)
do file.Close()
do file.Open("RS")
set newObject = {}.%FromJSONFile(file)
write !,"Property newObject.a is "_$LENGTH(newObject.a)_" characters long."
Property newObject.a is 3641144 characters long.
此解决方案还可用于从其他流读取输入。
(2)读取和写入全局字符流:
在本例中,我们序列化了两个大型动态实体(使用临时流,因为 %ToJSON()
每个流只能序列化一个实体)。标准流处理方法用于将每个临时流存储为流bigLines中的单独行:
set tmpArray = ##class(%Stream.GlobalCharacter).%New()
set dyn = [(longStr),(longStr)]
do dyn.%ToJSON(tmpArray)
set tmpObject = ##class(%Stream.GlobalCharacter).%New()
set dyn = {"a":(longStr),"b":(longStr),"c":(longStr)}
do dyn.%ToJSON(tmpObject)
set bigLines = ##class(%Stream.GlobalCharacter).%New()
do bigLines.CopyFrom(tmpArray)
do bigLines.WriteLine()
do bigLines.CopyFrom(tmpObject)
稍后,我们可以从bigLines反序列化每个动态实体:
do bigLines.Rewind()
while ('bigLines.AtEnd) {
write !,{}.%FromJSON(bigLines.ReadLineIntoStream())
}
7@%Library.DynamicArray
7@%Library.DynamicObject